欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA多线程详解

程序员文章站 2022-03-08 18:13:39
...

什么是线程?

在讲什么是线程之前先说说什么是进程

进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击桌面的IE浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。
要点:用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。

线程与进程的关系

线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。

线程的生命周期及五种基本状态

关于Java中线程的生命周期,首先看一下下面这张较为经典的图:
JAVA多线程详解
上图中基本上囊括了Java中多线程各重要知识点。掌握了上图中的各知识点,Java中的多线程也就基本上掌握了。
主要包括:Java线程具有五种基本状态

  • 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

  • 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

  • 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  • 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才
    有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    • 1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    • 2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    • 3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

Java多线程的创建及启动

java中线程的创建常见有如下四种基本形式:

1、 匿名内部类实现线程

/**
 * @desc 匿名内部类实现线程
 * @author xlosy_skl
 * @date 2018年8月8日12:18:44
 */
public static void main(String args[]){
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            System.out.println("休眠失败。");
        }
        System.out.println(Thread.currentThread().getName()+": run....  ");

    },"匿名内部类实现线程");//新建一个线程对象,处于新建状态
    t.start();//调用线程对象的start方法,处于就绪状态
}

注:这里使用了lambda表达式

2、 继承线程对象实现线程(继承Thread类,重写该类的run()方法)

/**
 * 继承线程对象
 */
public class MyThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            System.out.println("休眠失败。");
        }
        System.out.println(Thread.currentThread().getName()+": MyThread.run");
    }
}

/**
* @desc 继承线程对象实现线程
* @author xlosy_skl
* @date 2018年8月8日12:19:23
*/
public static void main(String args[]){
    MyThread myThread = new MyThread();
    myThread.setName("继承线程对象实现线程");
    myThread.start();
}

如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。

3、实现Runable接口来实现线程 (实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。)

/**
 * @author  xlosy_skl
 * @desc 实现Runable接口
 * @date
 */
public class MyThreadInterface implements Runnable{

    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            System.out.println("休眠失败。");
        }
        System.out.println(Thread.currentThread().getName()+": MyThreadInterface.run");
    }
}


/**
 * @desc 实现Runable接口来实现线程
 * @author xlosy_skl
 * @date 2018年8月8日12:19:23
 */
public static void main(String args[]){
    Thread thread = new MyThread(new MyThreadInterface(),"实现Runable接口实现");
    thread.start();
}

4、使用callable和Future来实现线程 (具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程)

/**
 * @author xlosy_skl
 * @desc 任务类
 * @date 2018年8月8日16:27:13
 */
public class MyCallable implements Callable<Double> {
    @Override
    public Double call() throws Exception {
        return Math.random();
    }
}

/**
 * @desc 使用callable和Futrue来实现线程
 * @author xlosy_skl
 * @date 2018年8月8日12:19:23
 */
public static void main(String args[]){
    MyCallable callable = new MyCallable();
    FutureTask futureTask = new FutureTask(callable);
    Thread calla_t = new Thread(futureTask);
    calla_t.start();
    try {
        System.out.println(futureTask.get());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。

需要特别注意的是:不能对同一线程对象两次调用start()方法。

Java多线程的就绪、运行和死亡状态

就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

由于实际的业务需要,常常会遇到需要在特定时机终止某一线程的运行,使其进入到死亡状态。目前最通用的做法是设置一boolean型的变量,当条件满足时,使线程执行体快速执行完毕。如:

public class ThreadTest {

    public static void main(String[] args) {

        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                thread.start();
            }
            if(i == 40){
                myRunnable.stopThread();
            }
        }
    }
}

class MyRunnable implements Runnable {

    private boolean stop;

