可重入的锁
Java 线程是可重入的锁。
比如,下列例子:
public class Counter {private int count = 0;public synchronized void add(int n) {if (n < 0) {dec(-n);} else {count += n;}}public synchronized void dec(int n) {count -= n;}}
可以看到,synchronized 修饰 add() 和 dec() 。但线程执行到 add() 方法内部,说明它已经获取了当前的 this 锁。如果传入的 n < 0,将在 add() 调用 dec() 。由于 dec() 方法也需要获取 this 锁。这时 dec() 同样能获取到 this 锁,这是因为 JVM 允许同一线程获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于 Java 的线程锁是可重入锁,并且是使用计时器来达到这个效果。在获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录 +1,每退出 synchronized 块,记录 -1,减到 0 时,才能真正释放锁。
死锁
在获取多个锁时,不同线程获取多个不同对象的锁可能导致死锁:
public void add(int m) {synchronized(lockA) { // 获得lockA的锁this.value += m;synchronized(lockB) { // 获得lockB的锁this.another += m;} // 释放lockB的锁} // 释放lockA的锁}public void dec(int m) {synchronized(lockB) { // 获得lockB的锁this.another -= m;synchronized(lockA) { // 获得lockA的锁this.value -= m;} // 释放lockA的锁} // 释放lockB的锁}
对于上述代码,线程1和线程2如果分别执行 add() 和 dec() 方法时:
- 线程1:进入
add(),获得lockA; - 线程2:进入
dec(),获得lockB。
随后:
- 线程1:准备获得
lockB,失败,等待中; - 线程2:准备获得
lockA,失败,等待中。
此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束 JVM 进程。因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。
如何避免死锁?
线程获取锁的顺序要一致。即严格按照先获取 lockA,再获取 lockB 的顺序,改写 dec() 方法如下:
public void dec(int m) {synchronized(lockA) { // 获得lockA的锁this.value -= m;synchronized(lockB) { // 获得lockB的锁this.another -= m;} // 释放lockB的锁} // 释放lockA的锁}
在理解死锁的时候,和可重入锁有点出入,这里记录一下:
可重入锁之所以可以重复获取锁,这是因为在同一个线程中,而死锁是多个不同的线程,获取对方的锁而导致的问题。
小结
Java 的 synchronized 锁是可重入锁;
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
避免死锁的方法是多线程获取锁的顺序要一致。
