避免死锁危险

在并发环境中,我们为了保证共享可变数据的线程安全性,需要使用加锁机制,如果锁使用不当可能会引起死锁,线程饥饿等问题。

在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();

    }

}

我们可以通过JDK提供的jstack或者jconsole工具查看死锁信息。

jstack -l pid查看堆栈信息:

图片

或者jconsole连接到进程上:

图片

图片

通过堆栈信息能够很直接看到死锁信息。

linux环境下dump出堆栈信息的方法我们后续再聊。

死锁的避免 

我们可以通过打破死锁发生的条件来避免死锁。

程序中的业务要求我们必须使用独占锁而不能使用共享锁,那我们就不能打破锁的互斥性。

破坏占有且等待:一次性申请所有资源;

破坏不可抢占:使用显示锁Lock中的tryLock功能来代替内置锁synchronized,可以检测死锁和从死锁中恢复过来。使用内置锁的线程获取不到锁会被阻塞,而显式锁可以指定一个超时时限(Timeout),在等待设置的时间后tryLock就会返回一个失败信息,也会释放其拥有的资源。

破坏循环等待:使线程按照固定的顺序获取锁,在设计中我们应尽量减少锁的交互数量,提前设计好锁的顺序并严格遵守。

结束语 

并发编程系列基础知识的学习到此结束了,后续如果遇到相关的知识再补充。

下一个系列《Java基础》扬帆启航,类加载、数据结构(包括线程安全的数据结构)等知识将与你相遇。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值