线程间的通信
这里我们有一个需求,如下:
-
分别开一个线程代表两个工作者,一个负责挖坑,一个负责埋土,要求挖一个埋一个,实现挖坑、埋土、挖坑、埋土.....循环下去
先看一下下边得实现
public class Test {
String s = "";
public synchronized void dig() {
s = "挖坑";
}
public synchronized void bury() {
s = "埋土";
}
public static void main(String[] args) {
Test test = new Test();
Thread thread1 = new Thread(new Runnable() {
public void run() {
while (true) {
test.dig();
System.out.println(Thread.currentThread().getName() + ":" +test.s);
}
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
while (true) {
test.bury();
System.out.println(Thread.currentThread().getName() + ":" +test.s);
}
}
});
thread1.setName("digWorker");
thread2.setName("buryWorker");
thread1.start();
thread2.start();
}
}
以上代码虽然利用了synchronized 关键字实现同步,竞争test得对象锁,但是因为两个线程得竞争时机我们没有控制,所以会导致一个线程出现多次竞争到锁得情况,出现挖坑、挖坑、挖坑、挖坑、埋坑、埋坑、挖坑…这种与需求不符得情况出现。
为了解决这种情况,实现上述需求,我们需要两个线程之间相互进行配合,这就需要我们进行线程中得通信。
针对以上诉求,我们有以下实现
- 定义一个共享资源类。
/**
* 定义一个资源类
*
* @author Mr.Li
*
*/
public class Resources {
/**
* 动作类型
*/
public ActionType action;
/**
* 定义一个标记 用于等待唤醒的切换 为了防止错过唤醒信号,而导致线程永久等待,初始为null
*/
public Boolean flag = null;
public enum ActionType {
Dig("挖坑"), Bury("埋土");
private ActionType(String action) {
this.action = action;
}
private String action = "";
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
}
}
- 定义挖掘工
/**
* 挖掘工
*
* @author Mr.Li
*/
public class DigWorker implements Runnable {
public Resources r; // 创建一个资源类型的变量
public DigWorker(Resources r) { // 有参构造函数
this.r = r;
}
@Override
public void run() {
while (true) {
synchronized (r) {
if (r.flag != null && r.flag) { // 如果r.flag=ture就等待
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (r.action == null || Resources.ActionType.Bury.equals(r.action)) {
r.action = Resources.ActionType.Dig;
} else {
break;
}
System.out.println(r.action.getAction());
r.flag = true;
r.notify();
}
}
}
}
- 定义埋土工
/**
* 埋土工
* @author Mr.Li
*/
public class BuryWorker implements Runnable {
public Resources r; // 创建一个资源类型的变量
public BuryWorker(Resources r) { // 有参构造函数
this.r = r;
}
@Override
public void run() {
while (true) {
synchronized (r) {
if (r.flag != null && r.flag == false) {
try {
r.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (r.action == null || Resources.ActionType.Dig.equals(r.action)) {
r.action = Resources.ActionType.Bury;
}else {
break;
}
System.out.println(r.action.getAction());
r.flag = false;
r.notify();
}
}
}
}
- 定义执行类
public class ThreadTest{
/**
*这里得资源对象,两个线程要操作用一个资源,否则相互间就收不到相互得信号
*/
static Resources resources = new Resources();
/**
* 需求:分别开一个线程代表两个工作者,一个负责挖坑,一个负责埋土,要求挖一个埋一个,实现挖坑、埋土、挖坑、埋土.....循环下去
* @param args
*/
public static void main(String[] args) {
Thread thread1 = new Thread(new DigWorker(resources));
Thread thread2 = new Thread(new BuryWorker(resources));
thread1.start();
thread2.start();
}
}
以上代码,我们先定义了一个共享资源,作为线程通信得渠道,用来进行线程之间得通信。这里有两个成员变量,一个是动作,一个是等待唤醒得标识,该标识控制两个线程之间得等待和唤醒得时机,初始值为null 是因为如果初始值为false,则刚开始等待埋土工就会进行等待,而假如说埋土。
挖掘工,通过有参构造函数将共享资源变量引入该类,做自旋,持有资源对象锁,如果资源标识为true则说明已经挖好坑,挖坑线程需要等待埋坑工操作后通知唤醒。标识为false时,进行挖坑,挖好后改变挖坑状态,并通知唤醒埋土工进行埋坑。
埋土工,同挖掘工,只不过标识相反。
执行类,定义共享资源,要保证两个线程操作的是同一个变量,否则两个线程相互收不到彼此得信号。
以上是一个简单得例子,在大家业务实现中,可以更好的去定义资源或者监视对象等,在此思想上进行扩展。
wait(),notify()和notifyAll()
Java有一个内建的等待机制来允许线程在等待信号的时候变为非运行状态。java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。
一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()
以下是例子使用了wait()和notify()的MyWaitNotify:
public class MonitorObject{
}
public class MyWaitNotify{
MonitorObject myMonitorObject = new MonitorObject();
public void doWait(){
synchronized(myMonitorObject){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
}
public void doNotify(){
synchronized(myMonitorObject){
myMonitorObject.notify();
}
}
}
等待线程将调用doWait(),而唤醒线程将调用doNotify()。当一个线程调用一个对象的notify()方法,正在等待该对象的所有线程中将有一个线程被唤醒并允许执行(校注:这个将被唤醒的线程是随机的,不可以指定唤醒哪个线程)。同时也提供了一个notifyAll()方法来唤醒正在等待一个给定对象的所有线程。
如你所见,不管是等待线程还是唤醒线程都在同步块里调用wait()和notify()。这是强制性的!一个线程如果没有持有对象锁,将不能调用wait(),notify()或者notifyAll()。否则,会抛出IllegalMonitorStateException异常。
但是,这怎么可能?等待线程在同步块里面执行的时候,不是一直持有监视器对象(myMonitor对象)的锁吗?等待线程不能阻塞唤醒线程进入doNotify()的同步块吗?答案是:的确不能。一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他线程也可以调用wait()或者notify()。
一旦一个线程被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块。换句话说:被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面。如果多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁。
wait()实现原理
在进行wait()之前,就代表着需要争夺Synchorized,而Synchronized代码块通过javap生成的字节码中包含monitorenter和monitorexit两个指令。在进如锁的时候会执行monitorenter,执行monitorenter指令可以获取对象的monitor。同时在执行Lock.wait()的时候也必须持有monitor对象。
在多核环境下,多个线程有可能同时执行monitorenter指令,并获取lock对象关联的monitor,但只有一个线程可以和monitor建立关联,这个线程执行到wait方法时,wait方法会将当前线程放入wait set,使其进行等待直到被唤醒,并放弃lock对象上的所有同步声明,意味着该线程释放了锁,其他线程可以重新执行加锁操作,notify方法会选择wait set中任意一个线程进行唤醒,notifyAll方法会唤醒monitor的wait set中所有线程。执行完notify方法并不会立马唤醒等待线程。那么wait具体是怎么实现的呢?
首先在HotSpot虚拟机中,monitor采用ObjectMonitor实现,每个线程都具有两个队列,分别为free和used,用来存放ObjectMonitor。如果当前free列表为空,线程将向全局global list请求分配ObjectMonitor。
ObjectMonitor对象中有两个队列,都用来保存ObjectWaiter对象,分别是_WaitSet 和 _EntrySet。_owner用来指向获得ObjectMonitor对象的线程
ObjectWaiter对象是双向链表结构,保存了_thread(当前线程)以及当前的状态TState等数据, 每个等待锁的线程都会被封装成ObjectWaiter对象。
_WaitSet :处于wait状态的线程,会被加入到wait set;
_EntrySett:处于等待锁block状态的线程,会被加入到entry set;
wait方法实现:
lock.wait()方法最终通过ObjectMonitor的void wait(jlong millis, bool interruptable, TRAPS);实现
1、将当前线程封装成ObjectWaiter对象node
2、通过ObjectMonitor::AddWaiter方法将node添加到_WaitSet列表中
3、通过ObjectMonitor::exit方法释放当前的ObjectMonitor对象,这样其它竞争线程就可以获取该ObjectMonitor对象
4、最终底层的park方法会挂起线程
ObjectSynchorizer::wait方法通过Object对象找到ObjectMonitor对象调用方法void ObjectMonitor::wait(),通过ObjectMonitor::AddWaiter调用把新建里的ObjectWaiter对象,放入到_WaitSet队列的末尾然后,在ObjectMonitor::exit释放锁,接着thread_ParkEvent->park,也就是进行wait。