它们都是线程之间协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。
final static Object obj = new Object();
public static void main(String[] args){
new Thread(()->{
synchronized(obj){
log.debug("执行....");
try{
obj.wait(); //让线程在obj上一直等待下去
}catch(InterruptedException e){
e.printStackTrace();
}
log.debug("其他代码....");
}
},"t1").start();
new Thread(()->{
synchronized(obj){
log.debug("执行....");
try{
obj.wait(); //让线程在obj上一直等待下去
}catch(InterruptedException e){
e.printStackTrace();
}
log.debug("其他代码....");
}
},"t2").start();
sleep(2);
log.debug("唤醒obj上其他线程");
synchronized(obj){
obj.notify(); //唤醒obj上一个线程
//obj.notifyAll(); //唤醒obj上所有等待线程
}
}
notify的一种结果
20:00:53.096 [Thread-0] c.TestWaitNotify - 执行…
20:00:53.099 [Thread-1] c.TestWaitNotify - 执行…
20:00:55.096 [main] c.TestWaitNotify - 唤醒obj上其他线程
20:00:55.096 [Thread-0] c.TestWaitNotify - 其他代码…
notifyAll的结果
19:58:15.457 [Thread-0] c.TestWaitNotify - 执行…
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行…
19:58:17.456 [main] c.TestWaitNotify - 唤醒obj上其他线程
19:58:17.456 [Thread-1] c.TestWaitNotify - 其他代码…
19:58:17.456 [Thread-0] c.TestWaitNotify - 其他代码…
wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程有机会获取对象的锁。无限制等待,直到notify为止;wait(long n)有时限的等待,到n毫秒后结束等待,或是被notify。
sleep(long n)和wait(long n)的区别
1)sleep是Thread方法,而wait是Object方法
2)sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
3)sleep在睡眠的同时,不会释放对象锁,但wait在等待的时候会释放对象锁
下述代码中有一个"小南"线程,五个"其他人"线程,一个"送烟的"线程。在没有烟时,小南使用sleep,不会释放room,也就是这个时候,其他人不会开始工作。
static final Obejct room = new Object();
static boolean hasCigarette = false; //有没有烟
static boolean hasTakeout = false; //有没有外卖
public static void main(String[] args){
new Thread(()->{
synchronized(room){
log.debug("有烟没?[{}]",hasCigarette);
if(!hasCigarette){
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]",hasCigarette);
if(hasCigarette){
log.debug("可以开始干活了");
}
}
},"小南").start();
for(int i = 0;i < 5;i++){
new Thread(()->{
synchronized(room){
log.debug("可以开始干活了");
}
},"其他人").start();
}
sleep(1);
new Thread(()->{
hasCigarette = true;
log.debug("烟到了哦!");
},"送烟的").start();
}
输出
下述代码在小南没有烟时,使用wait,此时会释放掉room的锁,其他人可以开始干活,当烟到的时候,调用room.notify(),唤醒线程(注意:此时因为“休息室”只有小南一人,所以notify唤醒的一定是小南)。
static final Obejct room = new Object();
static boolean hasCigarette = false; //有没有烟
static boolean hasTakeout = false; //有没有外卖
public static void main(String[] args){
new Thread(()->{
synchronized(room){
log.debug("有烟没?[{}]",hasCigarette );
if(!hasCigarette){
log.debug("没烟,先歇会!");
try{
room.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("有烟没?[{}]",hasCigarette);
if(hasCigarette){
log.debug("可以开始干活了");
}
}
},"小南").start();
for(int i = 0;i < 5;i++){
new Thread(()->{
synchronized(room){
log.debug("可以开始干活了");
}
},"其他人").start();
}
sleep(1);
new Thread(()->{
synchronized(room){
hasCigarette = true;
log.debug("烟到了哦!");
room.notify();
}
},"送烟的").start();
}
输出
解决了其他干活的线程阻塞的问题,但如果有其他线程也在等待条件呢(即“休息室”不止小南一人呢)?
下述代码有一个“小南”线程,一个“小女”线程,一个“送外卖的”线程。当没烟没外卖时,小南小女都会在“休息室”等待;当外卖送到时,触发room.notify(),按照jvm规范随机唤醒。
static final Obejct room = new Object();
static boolean hasCigarette = false; //有没有烟
static boolean hasTakeout = false; //有没有外卖
public static void main(String[] args){
new Thread(()->{
synchronized(room){
log.debug("有烟没?[{}]",hasCigarette );
if(!hasCigarette){
log.debug("没烟,先歇会!");
try{
room.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("有烟没?[{}]",hasCigarette);
if(hasCigarette){
log.debug("可以开始干活了");
}else{
log.debug("没干成活...");
}
}
},"小南").start();
new Thread(()->{
synchronized(room){
log.debug("外卖送到没?[{}]",hasTakeout);
if(!hasTakeout ){
log.debug("没外卖,先歇会!");
try{
room.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]",hasTakeout);
if(hasTakeout ){
log.debug("可以开始干活了");
}else{
log.debug("没干成活...");
}
}
},"小女").start();
sleep(1);
new Thread(()->{
synchronized(room){
hasTakeout = true;
log.debug("外卖到了哦!");
room.notify();
}
},"送外卖的").start();
}
输出
下述代码有一个“小南”线程,一个“小女”线程,一个“送外卖的”线程。当没烟没外卖时,小南小女都会在“休息室”等待;当外卖送到时,触发room.notifyAll(),唤醒“休息室”全部线程(这里即唤醒小南小女)。
static final Obejct room = new Object();
static boolean hasCigarette = false; //有没有烟
static boolean hasTakeout = false; //有没有外卖
public static void main(String[] args){
new Thread(()->{
synchronized(room){
log.debug("有烟没?[{}]",hasCigarette );
if(!hasCigarette){
log.debug("没烟,先歇会!");
try{
room.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("有烟没?[{}]",hasCigarette);
if(hasCigarette){
log.debug("可以开始干活了");
}else{
log.debug("没干成活...");
}
}
},"小南").start();
new Thread(()->{
synchronized(room){
log.debug("外卖送到没?[{}]",hasTakeout);
if(!hasTakeout ){
log.debug("没外卖,先歇会!");
try{
room.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]",hasTakeout);
if(hasTakeout ){
log.debug("可以开始干活了");
}else{
log.debug("没干成活...");
}
}
},"小女").start();
sleep(1);
new Thread(()->{
synchronized(room){
hasTakeout = true;
log.debug("外卖到了哦!");
room.notifyAll();
}
},"送外卖的").start();
}
输出
观察输出,可以发现"小南"线程被虚假唤醒了,这时把if改为while
static final Obejct room = new Object();
static boolean hasCigarette = false; //有没有烟
static boolean hasTakeout = false; //有没有外卖
public static void main(String[] args){
new Thread(()->{
synchronized(room){
log.debug("有烟没?[{}]",hasCigarette );
while(!hasCigarette){
log.debug("没烟,先歇会!");
try{
room.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("有烟没?[{}]",hasCigarette);
if(hasCigarette){
log.debug("可以开始干活了");
}else{
log.debug("没干成活...");
}
}
},"小南").start();
new Thread(()->{
synchronized(room){
log.debug("外卖送到没?[{}]",hasTakeout);
if(!hasTakeout ){
log.debug("没外卖,先歇会!");
try{
room.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]",hasTakeout);
if(hasTakeout ){
log.debug("可以开始干活了");
}else{
log.debug("没干成活...");
}
}
},"小女").start();
sleep(1);
new Thread(()->{
synchronized(room){
hasTakeout = true;
log.debug("外卖到了哦!");
room.notifyAll();
}
},"送外卖的").start();
}
输出
用notifyAll仅解决某个线程的唤醒问题,但使用if+wait判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了。
解决方法,用while + wait,当条件不成立,再次wait。
格式
synchronized(lock){
while(条件不成立){
lock.wait();
}
//干活
}
//另一个线程
synchronized(lock){
lock.notifyAll();
}
相对于synchronized它具备如下特点
与synchronized一样,都支持可重入
基本语法
//获取锁
reentrantLock.lock();
try{
//临界区
}finally{
//释放锁
reentrantLock.unlock();
}
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
lock.lock();
try{
log.debug("enter main");
m1();
}finally{
lock.unlock();
}
}
public static void m1(){
lock.lock();
try{
log.debug("enter m1");
m2();
}finally{
lock.unlock();
}
}
[main] - enter main
[main] - enter m1
[main] - enter m2
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(()->{
try{
//如果没有竞争那么此方法就会获取lock的对象锁
//如果有竞争就进入阻塞队列,可以被其它线程用interruput方法打断
log.debug("尝试获取锁");
lock.lockInterruptibly();
}catch(InterruptedException e){
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try{
log.debug("获取到锁");
}finally{
lock.unlock();
}
},"t1");
t1.start();
}
[ t1 ] - 尝试获取锁
[ t1 ] - 获取到锁
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(()->{
try{
//如果没有竞争那么此方法就会获取lock的对象锁
//如果有竞争就进入阻塞队列,可以被其它线程用interruput方法打断
log.debug("尝试获取锁");
lock.lockInterruptibly();
}catch(InterruptedException e){
e.printStackTrace();
log.debug("没有获得锁,返回");
return;
}
try{
log.debug("获取到锁");
}finally{
lock.unlock();
}
},"t1");
lock.lock();
t1.start();
sleep(1);
log.debug("打断 t1");
t1.interrupt();
}
输出
1> tryLock()
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.debug("尝试获得锁");
if(!lock.tryLock()){
log.debug("获取不到锁");
return;
}
try{
log.debug("获得到锁");
}finally{
lock.unlock();
}
},"t1");
t1.start();
}
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.debug("尝试获得锁");
if(!lock.tryLock()){
log.debug("获取不到锁");
return;
}
try{
log.debug("获得到锁");
}finally{
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获得到锁");
t1.start();
}
2>tryLock(long timeout, TimeUnit unit)
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.debug("尝试获得锁");
try{
if(!lock.tryLock(2,TimeUnit.SECONDS)){
log.debug("获取不到锁");
return;
}
}catch(InterruptedException e){
e.printStackTrace();
log.debug("获取不到锁");
return;
}
try{
log.debug("获得到锁");
}finally{
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获得到锁");
t1.start();
sleep(1);
log.debug("释放了锁");
lock.unlock();
}
synchronized中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待
ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比
使用流程
static final Obejct room = new Object();
static boolean hasCigarette = false; //有没有烟
static boolean hasTakeout = false; //有没有外卖
static ReentrantLock ROOM = new ReentrantLock();
//等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
//等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args){
new Thread(()->{
ROOM.lock();
try{
log.debug("有烟没?[{}]",hasCigarette );
while(!hasCigarette){
log.debug("没烟,先歇会!");
try{
waitCigaretteSet.await();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally{
ROOM.unlock();
}
},"小南").start();
new Thread(()->{
ROOM.lock();
try{
log.debug("外卖送到没?[{}]",hasTakeout);
if(!hasTakeout ){
log.debug("没外卖,先歇会!");
try{
waitTakeoutSet.await();
}catch(InterruptedException e){
e.printStackTrace();
}
}
log.debug("可以开始干活了");
}finally{
ROOM.unlock();
}
},"小女").start();
sleep(1);
new Thread(()->{
ROOM.lock();
try{
hasTakeout = true;
waitTakeoutSet.signal();
}finally{
ROOM.unlock();
}
},"送外卖的").start();
sleep(1);
new Thread(()->{
ROOM.lock();
try{
hasCigarette = true;
waitCigaretteSet.signal();
}finally{
ROOM.unlock();
}
},"送烟的").start();
}
JMM即Java MemoryModel,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
退不出的循环
先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止
static boolean run = true;
public static void main(String[] args)throws InterruptedException{
Thread t = new Thread(()->{
while(run){
//....
}
});
t.start();
sleep(1);
run = false; //线程t不会如预想的停下来
}
为什么呢?分析一下
1.初始状态,t线程刚开始从主内存读取了run的值到工作内存
2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率
3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
volatile static boolean run = true;
public static void main(String[] args)throws InterruptedException{
Thread t = new Thread(()->{
while(run){
//....
}
});
t.start();
sleep(1);
run = false; //线程t不会如预想的停下来
}
static boolean run = true;
//锁对象
final static Object lock = new Object();
public static void main(String[] args)throws InterruptedException{
Thread t = new Thread(()->{
while(true){
//....
synchronized(lock){
if(!run){
break;
}
}
}
});
t.start();
sleep(1);
log.debug("停止t");
synchronized(lock){
run = false;
}
}
synchronized和volatile都可以解决可见性问题,但是synchronized需要创建monitor,属于重量级操作,volatile相对更轻量,建议使用volatile。
可见性vs原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况:
上例从字节码理解是这样的:
比较一下之前我们讲线程安全时举的例子:两个线程一个i++一个i–,只能保证看到最新值,不能解决指令交错
注意
synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。
如果在前面示例的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,想一想为什么?
JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码
static int i;
static int j;
//在某个线程内执行如下赋值操作
i = ...;
j = ...;
可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
i = …;
j = …;
也可是
j = …;
i = …;
这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性。为什么要有重排指令这项优化呢?从CPU执行指令的原理来理解一下把
诡异的结果
int num = 0;
boolean ready = false;
//线程1执行此方法
public void actor1(I_Result r){
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
//线程2执行此方法
public void actor2(I_Result r){
num = 2;
ready = true;
}
I_Result 是一个对象,有一个属性r1用来保存结果,问,可能的结果有几种
有同学这么分析
但我告诉你,结果还有可能是0,信不信吧!
这种情况下是:线程2执行ready = true,切换到线程1,进入if分支,相加为0,再切回线程2执行num = 2
相信很多人已经晕了。
指令重排序的验证,可以借助java并发压测工具jcstress
那如何禁用指令重排序呢?加volatile
int num = 0;
volatile boolean ready = false;
//线程1执行此方法
public void actor1(I_Result r){
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
//线程2执行此方法
public void actor2(I_Result r){
num = 2;
ready = true;
}
为什么加在了ready上而不是num上,现在先简单提一下,此处的volatile相当于在ready指令下加了一个屏障,禁止ready上面的语句掉到屏障下面,即保证了先执行num = 2,再执行ready = true。
volatile的底层实现原理时内存屏障,Memory Barrier(Memory Fence)
public void actor2(I_Result r){
num = 2;
ready = true; //ready是volatile赋值带写屏障
//写屏障
}
public void actor1(I_Result r){
//读屏障
//ready是volatile读取值读屏障
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
public void actor2(I_Result r){
num = 2;
ready = true; //ready是volatile赋值带写屏障
}
public void actor1(I_Result r){
//ready是volatile读取值读屏障
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}
还是那句话,不能解决指令交错:
synchronized可以保证可见、共享、有序性,前提:共享变量都交给synchronized来管理