引言
死锁状态的大致情况是:Thread_1在获得A对象的锁后,紧接着去请求B对象的锁 ,Thread_2在获得了B对象的锁后,紧接着又去请求A对象的锁,如下图:
一、模拟一个死锁
public class DeadLockDemo {static class A {public synchronized void saying() {System.out.println(Thread.currentThread().getName() + " A start...........");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}new B().saying();System.out.println(Thread.currentThread().getName() + " A end.............");}}static class B {public synchronized void saying() {System.out.println(Thread.currentThread().getName() + " B start...........");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}new A().saying();System.out.println(Thread.currentThread().getName() + " B end.............");}}public static void main(String[] args) {new Thread(() -> new A().saying(), "t1").start();new Thread(() -> new B().saying(), "t2").start();}
}
可以看到在线程 t1 调用A对象的saying互斥方法的时候,t1拿到了A对象的锁,而如果想完成saying方法必须去请求B对象的锁才可以执行到B对象的saying互斥方法。线程 t2调用B对象的saying互斥方法的时候,t2拿到了B对象的锁,而如果想完成saying方法必须去请求A对象的锁才可以执行到A对象的saying互斥方法。
这就导致了死锁的出现,程序会陷入无休止的“死循环”中。
如果没有2秒的睡眠时间,程序会很快因内存溢出而瘫痪:
否则程序会不停的循环下去,直到崩溃。
二、synchronized 锁的错误用法
使用synchronized 并不简单,以下这些用法一定要在实际开发中注意避免。
2.1 synchronized 锁定字符串对象
synchronized 可以给对象加锁,但这些对象不应该包括 String、Integer 这类共享对象。说String、Integer是共享对象,是因为在某些情况下,Java会共享一些数据来提高性能和节约内存。
例如,一个字符串 "Hello",如果以此对象为锁定目标,那么就可能在非常不恰当的位置造成线程阻塞或死锁:
public class T {String s1 = "Hello";String s2 = "Hello";void m1() {synchronized (s1) {}}void m2() {synchronized (s2) {}}
}
如上代码所示,s1 和 s2 虽然声明了两个变量,但实际上,"Hello" 字符串是共享的,因此锁也是一份,如果你不希望造成莫名其妙的线程阻塞,一定要记得synchronized 不要加在 String、Integer 这类对象上。
2.2 锁对象的引用被重新赋值
理解这个问题需要清楚synchronized加锁的目标对象是什么,究竟是栈内存中的引用?还是堆内存中的对象数据?
我们保证同步的目的是有序的执行堆中的数据,所以很明显,synchronized 锁定的应该是堆内存中实际的对象,而不是栈中的引用。
那么如果引用被重新赋值,那么整个并发程序可能造成更加难以排查的问题:
public class T_ChangeLock {private Object lock = new Object();public void doSync() {synchronized (lock) {while (true) {try {TimeUnit.SECONDS.sleep(1);// 打印当前线程System.out.println(Thread.currentThread().getName());} catch (InterruptedException e) {}}}}public static void main(String[] args) throws InterruptedException {T_ChangeLock t = new T_ChangeLock();new Thread(() -> t.doSync(), "t1").start();// 启动第一个线程TimeUnit.SECONDS.sleep(1);// 锁对象改变t.lock = new Object();// 想一想,t2 是否可以被成功阻塞?new Thread(() -> t.doSync(), "t2").start();}
}
doSync 是个同步方法,方法内死循环输出当前线程ID,t1首先抢到 doSync 的执行权(即抢到 lock 锁对象),不出意外的话,其他线程都将无法执行 doSync 方法,然而,在执行了 lock = new Object() 方法后,奇怪的事情发生了:
所以,为了不让你的同步逻辑失效,请谨慎处理锁对象的引用。