1 线程间通信
线程间是需要通信的,系统中的各个线程进行都有自己的任务,有时候线程间的任务内容可能会有冲突。比如只有一个苹果,但是没有线程的任务都要求咬一口,那么,谁先下嘴呢?
线程的通信是有必要的,因为一个大苹果一个人吃,吃一会,歇一会儿,一下子可能吃不完,如果让多个线程多个人来吃,一个人一口,很快就吃完啦。
当然线程间的通信有可能是互斥关系、也有可能是协作关系。比如,如果要吃同一个苹果,那么便存在互斥关系(不能同时咬一个苹果),但是,这同样也是一种协作关系(一起吃完了一个苹果)。
1.1 volatile和synchronized关键字
对于某块内存上的变量(包括对象或者对象的成员变量),每个线程中其实都有自己的拷贝,这样可以加快程序的执行。但是,如果该内存的变量发生了变化,那么散落在各个地方的拷贝就不一定是最新的了。此时就会存在信息的错误。
volatile就是为了避免这个情况,将这个变量声明为volatile,意思就是只要用到这个变量,就一定要去共享内存中重新读取,来保证信息的一致性。并且,所有对这个变量的修改,都要马上写回共享内存中去。这样,就很慢呀…如果每个变量声明volatile,程序执行效率就很慢了。
而synchronized用于确保,一个时候只能有一个线程来访问。
每个对象都有一个监视它的Monitor对象,想要拿到这个对象的唯一访问权,也就是要先拿到它的Monitor对象,也就是要获取到锁。
如下图所示:
很多线程可能会同时想要去获取锁(monitorenter),但是只能有一个获取到,获取到的继续走,获取不到的,就进入到同步队列中去阻塞(等待)。等上一个线程释放了锁(monitorexit),再继续争抢。
2 等待/通知机制
2.1 概述
等待、通知两个是配对的。就像亲兄弟一样。其中,等待对应的方法是wait(),通知对应的方法是notify()和notifyAll()。
三个方法都是object中的方法,也就是每个对象都可以wait/notify。
前提:加了锁。
对象.wait()——表示,调用对象.wait()的线程有运行态(running)进入睡眠状态(WAITING)。线程获得了锁,但是还没有满足执行的条件,所以它应该用wait释放锁,然后线程进入到这个对象的等待队列中去。
对象.notify()——表示,所有WAITING在这个对象的线程中唤醒一个线程,当然,并不是马上唤醒,要等待锁被释放了才会唤醒。但是会将其从等待队列移动到同步队列。线程状态由WAITING变成BLOCKED。
对象.notifyAll()——所有等待在这个对象上的线程都被唤醒,方然,也不知马上醒来,同样要等待锁被释放了以后才行。同样会将所有等待队列的线程移动到同步队列。
如下图所示:
2.2 基本规则
等待方:
Synchronized(对象){
while(条件不满足){//唤醒后还是要继续判断这一步
对象.wait();
}
doSomeThing();
}
通知方:
Synchronized(对象){
doSomeThing();
改变条件;//这样等待方可能才会获得锁
对象.notifyAll();
}
为什么一般wait()都要放在while循环里面呢?试想一下,如果通知方调用的是notifyAll(),所有线程都被唤醒,如果不继续做while中condiction的判断,如果a线程想要执行doSomeThing的时候,实际上b线程已经获得了锁,那么冲按道理应该是b执行doSomeThing而不是a来执行,这时候与预想中是不一样的,所以需要用while来再次判断条件。
参考于 why-must-wait-always-be-in-synchronized-block
2.2 wait()和notify()/notifyAll()的使用场景
wait()和notify()/notifyAll()都需要在同步的场景下才可以使用的,否则会报错。如:
public static void main(String[] args) {
String newString = new String("323");
try {
newString.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这里个产生一个java.lang.IllegalMonitorStateException的错误。只有加上了synchronized才行。为什么一定要在Sychronized()中使用呢?原因是因为:
消费者/等待者
if(condiction){
doSomething();//line 1
object.wait();//line 2
}
生产者/通知者
if(condiction){
object.notify();//line 3
}
假如线程A wait()在一个对象object上了,如果没有notify,那么他就会一直wait无法醒过来,这不仅耗资源,而且极有可能会产生死锁。
试想,如果notify和wait均不加Sychronized,即不需要获得锁,当线程A 还没wait的时候,B线程已经notify了(如上述代码中,当等待者执行到line 1的时候,统治者已经执行了line 3),那么此时A就错过了一次notify,若之后B再也不notify,那么A就死定了。
由此可见,若notify、wait不加Sychronized,危害极大。
2.4 监视器与锁的区别
锁,是每个对象都有的,笔记1中有记载,对象头中有2bit的长度用于记录锁的信息。
而监视器是一种同步的数据结构,同样是每个对象都具有这样的同步结构,包含了等待队列、同步队列。通过监视器这种结构,来确保线程的互斥或协作关系。
3 Join()
假如在线程A中执行了线程B.join(),意思就是线程A要等待线程B执行完了以后才从join()中返回,继续往下执行。
初此之外,还有join(long mills)——超时返回。
private static void testJoin(){
Thread b = new Thread(){
@Override
public void run() {
try {
Thread.sleep(2000);//耗时操作
System.out.println("B线程结束啦");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
b.start();
try {
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束啦");
}
打印的结果将会是
B线程结束啦
主线程结束啦
目前能够想到的场景就是:异步计算。主线程开启了一子线程去执行耗时操作,然后主线程就先做接下来的操作,主线程最后的时候,再去b.join()等待子线程的结果。