java线程生命周期和多线程间通信wait(),notify(),notifyAll()

JAVA基础知识之多线程——线程的生命周期(状态)

线程有五个状态,分别是新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。

新建和就绪

程序使用new会新建一个线程,new出的对象跟普通对象一样,JVM会为其分配内存,初始化成员变量等,此时线程并没有运行,而是就是新建状态。

当线程对象调用start后,线程将进入就绪状态。JVM会为其创建函数调度栈和计数器,但此时线程依然没有运行,而是等待获取CPU执行片

下面的例子可以证明当线程对象调用start后,并不一定立即执行,

for (int i=0; i<50; i++) {
    System.out.println(Thread.currentThread().getName()+" "+i);
    if(i==20){
        SecondThread st = new SecondThread();
        new Thread(st, "new thread 1").start();
    }
}

上面是在main中创建子线程,下面是运行结果,

...
 
main 18
main 19
main 20
main 21
main 22
new thread 1 0
main 23
new thread 1 1
main 24
new thread 1 2
main 25
 
...

从上面的运行结果可以看到,原本在main中i=20的时候新建了一个子线程,并立即调用了start()使线程进入就绪状态,但是一直等到i=22的时候,子线程才开始运行,所以子线程何时会开始执行取决于CPU执行片的分配,由JVM调度器决定。

运行和阻塞状态

当就绪状态的线程获取了CPU执行片的之后,就进入运行状态,但是在执行过程中,可能会因为以下原因使线程进入阻塞状态,

  • CPU执行片已经用完,JVM切换到其他线程执行
  • 线程调用sleep()
  • 线程调用了阻塞IO方法,该方法返回之前,线程会一直阻塞
  • 线程试图获取被其他线程持有的同步监视器
  • 线程在等待某个通知
  • 程序调用了线程的suspend()将线程挂起。(容易死锁,不推荐)

线程从运行进入阻塞状态之后,接着只能继续阻塞或者再次进入就绪状态,下面情况会使线程由阻塞状态重新进入就绪状态,

  • 线程调用的slee()经过了指定时间
  • 线程调用的阻塞IO方法返回
  • 线程成功获取同步监视器
  • 线程收到其他线程发出的通知
  • 被挂起(suspend)的线程又被程序调用了resume方法

下图演示了线程状态转换过程,

注意从上图可知,

线程从阻塞状态只能进入就绪状态,

通常情况下,就绪状态和运行状态的转换是不受程序控制的,而是由JVM线程调度机制控制的

yield()方法可以让运行状态的线程进入就绪状态

线程死亡

线程结束后就处于死亡状态,线程会以如下三种方式结束,

  • run()或call()正常执行完成,线程正常结束
  • 线程抛出一个未捕获的Exception或Error
  • 直接调用线程的stop()方法结束线程,容易死锁

注意,子线程一旦启动,其地位和主线程是一样的,所以一旦主线程结束了,子线程不会受影响,不会跟着结束

线程对象的isAlive()方法在就绪,运行,阻塞时返回true,在新建,死亡时返回false

对已经死亡的线程调用start()是无效的,会抛出异常。 死亡的线程不可再次作为线程来执行。

对于新建的线程,调用两次start()方法也会抛出异常


多线程间通信wait(),notify(),notifyAll()

在Java中,可以通过配合调用Object对象的wait()方法和notify()方法或notifyAll()方法来实现线程间的通信。在线程中调用wait()方法,将阻塞等待其他线程的通知(其他线程调用notify()方法或notifyAll()方法),在线程中调用notify()方法或notifyAll()方法,将通知其他线程从wait()方法处返回。

      Object是所有类的超类,它有5个方法组成了等待/通知机制的核心:notify()、notifyAll()、wait()、wait(long)和wait(long,int)。在Java中,所有的类都从Object继承而来,因此,所有的类都拥有这些共有方法可供使用。而且,由于他们都被声明为final,因此在子类中不能覆写任何一个方法。

