“怎么还慢吞吞的!”,JVM翘着二郎腿在调度室里大声喊道。“Thread-2,你已经被我创建出来了,赶紧干活!”。
Thread-2这才反应过来,看看自己的身体,是个结构分明、线条优美的线程栈。先看看自己有啥东西:一个程序计数器、一连串的函数栈帧。程序计数器记录程序执行到哪里了,函数栈帧是一个方法的信息体,里面包含局部变量表、操作数栈等。
“好的,老板。我看到我有一个run方法,里面调用了produce()方法,用人类的话说,我应该是个生产者啊”。JVM点了点头,这小子挺机灵的。“那你赶紧去生产产品吧,消费者线程打电话催了好几次了”。
消费者线程,那是个啥?
跟你一样,也是由我创建出来的一个线程。只是职责与你不同,你负责生产产品,它负责消费产品。
它在哪里,我怎么看不见。
你们彼此都是有独立空间的,但是你们需要协作完成工作,你得注意两点,①操作临界区数据时要进行互斥(同一时刻只能有一个线程操作这个共享变量),②修改后你得通知其他线程可以操作这个共享变量了;
你叫Producer类,你有一个放在方法区的实例变量taskQueue(List<Integer> taskQueue),你往这个容器中放东西,消费者线程的Consumer类也要持有同一个实例变量,这样它们就可以从这个容器里取产品了。JVM怕它不懂,多解释了几句。
看老大停顿了,天性活泼多问的Thread-2正准备开口接着问...
“先调用produce()函数栈帧,走一遍指令再说”。JVM没搭理它,发出了指令。
过了1ms,活泼的Thread-2又打电话来了。我已经走了一遍了,刚开始让我判断是否生产到5个了,如果到了就调用wait()方法。因为数量为0所以就继续走下面的逻辑,生产了一个产品放在taskQueue中,然后调用了notify()方法。调用这两个方法的时候,到底发生了什么啊?
你这大大咧咧的性格,怎么忘了描述最重要的那点了呢?
Thread-2摸摸自己红扑扑的脸蛋,生怕老板骂它。
你刚开始运行的时候,系统是不是让你去抢占taskQueue的monitor,你首先得抢到这个实例变量的monitor,你才能运行。才有后面调用wait()或notify()的事。
这些都是synchronized在起作用,它的作用是在代码块前后加上monitorenter和monitorexit两个字节码指令,这样就不会有其他线程同时操作taskQueue这个变量。这个就是我们常说的互斥锁。当然synchronized可以加在方法上或者包裹一个代码块,那说来就话长了。
我想起来了,我在taskQueue的对象头中确实看到了是我持有了锁。执行taskQueue.wait()或taskQueue.notify()方法的时候,是需要持有这个对象的monitor的。
是的,其中wait()、notify()这样的方法是要在synchronized关键字包裹的中才能执行的,否则会抛出IllegalMonitorStateException异常。
那这两个方法的作用是啥咧?
你先生产到5个产品。
JVM刚准备看会新闻,Thread-2来电了。我生产5个了,调用了wait()方法,释放了monitor。我现在身子就好像僵硬了一样,什么都干不了。JVM哈哈哈一笑,你这是把自己放在了一个等待池中,等待taskQueue实例变量的锁。让我跟你说说现在外面都发生了什么吧。
还记得你生产第一个产品的时候,就调用了notify()方法吧。这个方法会通知其他的线程来消费产品。但是他们没有进来消费,因为你还没有释放taskQueue的monitor,等到你生产满5个后,会调用wait()方法,这个方法会让你释放掉monitor,并进入等待池。
消费者线程那边也是同样的逻辑,他们会在收到通知后去竞争taskQueue的monitor,竞争到的线程开始进入consume()方法,它同样需要加互斥锁。当他们消费完了之后,会调用wait()方法,你就有机会去争夺taskQueue的monitor。抢到以后,你就可以继续工作了。
最终的结果你可以看下:
Thread-0先运行,发现集合为空,则进入等待池。生产者生产产品,达到5个后进入wait状态,释放taskQueue的monitor,消费者线程Thread-0获得taskQueue的monitor进入运行状态。以此类推消费线程Thread-1的行为。
JVM似乎很中意这个刚出生的娃娃。“看你天赋异禀,老哥赐你锦囊一幅,当你迷茫的时候,记得拿出来看看”。
锦囊第一法:搬完砖了怎么休息?
调用wait方法,它是Object类的方法,final native修饰,即本地方法,不可被子类重写!必须在sychronized块中调用;
作用:当前线程Thread T释放对象锁,并将自己放在等待池(wait set)中。直到其他线程获取这个对象的monitor控制权,以及发生以下四种情况之一时,线程Thread T将被唤醒:
①其他线程持有同一个对象的monitor并调用notify()方法
②其他线程持有同一个对象的monitor并调用notifyAll()方法
③超过等待时间
④被其他线程打断
调用wait方法的要点:
- 当前线程必须拥有这个对象的monitor,否则会抛出IllegalMonitorStateException异常;
- 当前线程将自己放在wait set中,并解除所有在这个对象上的同步声明;
- 当前线程调用wait方法后返回,线程及对象的同步状态与调用wait方法时是一致的;
- 当被唤醒时,会与其他线程一起竞争获取对象的同步权(获取monitor);
怎么用?
线程的唤醒缘故不一定是上面提到的四种情况,有时候可能会是假唤醒状态,所以需要在轮询+条件判断的代码块中使用,在不满足条件时,让线程一直等待:
synchronized (obj) {
while (condition does not hold)
obj.wait(timeout);
... // Perform action appropriate to condition
}
锦囊第二法:怎么通知其他线程小伙伴?
调用notify()或者notifyAll()方法。它们都是Object类的final native方法,必须在sychronized块中调用;
作用:notify是唤醒等待相同对象monitor的其中一个线程Thread T(Thread T须调用了wait方法),至于是哪个线程被唤醒,这要取决于具体实现,我们无法判断。notifyAll是唤醒所有等待相同对象monitor的线程们。其他跟notify一样。
Thread T不会立马进入运行状态:
①当前调用了notify()方法后,当前线程不一定会立马释放对象的monitor,直到当前线程的同步块执行完成后才会释放;
②Thread T得到可以唤醒的通知,对象的monitor也被释放了,此时会与其他等待当前对象monitor的对象一同竞争这个monitor,获取到这个对象的monitor后方可进入运行状态。
像这样用就可以了:
synchronized(lockObject){
//establish_the_condition;
lockObject.notify();
//any additional code if needed
}
Thread-2就这样慢慢熟悉了如何与其他线程进行协作,保证数据安全地运行,后来它了解到人类为了写出线程安全的代码,需要考虑到原子性、可见性、有序性等。看来又有新东西可以学了。