为什么 java wait/notify 必须与 synchronized 一起使用
这个问题就是书本上没怎么讲解,就是告诉我们这样处理,但没有解释为什么这么处理?我也是基于这样的困惑去了解原因。
synchronized是什么
Java中提供了两种实现同步的基础语义:synchronized方法和synchronized块, 看个demo:
public class SyncTest {\\ 1、synchronized方法public synchronized void syncMethod(){System.out.println("hello method");}\\ 2、synchronized块public void syncBlock(){synchronized (this){System.out.println("hello block");}}
}
具体还要区分:
-
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
不同实例对象的访问,是不会形成锁的。 -
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
它具有的特性:
- 1、原子性
- 2、可见性
- 3、有序性
- 4、可重入性
synchronized如何实现锁
这样看来synchronized实现的锁是基于class对象来实现的,我们来看看如何实现的,它其实是跟class对象的对象头一起起作用的,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。其中对象头中有一个Mark Word,这里主要存储对象的hashCode、锁信息或分代年龄或GC标志等信息,把可能的情况列出来大概如下:
其中synchronized就与锁标志位一起作用实现锁。主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {_header = NULL;_count = 0; //记录个数_waiters = 0,_recursions = 0;_object = NULL;_owner = NULL;_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;}
上面有2个字段很重要:
- _WaitSet队列
处于wait状态的线程,会被加入到_WaitSet。 - _EntryList队列
处于等待锁block状态的线程,会被加入到该列表。 - _owner
_owner指向持有ObjectMonitor对象的线程
我们来模拟一下进入锁的流程:
- 1、当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合
- 2、当线程获取到对象的monitor 后进入 _Owner 区域,并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1
- 3、若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。
- 4、若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
wait/notify
这两个是Java对象都有的属性,表示这个对象的一个等待和通知机制,
不用synchronized 会怎么样
参考其他博客,我们来看看不使用synchronized会怎么样,假设有2个线程,分别做2件事情,T1线程代码逻辑:
while(!条件满足) // line 1
{ obj.wait(); // line 2
}
doSomething();
T2线程的代码逻辑:
更改条件为满足; // line 1
obj.notify(); // line 2
多线程环境下没有synchronized,没有锁的情况下可能会出现如下执行顺序情况:
- 1、T1 line1 满足while 条件
- 2、T2 line1 执行
- 3、T2 line2 执行,notify发出去了
- 4、T1 line2 执行,wait再执行
这样的执行顺序导致了notify通知发出去了,但没有用,已经wait是在之后执行,所以有人说没有保证原子性,就是line1 和line2 是一起执行结束,这个也被称作lost wake up问题。解决方法就是可以利用synchronized来加锁,于是有人就写了这样的代码:
synchronized(lock)
{ while(!条件满足) { obj.wait(); } doSomething();
}
synchronized(lock)
{ 更改条件为满足; obj.notify();
}
这样靠锁来做达到目的。但这代码会造成死锁,因为先T1 wait(),再T2 notify();而问题在于T1持有lock后block住了,T2一直无法获得lock,从而永无可能notify()并将T1的block状态解除,就与T1形成了死锁。所以JVM在实现wait()方法时,一定需要先隐式的释放lock,再block,并且被notify()后从wait()方法返回前,隐式的重新获得了lock后才能继续user code的执行。要做到这点,就需要提供lock引用给obj.wait()方法,否则obj.wait()不知道该隐形释放哪个lock,于是调整之后的结果如下:
synchronized(lock)
{ while(!条件满足) { obj.wait(lock); // obj.wait(lock)伪实现 // [1] unlock(lock) // [2] block住自己,等待notify() // [3] 已被notify(),重新lock(lock) // [4] obj.wait(lock)方法成功返回 } doSomething();
}
[最终形态] 把lock和obj合一
其它线程API如PThread提供wait()函数的签名是类似cond_wait(obj, lock)的,因为同一个lock可以管多个obj条件队列。而Java内置的锁与条件队列的关系是1:1,所以就直接把obj当成lock来用了。因此此处就不需要额外提供lock,而直接使用obj即可,代码也更简洁:
synchronized(obj)
{ while(!条件满足) { obj.wait(); } doSomething();
}
synchronized(lock)
{ 更改条件为满足; obj.notify();
}
lost wake up
wait/notify 如果不跟synchronized结合就会造成lost wake up,难以唤醒wait的线程,所以单独使用会有问题。
参考博客
为什么 java wait/notify 必须与 synchronized 一起使用,jvm究竟做了些什么
面试官:为什么wait()方法要放在同步块中?
深入理解Java并发之synchronized实现原理