    @Override
    public void run() {
        for (int i = 0; i < 100 && !stop; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public void stopThread() {
        this.stop = true;
    }

}

Thread常用方法

  • Thread.yield() :当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
  • Thread.sleep():当前线程暂停一段时间
  • join():在一个线程中调用other.join(),将等待other执行完后才继续本线程。    
  • interrupt():sleep()和join()两个函数皆可以被打断,即当执行这两个函数或者想达到或处于这两个函数执行后的状态皆可以被中断这种状态,wait状态下的线程也可以被中断。

JAVA多线程之wait/notify

① wait() 与 notify/notifyAll 方法必须在同步代码块中使用
wait() 与 notify/notifyAll() 是Object类的方法,在执行两个方法时,要先获得锁。那么怎么获得锁呢?

需要使用synchronized关键字获得锁(具体请查看下面节点:JAVA多线程之Synchronized关键字–对象锁的特点)。因此,wait()与 notify/notifyAll()经常与synchronized搭配使用,即在synchronized修饰的同步代码块或方法里面调用wait() 与 notify/notifyAll()方法。

②wait() 与 notify/notifyAll() 的执行过程

由于 wait() 与 notify/notifyAll()是放在同步代码块中的,因此线程在执行它们时,肯定是进入了临界区中的,即该线程肯定是获得了锁的。当线程执行wait()时,会把当前的锁释放,然后让出CPU,进入等待状态。


当执行notify/notifyAll方法时,会唤醒一个处于等待该 对象锁的线程,然后继续往下执行,直到执行完退出对象锁锁住的区域(synchronized修饰的代码块)后再释放锁

从这里可以看出,notify/notifyAll()执行后,并不立即释放锁,而是要等到执行完临界区中代码后,再释放。故,在实际编程中,我们应该尽量在线程调用notify/notifyAll()后,立即退出临界区。即不要在notify/notifyAll()后面再写一些耗时的代码。

public class Service {

    public void testMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("begin wait() ThreadName="
                        + Thread.currentThread().getName());
                lock.wait();
                System.out.println("  end wait() ThreadName="
                        + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void synNotifyMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("begin notify() ThreadName="
                        + Thread.currentThread().getName() + " time="
                        + System.currentTimeMillis());
                lock.notify();
                Thread.sleep(5000);
                System.out.println("  end notify() ThreadName="
                        + Thread.currentThread().getName() + " time="
                        + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

从上面的代码可以看出,wait() 与 notify/notifyAll()都是放在同步代码块中才能够执行的。如果在执行wait() 与 notify/notifyAll() 之前没有获得相应的对象锁,就会抛出:java.lang.IllegalMonitorStateException异常。

在第8行,当ThreadA线程执行lock.wait();这条语句时,释放获得的对象锁lock,并放弃CPU,进入等待队列。

当另一个线程执行第23行lock.notify();,会唤醒ThreadA,但是此时它并不立即释放锁,接下来它睡眠了5秒钟(sleep()是不释放锁的,事实上sleep()也可以不在同步代码块中调用),直到第28行,退出synchronized修饰的临界区时,才会把锁释放。这时,ThreadA就有机会获得另一个线程释放的锁,并从等待的地方起(第9行)起开始执行。

③中断 调用wait()方法进入等待队列的线程

public class Service {

    public void testMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("begin wait()");
                lock.wait();
                System.out.println("  end wait()");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("出现异常");
        }
    }
}

public class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        Service service = new Service();
        service.testMethod(lock);
    }
}

注意,在第23行wait()方法是Object类的对象lock调用的。而下面的interrupt()方法是ThreadA类的对象调用的。在ThreadA里面,将Object的对象作为参数传给了testMethod()方法,ThreadA的run()方法去调用testMethod(),从而wait()使ThreadA的线程暂停了(暂停当前执行wait()的线程)。从这里可以看出一个区别:

Object类中与线程有关的方法:
1) notify/notifyAll
2) wait()/wait(long)

java.lang.Thread中与之相关的方法:
1) interrupt()
2) sleep()/sleep(long)
3) join()/suspend()/resume()….

测试类代码如下:

public class Test {

