避免死锁危险

在并发环境中,我们为了保证共享可变数据的线程安全性,需要使用加锁机制,如果锁使用不当可能会引起死锁,线程饥饿等问题 。
在JAVA应用程序中如果发生死锁,程序是无法自动恢复的,严重会造成程序崩溃,所以开发中在设计阶段就要规避死锁发生的情况 。
 
什么是死锁 
死锁:每个线程拥有其他线程需要的资源,同时又等待其他线程拥有的资源,并且每个线程在获得所需要的资源前都不会放弃已经拥有的资源 。
程序死锁发生的场景:
1)交叉锁导致死锁
在线程A持有锁L并想获取锁R的同时,线程B持有锁R并尝试获得锁L,那么这两个线程将永远的阻塞下去 。交叉锁的发生一般是因为线程以不同的顺序获取锁 。
2)资源死锁
内存不足或者我们在程序中使用了线程池和信号量对资源进行限制时,两个线程互相等待彼此释放资源而进入永久阻塞 。
3)死循环死锁
程序由于代码缺陷或者重试机制而使代码陷入死循环,造成了内存和cpu的大量消耗而使线程进入阻塞 。
当Java程序发生死锁时,阻塞的线程将永远不能使用了,而且可能造成程序停止或者使CPU飙高使程序性能很差 。恢复程序的唯一方式就是重启应用 。
死锁的发生大多数是偶然情况,并不代表一个类发生死锁,它就一直死锁,这也是死锁难以排查的原因 。
通过死锁发生的场景我们可以总结出死锁发生的条件:

  • 互斥:即锁具有排他性,只有一个线程能够获取锁;
  • 占有且等待:线程获取到锁时,如果需要的资源没有获取到将一直阻塞等待需要的资源;
  • 不可抢占:获取锁的线程持有的资源不能被其他线程抢占;
  • 循环等待:陷入死锁等待的线程一定是形成了一个循环等待环路 。
 
死锁的检测 
如果一个程序一次最多获得一个锁,那么就不会发生死锁问题,但是开发中经常出现程序需要获取多个锁的场景,那么这个时候就必须考虑锁的顺序问题 。
如果所有的线程以固定的顺序获取锁也是不会出现死锁问题的,当线程试图以不同的顺序来获取锁时,死锁将会发生 。
下面的示例将会发生死锁:
public class DeadlockTest {    //创建两个锁对象    private final Object leftMonitor = new Object();    private final Object rightMonitor = new Object();    /**     * 持有L锁想要获取R锁     */    @SneakyThrows    public void leftForRight() {        synchronized (leftMonitor){            //休眠一下,给R加锁的机会            TimeUnit.SECONDS.sleep(1);            synchronized (rightMonitor){                System.out.println("leftForRight获取到锁");            }        }    }    /**     * 持有R锁获取L锁     */    public void rightForLeft() {        synchronized (rightMonitor){            synchronized (leftMonitor){                System.out.println("rightForLeft获取到锁");            }        }    }    public static void main(String[] args) {        DeadlockTest deadlockTest = new DeadlockTest();        ExecutorService executor = Executors.newFixedThreadPool(2);        executor.execute(()->{            deadlockTest.leftForRight();        });        executor.execute(()->{            deadlockTest.rightForLeft();        });        executor.shutdown();    }}


推荐阅读