这里详细说明一下各个方法在使用中需要注意的几点:

1、wait()

      public final void wait()  throws InterruptedException,IllegalMonitorStateException

     该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch结构。

     2、notify()

     public final native void notify() throws IllegalMonitorStateException

        该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。

     该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。这与下面的notifyAll()方法执行后的情况不同。 

     3、notifyAll()

     public final native void notifyAll() throws IllegalMonitorStateException

      该方法与notify()方法的工作方式相同,重要的一点差异是:

      notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

     4、wait(long)和wait(long,int)

     显然,这两个方法是设置等待超时时间的,后者在超值时间上加上ns,精度也难以达到,因此,该方法很少使用。对于前者,如果在等待线程接到通知或被中断之前,已经超过了指定的毫秒数,则它通过竞争重新获得锁,并从wait(long)返回。另外,需要知道,如果设置了超时时间,当wait()返回时,我们不能确定它是因为接到了通知还是因为超时而返回的,因为wait()方法不会返回任何相关的信息。但一般可以通过设置标志位来判断,在notify之前改变标志位的值,在wait()方法后读取该标志位的值来判断,当然为了保证notify不被遗漏,我们还需要另外一个标志位来循环判断是否调用wait()方法。


都知道在Java中,我们可以通过继承Thread或者实现Runnable接口来实现多线程,这些线程会各自执行自己的任务,但是一个人的力量是有限的,一个线程的力量也是有限的,要想使系统各部分配合得更好,我们需要实现各个线程间的通信。要实现线程间的通信最好的方法就是使用wait/notify机制。

这里有两个新问题:

  1. 什么是线程间的通信?
  2. 如何使用wait/notify机制实现线程间的通信?

为了解决这两个问题,我们来看看下面这段代码:

public class ThreadA extends Thread{
    private MyList list;
    public ThreadA(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            list.add();
            System.out.println("第  "+i+" 此执行add操作");
        }
    }
    public static void main(String[] args) {
        MyList list = new MyList();
        ThreadA a = new ThreadA(list);
        ThreadB b = new ThreadB(list);
        a.start();
        b.start();
    }
}