    public static void main(String[] args) {
        try {
            Object lock = new Object();

            ThreadA a = new ThreadA(lock);
            a.start();

            Thread.sleep(5000);

            a.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当执行第13行的interrupt()时,处于wait中的线程“立即”被唤醒(一般是立即响应中断请求),并抛出异常。此时,线程也就结束了。

④notify 通知的顺序不能错

假设在线程A中执行wait(),在线程B中执行notify()。但如果线程B先执行了notify()然后结束了,线程A才去执行wait(),那此时,线程A将无法被正常唤醒了(还可以通过③中提到的interrupt()方法以抛出异常的方式唤醒^~^)。

⑤多线程中测试某个条件的变化用 if 还是用 while?

回答是while,为什么呢?那是因为第一个线程执行的时候判断条件成立则进行等待(阻塞),第二个线程判断不成立则进行操作,这时候操作完且改变了第一个线程开始的判断条件,但是第二个线程调用了notify通知了,这时第一个线程没有去重新判断条件,而是直接执行了后面的代码,所以后面的代码执行则会处于不满足条件下执行,固然报错了呀。

以下给个简单的例子进行实践:
Add类,负责添加数据:

public class Add {

    private String lock;
    public Add(String lock) {
        super();
        this.lock = lock;
    }

    public void add() {
        synchronized (lock) {
            ValueObject.list.add("anyString");
            lock.notifyAll();
        }
    }
}

public class ThreadAdd extends Thread {

    private Add p;

    public ThreadAdd(Add p) {
        super();
        this.p = p;
    }

    @Override
    public void run() {
        p.add();
    }
}

Subtract类,负责删除数据—-先要进行条件判断,然后执行wait(),这意味着:wait等待的条件可能发生变化!!!

public class Subtract {

    private String lock;

    public Subtract(String lock) {
        super();
        this.lock = lock;
    }

    public void subtract() {
        try {
            synchronized (lock) {
                if(ValueObject.list.size() == 0) {//将这里的if改成while即可保证不出现越界异常!!!!
                    System.out.println("wait begin ThreadName="
                            + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("wait   end ThreadName="
                            + Thread.currentThread().getName());
                }
                ValueObject.list.remove(0);
                System.out.println("list size=" + ValueObject.list.size());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadSubtract extends Thread {

    private Subtract r;

    public ThreadSubtract(Subtract r) {
        super();
        this.r = r;
    }

    @Override
    public void run() {
        r.subtract();
    }
}

封装的List队列:

public class ValueObject {

    public static List list = new ArrayList();

}

测试类:

public class Run {

    public static void main(String[] args) throws InterruptedException {

        String lock = new String("");

        Add add = new Add(lock);
        Subtract subtract = new Subtract(lock);

        ThreadSubtract subtract1Thread = new ThreadSubtract(subtract);
        subtract1Thread.setName("subtract1Thread");
        subtract1Thread.start();

        ThreadSubtract subtract2Thread = new ThreadSubtract(subtract);
        subtract2Thread.setName("subtract2Thread");
        subtract2Thread.start();

        Thread.sleep(1000);

        ThreadAdd addThread = new ThreadAdd(add);
        addThread.setName("addThread");
        addThread.start();

    }
}

以上代码执行会报越界异常,将Subtract 类的subtract()方法中的if判断改为while,则相当于增加了重检机制重新判断了可执行后面代码的条件,这时判断为不满足则不会执行后面的代码就不会报越界异常了哈。

JAVA多线程之Synchronized关键字–对象锁的特点

所谓对象锁,就是synchronized 给某个对象加锁
synchronized可以修饰实例方法,如下形式:

public class MyObject {
    public synchronized void methodA() {
        //do something....
    }
}

这里,synchronized 关键字锁住的是当前对象。这也是称为对象锁的原因。
为啥锁住当前对象?因为 methodA()是个实例方法,要想执行methodA(),需要以 对象.方法() 的形式进行调用(obj.methodA(),obj是MyObject类的一个对象,synchronized就是把obj这个对象加锁了)。

上面代码也可写成这样:

public class MyObject {
    public void methodA() {
        synchronized(this){
            //do something....
        }
    }
}

使用synchronized关键字同步一个明显的特点是:MyObject类中定义有多个synchronized修饰的实例方法时,若多个线程拥有同一个MyObject类的对象,则这些方法只能以同步的方式执行。即,执行完一个synchronized修饰的方法后,才能执行另一个synchronized修饰的方法。

public class MyObject {

    public synchronized  void methodA() {
        //do something....
    }

    public synchronized void methodB() {
        //do some other thing
    }
}

MyObject类中有两个synchronized修饰的方法。

public class ThreadA extends Thread {

    private MyObject object;
    //省略构造方法
    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

线程A执行methodA()

public class ThreadB extends Thread {

    private MyObject object;
    //省略构造方法
    @Override
    public void run() {
        super.run();
        object.methodB();
    }
}

线程B执行methodB()

public class Run {
    public static void main(String[] args) {
        MyObject object = new MyObject();

        //线程A与线程B 持有的是同一个对象:object
        ThreadA a = new ThreadA(object);
        ThreadB b = new ThreadB(object);
        a.start();
        b.start();
    }
}

由于线程A和线程B持有同一个MyObject类的对象object,尽管这两个线程需要调用不同的方法,但是必须是同步的,比如:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。

从上可以看出,本文中讲述的 synchronized锁的范围是整个对象。如果一个类中有多个synchronized修饰的同步方法,且多个线程持有该类的同一个对象(该类的相同的对象),尽管它们调用不同的方法,各个方法的执行也是同步的。
如果各个同步的方法之间没有共享变量,或者说各个方法之间没有联系,但也只能同步执行,这会影响效率。

使用synchronized避免 因数据不一致性而导致读脏数据的情况

public class MyObject {

    private String userName = "b";
    private String passWord = "bb";

    public synchronized void methodA(String userName, String passWord) {
        this.userName = userName;
        try{
            Thread.sleep(5000);
        }catch(InterruptedException e){

        }
        this.passWord = passWord;
    }

    synchronized public void methodB() {
        System.out.println("userName" + userName + ": " + "passWord" + passWord);
    }
}

methodA()负责更改用户名和密码。在现实中,一个用户名对应着一个密码。。。
methodB()负责读取用户名和密码。

如果methodB()没有用synchronized 修饰,线程A在调用methodA()执行到第7行,更改了用户名,因某种原因(比如在第9行睡眠了)放弃了CPU。

此时,如果线程B去执行methodB(),那么读取到的用户名是线程A更改了的用户名(“a”),但是密码却是原来的密码(“bb”)。因为,线程A睡眠了,还没有来得及更改密码。

但是,如果methodB()用synchronized修饰,那么线程B只能等待线程A执行完毕之后(即改了用户名,也改了密码),才能执行methodB读取用户名和密码。因此,就避免了数据的不一致性而导致的脏读问题。

使用CountDownLatch并发发令枪达到多线程同时执行

CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。对于给定的计数 初始化CountDownLatch,可以调用了 countDown() 方法,在当前计数到达零之前,await()方法会一直受阻塞,当计数到达零时,会释放所有等待的线程,await() 的所有后续调用都将立即返回。但这种现象只出现一次——计数无法被重置,如果需要重置计数,请考虑使用 CyclicBarrier。

CountDownLatch 是一个通用同步工具,它有很多用途,将计数 初始化为1的 CountDownLatch可用作一个简单的开/关锁存器或入口,在通过调用 countDown() 的线程打开入口前,所有调用 await()的线程都一直在入口处等待。用 N 初始化的 CountDownLatch 可以使一个线程在 N个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。

CountDownLatch 的一个有用特性是,它不要求调用 countDown() 方法的线程等到计数到达零时才继续,而在所有线程都能通过之前,它只是阻止任何线程继续通过await(),它比CyclicBarrier有更大的灵活性,它可以控制不确定数目的线程,而不是像CyclicBarrier在确定数目的线程wait()时就会通过,只有当countDown()的值为0时才允许所有的线程通过。

public class CountdownLatchTest {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(3);      
        for(int i=0;i<3;i++){
            Runnable runnable = new Runnable(){
                    public void run(){
                    try {
                        System.out.println("线程" + Thread.currentThread().getName() + 
                                "正准备接受命令");         
                        cdOrder.await();
                        System.out.println("线程" + Thread.currentThread().getName() + 
                        "已接受命令");                               
                        Thread.sleep((long)(Math.random()*10000));  
                        System.out.println("线程" + Thread.currentThread().getName() + 
                                "回应命令处理结果");                
                        cdAnswer.countDown();                       
                    } catch (Exception e) {
                        e.printStackTrace();
                    }               
                }
            };
            service.execute(runnable);
        }       
        try {
            Thread.sleep((long)(Math.random()*10000));

            System.out.println("线程" + Thread.currentThread().getName() + 
                    "即将发布命令");                      
            cdOrder.countDown();
            System.out.println("线程" + Thread.currentThread().getName() + 
            "已发送命令,正在等待结果");    
            cdAnswer.await();
            System.out.println("线程" + Thread.currentThread().getName() + 
            "已收到所有响应结果");   
        } catch (Exception e) {
            e.printStackTrace();
        }               
        service.shutdown();
    }
}
线程pool-1-thread-2正准备接受命令
线程pool-1-thread-3正准备接受命令
线程pool-1-thread-1正准备接受命令
线程main即将发布命令
线程main已发送命令,正在等待结果
线程pool-1-thread-3已接受命令
线程pool-1-thread-2已接受命令
线程pool-1-thread-1已接受命令
线程pool-1-thread-1回应命令处理结果
线程pool-1-thread-3回应命令处理结果
线程pool-1-thread-2回应命令处理结果
线程main已收到所有响应结果