一、概述
二、wait/notify 机制
三、Condition
四、生产者/消费者模式
五、线程间的通信——管道
六、方法Join的使用
线程与线程之间不是相互独立的个体,它们彼此之间需要相互通信和协作,最典型的例子就是生产者-消费者问题:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。本文首先介绍 wait/notify 机制,并对实现该机制的两种方式——synchronized+wait-notify模式和Lock+Condition模式进行详细剖析,以作为线程间通信与协作的基础。进一步地,以经典的生产者-消费者问题为背景,熟练对 wait/notify 机制的使用。最后对 Thread 类中的 join() 方法进行源码分析,并以宿主线程与寄生线程的协作为例进行说明。在下面的例子中,虽然两个线程实现了通信,但是凭借线程B不断地通过while语句轮询来检测某一个条件,这样会导致CPU的浪费。因此,需要一种机制来减少CPU资源的浪费,而且还能实现多个线程之间的通信,即 wait/notify 机制。
在这之前,线程间通过共享数据来实现通信,即多个线程主动地读取一个共享数据,通过 同步互斥访问机制保证线程的安全性。等待/通知机制主要由Object类中的wait()、notify() 和 notifyAll()三个方法来实现,这三个方法均非Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有monitor(锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作,而不是用当前线程来操作,因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。
1.wait()——让当前线程 (Thread.concurrentThread() 方法所返回的线程) 释放对象锁并进入等待(阻塞)状态。
- 方法声明:public final native void wait(long timeout) throws InterruptedException;
- 方法作用:Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on this object’s monitor by calling one of the wait() methods.This method causes the current thread (call it T):① to place itself in the wait set for this object;② to relinquish (放弃) any and all synchronization claims on this object;③ Thread T becomes disabled for thread scheduling purposes and lies dormant (休眠) until one of four things happens:Some other thread invokes the notify method for this object and thread T happens to be arbitrarily chosen as the thread to be awakened;Some other thread invokes the notifyAll method for this object;Some other thread interrupts thread T;The specified amount of real time has elapsed, more or less. If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified (等待时间为 0 意味着永远等待,直至线程被唤醒) .The thread T is then removed from the wait set for this object and re-enabled for thread scheduling. It then competes in the usual manner with other threads for the right to synchronize on the object; once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T is exactly as it was when the wait method was invoked.
- 方法使用条件:This method should only be called by a thread that is the owner of this object’s monitor.
- 异常:运行时(不受检查)异常 IllegalMonitorStateException: if the current thread is not the owner of this object’s monitor; IllegalArgumentException: if the value of timeout is negative;受检查异常 (中断阻塞线程,抛 InterruptedException并终止线程,释放锁,释放CPU) InterruptedException: if any thread interrupted the current thread before or while the current thread was waiting for a notification. The interrupted status of the current thread is cleared when this exception is thrown.
2.notify()——唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
- 方法声明:public final native void notify();
- 方法作用:Wakes up a single thread that is waiting on this object’s monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation.The awakened thread will not be able to proceed until the current thread relinquishes (放弃) the lock on this object.(在执行 notify() 方法后,当前线程不会马上释放该锁对象,呈 wait 状态的线程也并不能马上获取该对象锁。只有等到执行notify()方法的线程退出synchronized代码块/方法后,当前线程才会释放锁,而相应的呈wait状态的线程才可以去争取该对象锁。) The awakened thread will compete in the usual manner with any other threads that might be actively competing to (竞争) synchronize on this object; the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.
- 方法使用条件This method should only be called by a thread that is the owner of this object’s monitor. A thread becomes the owner of the object’s monitor in one of three ways:By executing a synchronized instance method of that object;By executing the body of a synchronized statement that synchronizes on the object;For objects of type Class, by executing a synchronized static method of that class.Only one thread at a time can own an object’s monitor(互斥锁).
- 异常:运行时(不受检查)异常 IllegalMonitorStateException: if the current thread is not the owner of this object’s monitor.
3.notifyAll()——唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
- 方法声明:public final native void notifyAll();
- 方法作用:Wakes up all threads that are waiting on this object’s monitor. A thread waits on an object’s monitor by calling one of the wait methods.The awakened threads will not be able to proceed until the current thread relinquishes (放弃) the lock on this object. The awakened threads will compete in the usual manner with any other threads that might be actively competing to (竞争) synchronize on this object; the awakened threads enjoy no reliable privilege or disadvantage in being the next thread to lock this object.
- 方法使用条件:This method should only be called by a thread that is the owner of this object’s monitor.
- 异常:运行时(不受检查)异常 IllegalMonitorStateException: if the current thread is not the owner of this object’s monitor.
4.小结
从以上描述可以得出:wait()、notify() 和 notifyAll()方法是 本地方法,并且为 final 方法,无法被重写;调用某个对象的 wait() 方法能让 当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁);调用某个对象的 notify() 方法能够唤醒 一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程。
- 方法调用与线程状态关系
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度;反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。
- 使用举例
- 多个同类型线程的场景(wait 的条件发生变化)
当线程subtract1Thread被唤醒后,将从wait处继续执行。但由于线程subtract2Thread先获取到锁得到运行,导致线程subtr-act1Thread的wait的条件发生变化(不再满足),而 线程subtract1Thread 却毫无所知,导致异常产生。像这种有多个相同类型的线程场景,为防止wait的条件发生变化而导致的线程异常终止,我们在阻塞线程被唤醒的同时还必须对wait的条件进行额外的检查,如下所示:只需将 线程类ThreadSubtract 的 run()方法中的 if 条件 改为 while 条件 即可。
Condition是在java 1.5中出现的,它用来替代传统的Object的wait()/notify()实现线程间的协作,它的使用依赖于 Lock,Condition、Lock 和 Thread 三者之间的关系如下图所示。相比使用Object的wait()/notify(),使用Condition的await()/signal()这种方式能够更加安全和高效地实现线程间协作。Condition是个接口,基本的方法就是await()和signal()方法。Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,Conditon的await()/signal() 与 Object的wait()-notify() 有着天然的对应关系:Conditon中的await()对应Object的wait();Condition中的signal()对应Object的notify();Condition中的signalAll()对应Object的notifyAll()。
使用Condition往往比使用传统的通知等待机制(Object的wait()/notify())要更灵活、高效,例如,我们可以使用多个Condition实现通知部分线程:
输出结果如下图所示,我们可以看到只有线程A被唤醒,线程B仍然阻塞。实际上,Condition 实现了一种分组机制,将所有对临界资源进行访问的线程进行分组,以便实现线程间更精细化的协作,例如通知部分线程。我们可以从上面例子的输出结果看出,只有conditionA范围内的线程A被唤醒,而conditionB范围内的线程B仍然阻塞。
等待/通知机制 最经典的应用就是 生产者-消费者模型。下面以多生产者-多消费者问题为背景,分别运用两种模式 —— synchronized+wait-notify模式和Lock+Condition模式实现 wait-notify 机制。示例一传统实现:
对于生产者-消费者问题,有两个要点需要注意:第一,在多个同类型线程(多个生产者线程或者消费者线程)的场景中,为防止wait的条件发生变化而导致线程异常终止,我们在阻塞线程被唤醒的同时还必须对wait的条件进行额外的检查,即 使用 while 循环代替 if 条件;第二,在多个同类型线程(多个生产者线程或者消费者线程)的场景中,为防止生产者(消费者)唤醒生产者(消费者),保证生产者和消费者互相唤醒,需要 使用 notify 替代 notifyAll。示例二,使用condition方式实现:
1. join() 的定义
假如在main线程中调用thread.join方法,则main线程会等待thread线程执行完毕或者等待一定的时间。详细地,如果调用的是无参join方法,则等待thread执行完毕;如果调用的是指定了时间参数的join方法,则等待一定的时间。join()方法有三个重载版本:
以 join(long millis) 方法为例,其内部调用了Object的wait()方法,如下图:
根据以上源代码可以看出,join()方法是通过wait()方法 (Object 提供的方法) 实现的。当 millis == 0 时,会进入 while(isAlive()) 循环,并且只要子线程是活的,宿主线程就不停的等待。 wait(0) 的作用是让当前线程(宿主线程)等待,而这里的当前线程是指 Thread.currentThread() 所返回的线程。所以,虽然是子线程对象(锁)调用wait()方法,但是阻塞的是宿主线程。
2.join() 使用实例及原理
看上面的例子,当 main线程 运行到 thread1.join() 时,main线程会获得线程对象thread1的锁(wait 意味着拿到该对象的锁)。只要 thread1线程 存活, 就会调用该对象锁的wait()方法阻塞 main线程。那么,main线程被什么时候唤醒呢?事实上,有wait就必然有notify。在整个jdk里面,我们都不会找到对thread1线程的notify操作。这就要看jvm代码了:
至此,thread1线程对象锁调用了notifyall,那么main线程也就能继续跑下去了。由于 join方法 会调用 wait方法 让宿主线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。结合 join 方法的声明,可以总结出以下三条:join方法同样会会让线程交出CPU执行权限;join方法同样会让线程释放对一个对象持有的锁;如果调用了join方法,必须捕获InterruptedException异常或者将该异常向上层抛出。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/12926.html