Java并发编程艺术 - 死锁产生及优化

本文介绍死锁产生的条件及优化方案。

死锁产生

  1. 线程互相等待

    常见的死锁有JDK死锁和数据库死锁。

    以JDK死锁为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class Deadlock {
    static class Friend {
    private final String name;
    public Friend(String name) {
    this.name = name;
    }
    public String getName() {
    return this.name;
    }

    //同步锁bow
    public synchronized void bow(Friend bower) {
    System.out.format("%s: %s"
    + " has bowed to me!%n",
    this.name, bower.getName());
    //调用同步锁bowBack
    bower.bowBack(this);
    }

    //同步锁bowBack
    public synchronized void bowBack(Friend bower) {
    System.out.format("%s: %s"
    + " has bowed back to me!%n",
    this.name, bower.getName());
    }
    }

    public static void main(String[] args) {
    final Friend alphonse =
    new Friend("Alphonse");
    final Friend gaston =
    new Friend("Gaston");
    new Thread(new Runnable() {
    public void run() { alphonse.bow(gaston); }
    }).start();
    new Thread(new Runnable() {
    public void run() { gaston.bow(alphonse); }
    }).start();
    }
    }

    输出:

    1
    2
    Alphonse: Gaston  has bowed to me!
    Gaston: Alphonse has bowed to me!

    分析:

    执行程序期望是alphonse向gaston鞠躬,并等待gaston还礼;gaston向alphonse鞠躬,并等待alphonse还礼。

    两个线程,分别是alphonse线程和gaston线程;alphonse线程传入gaston对象,gaston传入alphonse对象,均执行执行bowBack

    问题是:
    如果单线程执行,例如alphonse线程执行,结果会是:

    1
    2
    Alphonse: Gaston  has bowed to me!
    Gaston: Alphonse has bowed back to me!

    多线程执行时,alphonse和gaston互相鞠躬,但是均等待对方回礼,则在bowBack产生等待死锁。

    • alphonse和gaston均为对象锁,但是内部进行了互相调用bowBack(Friend bower)

    • alphonse获得对象锁,gaston获得对象锁。

    • alphonse同步调用bow,gaston同步调用bow;没有问题,不会死锁。

    • alphonse同步锁继续,使用gaston对象调用bowBack,但是gaston也在使用alphonse对象调用bowBack,产生问题。

    • alphonse->gaston->bowBack;gaston->alphonse->bowBack;因此alphonse和gaston对象锁都互相加锁且等待对方释放锁,导致死锁。

  2. 死锁查看

    查看进程执行情况:

    1
    jstack -l 69733

或者采用VisualVM,查看两个对象锁处于等待状态
VisualVM

  1. 除了死锁还有活锁、饥饿锁

    Oracle说明

避免死锁方法

  1. 避免一个线程操作获取多个锁,例如上例中一个方法内获取两个对象锁

  2. 避免一个线程在锁内占用多个资源,尽量保证每个锁占用一个,例如上例一个方法期望锁定两个对象

  3. 采用定时锁,即lock.tryLock(timeout),即在限定时间内获取锁,获取不到则放弃,防止死锁等待

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * @return {@code true} 如果当前线程请求到锁或者本来就拥有锁
    * @throws InterruptedException if the current thread is interrupted
    * @throws NullPointerException if the time unit is null
    */
    public boolean tryLock(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
  4. 对于数据库锁,加锁和解锁在一个数据库连接,否则解锁失败。同一个对象锁进行加锁和解锁操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try {
    //关闭事务自动提交(开启事务)
        this.connection.setAutoCommit(false);
        //无异常,手动提交
        this.connection.commit();
    catch(Exception e) {
    //当前;连接回滚
        this.connection.rollback();
    }

并发包JUC使用

参考Orace文档

------ 本文结束------

本文标题:Java并发编程艺术 - 死锁产生及优化

文章作者:Perkins

发布时间:2019年04月22日

原始链接:https://perkins4j2.github.io/posts/20463/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。