class ThreadB extends Thread{
    private MyList list;
    public ThreadB(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {

        while( true ) {
            if (list.size() == 3) {
                System.out.println("线程Thread检测到list中的长度变为3了,停止该线程");
                Thread.interrupted();
            }
        }
    }
}
class MyList{
    private ArrayList<String> list = new ArrayList<String>();
    public void add() {
        list.add("abc");
    }
    public int size() {
        return list.size();
    }
}

执行结果如下:

第 0 此执行add操作 
第 1 此执行add操作 
第 2 此执行add操作 
线程Thread检测到list中的长度变为3了,停止该线程 
第 3 此执行add操作 
第 4 此执行add操作

在上面的例子中实现了两个线程,其中线程ThreadA的任务是向list中添加值,而线程ThreadB的任务是监听list中的数量,一旦list的长度达到了3,就通过interrupted方法停止该线程的监听操作。

在这里线程ThreadA与线程ThreadB就实现了通信,这个例子中的通信指的就是:线程ThreadA执行向list添加值的操作,而线程ThreadB通过不断的执行while监听来观察线程ThreadA的操作,从而做出自己相应的操作。可是这种通信方式有一个弊端,线程ThreadB需要不断的执行while循环来达到监听的目的,这样对CPU的资源会产生浪费。服务端的资源是非常宝贵的,所以就出现了wait/notify机制来解决这个问题。

我们看看使用wait/notify机制后,上面的线程间通信会变成什么样?

public class ThreadA extends Thread{
    private MyList list;
    public ThreadA(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        synchronized (list) {
            for (int i = 0; i < 5; i++) {
                list.add();
                System.out.println("第  "+i+" 此执行add操作");
                    try {
                        if (list.size() == 3) 
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            }
        }
    }
    public static void main(String[] args) {
        MyList list = new MyList();
        ThreadA a = new ThreadA(list);
        ThreadB b = new ThreadB(list);
        a.start();
        b.start();
    }
}
class ThreadB extends Thread{
    private MyList list;
    public ThreadB(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        synchronized (list) {
            System.out.println("线程Thread检测到list中的长度变为3了,停止该线程");    
            list.notify();  
            System.out.println("notify方法调用结束");
        }
    }
}
class MyList{
    private ArrayList<String> list = new ArrayList<String>();
    public void add() {
        list.add("abc");
    }
    public int size() {
        return list.size();
    }
}

执行结果如下:

第 0 此执行add操作 
第 1 此执行add操作 
第 2 此执行add操作 
线程Thread检测到list中的长度变为3了,停止该线程 
notify方法调用结束 
第 3 此执行add操作 
第 4 此执行add操作

上面是使用了wait/notify机制后的代码实现,两种方式的执行结果相同。但是代码的实现方式却大有不同,可以看到线程ThreadB里没有了while循环,说明在该线程里不用一直监听list中的值了,所以资源浪费的问题得到了解决,这其中的原理是什么呢?

首先需要注意的一点是,wait/notify方法必须存在于同步代码块里,因为这两个方法需要由锁对象去调用,在上面的例子中锁对象是list,调用wait与notify的方法都是同一个锁对象,具体用意在下面再讲。

然后我们来仔细说说代码的执行流程,首先我们先启动的是a线程,a线程拿到锁对象list,进入该线程run方法中的同步代码块,执行for循环,for循环中一直执行add添加操作,并进行if判断,当执行到第三次的时候进入if判断,执行list.wait(),当执行完这一句代码,当前线程立即释放list锁对象,由于此时,b线程也在等待同一把锁对象,所以该线程的拿到锁对象后执行对应的同步代码块内的内容。

当执行完list.notify()后,线程b会唤醒线程a,使其处于runnable状态,一旦抢到锁对象后立即接着list.wait()后的代码执行,与list.wait()不同的是,线程b在执行完list.notify()后没有立即释放锁,而是在执行完同步代码块中的所有内容后,锁才会被释放,锁被释放后,由于线程a已经被notify唤醒,所以会接着上次的地方执行。

在使用等待/唤醒机制的时候,有几个需要注意的地方。

  • 一个notify()方法只会唤醒一个处于等待中的线程,要想唤醒所有,可以使用notifyAll()方法
  • notify()方法只会唤醒被同一个锁对象wait()的线程。

对于第二点,如果我们将ThreadB修改成这样:

class ThreadB extends Thread{
    private MyList list;
    public ThreadB(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        synchronized (this) {
            System.out.println("线程Thread检测到list中的长度变为3了,停止该线程");
            this.notify();
            System.out.println("notify方法调用结束");
        }
    }
}

执行结果如下,并且程序一直没有停止,可见线程a没有被唤醒。

第 0 此执行add操作 
第 1 此执行add操作 
第 2 此执行add操作 
线程Thread检测到list中的长度变为3了,停止该线程 
notify方法调用结束

这是因为调用notify与wait方法不是使用的同一个锁对象,可以理解为这两个notify和wait不是一对,所以没达到唤醒的目的。

总结

经过对比发现,我发现wait/notify机制更像是一种主动推送的实现,一旦A线程达到了某种状态,可以通过wait方法来通知与A线程使用同一个锁对象的线程,让他们去做相应的操作,由于只有一把锁,所以只能有一个线程获得执行机会,这个获得执行机会的线程肯定是在A线程中达到某种状态后才会执行,所以也就间接实现了通信的目的。

原文来源:

https://blog.csdn.net/KKSJS/article/details/81087138

https://www.cnblogs.com/lt132024/p/6442880.html

https://www.cnblogs.com/fysola/p/6066290.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值