JAVA并发编程实战-避免活跃度危险

本文详细探讨了死锁的原理,包括锁顺序死锁、动态顺序死锁及协作对象间的死锁,提供了解决策略如定时锁和线程转储分析。还介绍了活锁、饥饿和弱响应性等活跃度危险。实例演示了如何通过合理设计锁顺序和开放调用来降低死锁风险。
摘要由CSDN通过智能技术生成

思维导图

在这里插入图片描述

1 死锁

比如经典的哲学家进餐问题,很好的阐释了死锁。
在这里插入图片描述
如果每个人都拿起左边的筷子,等待右侧筷子可用,同时不放弃手中的筷子,将导致死锁产生。

当一个线程永远占有有一个锁,而其它线程尝试去占有这个锁,那么它们将永远被阻塞。

1.1 锁顺序死锁

比如下列demo-1:

private final Object leftLock = new Object();
    private final Object rightLock = new Object();

    private final Object tieLock = new Object();

    public void leftOperation() {
        synchronized (leftLock) {
            synchronized (rightLock) {
                doSomething();
            }
        }
    }

    public void rightOperation() {
        synchronized (rightLock) {
            synchronized (leftLock) {
                doElse();
            }
        }
    }

如果两个线程有如下的操作线将发生死锁:
在这里插入图片描述
主要是由于两个线程试图通过不同的顺序获取多个相同的锁。

如果所有的线程能够以固定的秩序获取锁,程序就不会出现锁顺序锁死了。

1.2 动态的锁顺序死锁

有时你并不能一目了然看清是否能避免死锁发生,如下demo-2:

//动态加锁顺序产生死锁
    public void transferMoney(Account from, Account to, Integer amount) {
        synchronized (from) {
            synchronized (to) {
                if (from.getBalance().compareTo(amount) < 0) {
                    throw new IllegalStateException();
                } else {
                    from.debit(amount);
                    to.credit(amount);
                }
            }
        }

    }

如果发生以下序列,将产生死锁:

  • A->B和B->A同时发生,同时持有锁,又互相等待锁。

我们应该制定锁的顺序,并应用到程序中,获得锁的顺序必须始终遵守这个固定顺序。

如下demo-3可以避免死锁:

//制定锁的顺序避免死锁
    public void transferUpdate(Account from, Account to, Integer amount) {
        class Helper{
            public  void transfer(Account from, Account to, Integer amount) {
                if (from.getBalance().compareTo(amount) < 0) {
                    throw new IllegalStateException();
                } else {
                    from.debit(amount);
                    to.credit(amount);
                }
            }
        }
        int fromHashCode = System.identityHashCode(from);
        int toHashCode = System.identityHashCode(to);

        if (fromHashCode < toHashCode) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer(from, to, amount);
                }
            }
        } else if (toHashCode < fromHashCode) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer(from, to, amount);
                }
            }
        } else {
            synchronized (tieLock) {
                synchronized (from) {
                    synchronized (to) {
                        new Helper().transfer(from, to, amount);
                    }
                }
            }
        }
    }

通过对象的固定hash来实现锁顺序策略,如果出现相同,意味着需要引入第三个锁防止陷入重新死锁情况。

由于System.identityHashCode的hash冲突概率很小,所以该技术以最小代价,换来了最大的安全性。

1.3 协作对象间的死锁

思考如下协作对象demo-3:
Taxi类

/**
 * 协作对象间加锁可能产生死锁
 */
public class Taxi {
    private final Dispatcher dispatcher;
    private Point location, destination;

    public Taxi(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    public synchronized Point getLocation() {
        return location;
    }

    //顺序加锁
    public synchronized void setLocation(Point location1) {
        this.location = location1;
        if (location1.equals(destination)) {
            dispatcher.notifyAvaiable(this);
        }
    }
}

Dispatcher类

class Dispatcher {
    private final Set<Taxi> taxis;
    private final Set<Taxi> avaiableTaxis;

    public Dispatcher(Set<Taxi> taxis, Set<Taxi> avaiableTaxis) {
        this.taxis = taxis;
        this.avaiableTaxis = avaiableTaxis;
    }

    public synchronized void notifyAvaiable(Taxi taxi) {
        avaiableTaxis.add(taxi);
    }

    //顺序加锁
    public synchronized Image getImage() {
        Image image = new Image();
        for (Taxi taxi : taxis) {
            image.drawMarker(taxi.getLocation());
        }
        return image;
    }
    
    class Image{

