锁的定义:死锁是指两个或两个以上的进程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。竞争的资源能够是:锁、网络链接、磁盘共享变量等一切能够称做是 【资源】的东西。java
咱们使用锁来保证线程安全,可是使用不当与滥用可能就会引发死锁。并发程序一旦死锁,通常没有特别好的办法,不少时候只能重启。因此咱们必定要比避免死锁。数据库
举个不恰当的例子:如今岳不群经过阴谋手段获取到了葵花宝典的上册,而后就闭关修炼自宫了,此刻他想继续争夺下册一块练,否则自宫就白忙活了。这个时候下册被林平之拿到了,他也要修炼葵花宝典,因此藏着下册去找上册来自宫。如今问题来了,岳不群找不到下册。林平之拿不到上册,两我的就只能干瞪眼谁也不愿交出本身的,同事还要获取对方的。安全
若是此时有一个线程 A ,按照先获持有锁 a 再获取锁 b的顺序得到锁,同时另一个线程 B,按照先获取锁 b 再获取锁 a 的顺序获取锁。以下图所示: 其实线程 A 就是岳不群、线程 B 是林平之,葵花宝典上下册分别是 lockA,lockBbash
接着咱们用代码模拟上线的执行过程,默认使用 SpringBoot 环境网络
@Component
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public void deadLock() {
Thread threadA = new Thread(() -> {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "尝试获取 lockB ");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(() -> {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取 lockB 成功 ");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "尝试获取 lockA ");
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
threadB.start();
}
}
复制代码
单元测试并发
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Autowired
private DeadLock deadLock;
@Test
public void contextLoads() {
deadLock.deadLock();
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
控制台打印工具
Thread-4获取 lockB 成功
Thread-3获取 lockA 成功
Thread-3尝试获取 lockB
Thread-4尝试获取 lockA
复制代码
咱们能够发现 Thread-3 获取 lockA 成功后尝试获取 lockB 一直不能成功。相互等待对方释放造成了死锁。单元测试
该指令能够生成虚拟机当前时刻的线程快照。线程快照是当前每一条线程正在执行的方法对战的集合,主要目的是定位线程出现长时间停顿的缘由,好比 线程间死锁
、死循环
、请求外部资源致使的长时间等待
等。测试
先经过 jps 获取正在执行的进程 id。ui
$ jps
23264 Jps
8472 JUnitStarter
复制代码
再用jstack 查看当前进程的堆栈信息
$ jstack -F 8472
Attaching to process ID 8472, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.181-b13
Deadlock Detection:
Found one Java-level deadlock:
=============================
"Thread-4":
waiting to lock Monitor@0x000000001f0134f8 (Object@0x00000007721d90f0, a java/lang/Object),
which is held by "Thread-3"
"Thread-3":
waiting to lock Monitor@0x000000001f011ef8 (Object@0x00000007721d90e0, a java/lang/Object),
which is held by "Thread-4"
Found a total of 1 deadlock.
Thread 21: (state = BLOCKED)
- com.zero.demo.deadlock.DeadLock.lambda$deadLock$1() @bci=79, line=35 (Interpreted frame)
- com.zero.demo.deadlock.DeadLock$$Lambda$170.run() @bci=0 (Interpreted frame)
- java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)
Thread 20: (state = BLOCKED)
- com.zero.demo.deadlock.DeadLock.lambda$deadLock$0() @bci=79, line=20 (Interpreted frame)
- com.zero.demo.deadlock.DeadLock$$Lambda$169.run() @bci=0 (Interpreted frame)
- java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)
复制代码
能够看到存在死锁 Found a total of 1 deadlock.
咱们知道了死锁如何产生的,那么就知道该如何去预防。若是一个线程每次只能获取一个锁,那么就不会出现因为嵌套持有锁顺序致使的死锁。
若是必须获取多个锁,咱们就要考虑不一样线程获取锁的顺序。
上面的例子出现死锁的根本缘由就是获取所的顺序是乱序的,超乎咱们控制的。上面例子最理想的状况就是把业务逻辑抽离出来,把获取锁的代码放在一个公共的方法里面,让这两个线程获取锁
都是从个人公共的方法里面获取,当Thread1线程进入公共方法时,获取了A锁,另外Thread2又进来了,可是A锁已经被Thread1线程获取了,因此只能阻塞等待。Thread1接着又获取锁B,Thread2线程就不能再获取不到了锁A,更别说再去获取锁B了,这样就有必定的顺序了。只有当线程1释放了全部锁,线程B才能获取。
好比前面的例子咱们改为
@Component
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public void deadLock() {
Thread threadA = new Thread(() -> {
getLock();
});
Thread threadB = new Thread(() -> {
getLock();
});
threadA.start();
threadB.start();
}
private void getLock() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "尝试获取 lockB ");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
复制代码
查看打印结果,咱们发现 线程4 获取成功而后线程3才能继续获取。
Thread-4获取 lockA 成功
Thread-4尝试获取 lockB
Thread-4获取 lockB 成功
Thread-3获取 lockA 成功
Thread-3尝试获取 lockB
Thread-3获取 lockB 成功
复制代码
当线程获取锁超时了则放弃,这样就避免了出现死锁获取的状况。当使用synchronized关键词提供的内置锁时,只要线程没有得到锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException
方法,该方法能够按照固定时长等待锁,所以线程能够在获取锁超时之后,主动释放以前已经得到的全部的锁。经过这种方式,也能够颇有效地避免死锁。
咱们再来回顾一下死锁的定义,“死锁是指两个或两个以上的进程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。” 死锁条件里面的竞争资源,能够是线程池里的线程、网络链接池的链接,数据库中数据引擎提供的锁,等等一切能够被称做竞争资源的东西。
final ExecutorService executorService =
Executors.newSingleThreadExecutor();
Future<Long> f1 = executorService.submit(new Callable<Long>() {
public Long call() throws Exception {
System.out.println("start f1");
Thread.sleep(1000);//延时
Future<Long> f2 =
executorService.submit(new Callable<Long>() {
public Long call() throws Exception {
System.out.println("start f2");
return -1L;
}
});
System.out.println("result" + f2.get());
System.out.println("end f1");
return -1L;
}
});
复制代码
线程池类型是单一线程,可是任务1依赖任务2的执行结果,因为单线程模式,任务1没有执行完,任务2永远得不到执行,就死锁了。
在个人理解当中,死锁就是“两个任务以不合理的顺序互相争夺资源”形成,所以为了规避死锁,应用程序须要妥善处理资源获取的顺序。 另外有些时候,死锁并不会立刻在应用程序中体现出来,在一般状况下,都是应用在生产环境运行了一段时间后,才开始慢慢显现出来,在实际测试过程当中,因为死锁的隐蔽性,很难在测试过程当中及时发现死锁的存在,并且在生产环境中,应用出现了死锁,每每都是在应用情况最糟糕的时候——在高负载状况下。所以,开发者在开发过程当中要谨慎分析每一个系统资源的使用状况,合理规避死锁,另一旦出现了死锁,也能够尝试使用本文中提到的一些工具,仔细分析,老是能找到问题所在的。
关注公众号 JavaStorm 转发与点赞一块儿牛逼。