        public void drawMarker(Point location) {

        }
    }
}

虽然没有显示调用锁,但是Taxi.setLocation方法和Dispatcher.getImage由于获取锁仍然可能发生死锁情况:
在这里插入图片描述

1.4 开放调用

在持有锁的时候调用一个外部方法很难进行分析,因此相当危险。
当调用的方法不需要持有锁的时候,被称为开放调用。

上述的demo-3可以通过开放调用,减小死锁风险,做如下改进demo-4:

/**
     * 开放调度,缩减synchronized块。
     */
    public void setLocationNow(Point point) {
        boolean isDestination;
        synchronized (this) {
            location = point;
            isDestination = location.equals(destination) ? true:false;
        }
        if (isDestination) {
            dispatcher.notifyAvaiable(this);
        }
    }
/**
     * 开放调度,缩减synchronized块。
     */
    public Image getImageNow() {
        Image image = new Image();
        Set<Taxi> copy;
        synchronized (this) {
            copy = new HashSet<>(taxis);
        }
        for (Taxi taxi : copy) {
            image.drawMarker(taxi.getLocation());
        }
        return image;
    }

通过减小synchronized块,减少死锁发生情况。

1.5 资源死锁

当持有和等待目标变为资源时,会发生类似的死锁。
比如当一个线程需要获取两个数据库连接时,如果多个线程按照随机顺序获取,大概率出现死锁。

另一种形式的基于资源的死锁是线程饥饿死锁。

2 避免和诊断死锁

首先应该设计时避免死锁:

如果你必须获取多个锁,那么设计锁的顺序就是你的工作:尽量减少锁的交互数量,遵守并文档化锁的顺序。

2.1 尝试定时的锁

可以使用juc中的Lock类代替内部锁机制,因为显示的锁可以让你定义超时事件,不至于一直等待。

如下demo-5演示:

public class LockTes {
    public final Lock left = new ReentrantLock();
    public final Lock right = new ReentrantLock();

    public static void main(String[] args) {
        LockTes lockTes = new LockTes();
        Thread threadOne = new Thread(() -> {
            lockTes.printOne();
        });

        Thread threadTwo = new Thread(() -> {
            lockTes.printTwo();
        });
        threadOne.start();
        threadTwo.start();
    }

    public void printOne() {
        try {
            left.tryLock(5L, TimeUnit.SECONDS);
            Thread.sleep(2000);
            System.out.println("One进入left内部");
            if (!right.tryLock(5L, TimeUnit.SECONDS)) {
                System.out.println("等待获取超时,放弃获取right");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            left.unlock();
        }
    }

    public void printTwo() {
        try {
            right.tryLock(5L, TimeUnit.SECONDS);
            Thread.sleep(10000);
            System.out.println("Two进入right内部");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            right.unlock();
        }
    }
}

运行结果:
在这里插入图片描述

当线程threadOne试图获取right时,将会等待五秒然后失败,因为线程threadTwo获取right后休眠了10s。

2.2 通过线程转储分析死锁

JVM可以使用线程转储帮你识别死锁的发生。

我们以demo-1为例,如下测试的代码:

public static void main(String[] args) {
        LeftRightDeadLock leftRightDeadLock = new LeftRightDeadLock();
        new Thread(() -> {
            try {
                leftRightDeadLock.leftOperation();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            try {
                leftRightDeadLock.rightOperation();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

启动程序后,控制台通过jps获取进程PID:
在这里插入图片描述
之后通过jstack -F pid号强制获取线程转储信息:
在这里插入图片描述

可以看此时数线程间发生的锁持有关系和死锁情况。

3 其它的活跃度危险

包括饥饿、丢失信号和活锁。

3.1 饥饿

当线程访问它需要的资源时却被永久拒绝,将会发生饥饿。
线程API定义了线程优先级作为调度参考。

java定义了10个线程优先级,有些OS本身优先级少于10个,将会导致多个java线程优先级会映射到OS相同的优先级。

通常不使用线程优先级,因为这会增加平台依赖性,并可能引起活跃度问题。

3.2 弱响应性

除饥饿外的另一个问题是弱响应性。
不良的锁管理也可能引起弱响应性:

  • 比如:对一个大容器加锁迭代,迭代元素处理又占有大量时间,将导致其它想要访问改容器线程必须等待很长时间。

3.3 活锁

活锁是线程活跃度失败的另一种形式,尽管没有阻塞,线程仍不能继续进行,因为它不断尝试相同的操作,却总是失败。

比如消息队列的消息回退导致的毒药信息问题。

解决活锁的一种方案是对重试机制引入一些随机性。

参考文献

[1]. 《JAVA并发编程实战》.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LamaxiyaFc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值