《重新学习多线程》 -- 多把锁,死锁

130 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

多把锁

一间大屋子有两个功能:睡觉、学习,互不相干。

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法是准备多个房间(多个对象锁) 例如

class BigRoom {

    public void sleep() {

        synchronized (this) {

        log.debug("sleeping 2 小时");

        Sleeper.sleep(2);

    }

}

public void study() {

    synchronized (this) {

        log.debug("study 1 小时");

        Sleeper.sleep(1);

    }
}

}

执行

BigRoom bigRoom = new BigRoom();

new Thread(() -> {

    bigRoom.compute();

},"小南").start();

new Thread(() -> {

    bigRoom.sleep();

},"小女").start();

结果:

image.png

改进:

carbon (18).png

将锁的粒度细分

  • 好处,是可以增强并发度

  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁 例:

carbon (19).png

定位死锁

  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

image.png

image.png

  • 避免死锁要注意加锁顺序

  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

哲学家就餐问题

image.png

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。

  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。

  • 如果筷子被身边的人拿着,自己就得等待

筷子类

class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

哲学家类

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while (true) {
            // 尝试获得左手筷子
            synchronized (left) {
                // 尝试获得右手筷子
                synchronized (right) {
                    eat();
                }
            }
        }
    }

    Random random = new Random();
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

就餐

public static void main(String[] args) {
    Chopstick c1 = new Chopstick("1");
    Chopstick c2 = new Chopstick("2");
    Chopstick c3 = new Chopstick("3");
    Chopstick c4 = new Chopstick("4");
    Chopstick c5 = new Chopstick("5");
    new Philosopher("苏格拉底", c1, c2).start();
    new Philosopher("柏拉图", c2, c3).start();
    new Philosopher("亚里士多德", c3, c4).start();
    new Philosopher("赫拉克利特", c4, c5).start();
    new Philosopher("阿基米德", c5, c1).start();
}

image.png 会发现执行多次之后卡住无法向下执行 使用jconsole检查死锁

名称: 柏拉图
状态: cn.itcast.n4.deadlock.v1.Chopstick@407a59ea上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0

堆栈跟踪: 
cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
   - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@513e2c4d
   
名称: 苏格拉底
状态: cn.itcast.n4.deadlock.v1.Chopstick@513e2c4d上的BLOCKED, 拥有者: 柏拉图
总阻止数: 5, 总等待数: 6

堆栈跟踪: 
cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
   - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@118b2d80

名称: 柏拉图
状态: cn.itcast.n4.deadlock.v1.Chopstick@407a59ea上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0

堆栈跟踪: 
cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
   - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@513e2c4d
   
名称: 亚里士多德
状态: cn.itcast.n4.deadlock.v1.Chopstick@16faf7d6上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 8, 总等待数: 8

堆栈跟踪: 
cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
   - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@407a59
   
名称: 赫拉克利特
状态: cn.itcast.n4.deadlock.v1.Chopstick@26c4e013上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0

堆栈跟踪: 
cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
   - 已锁定 cn.itcast.n4.deadlock.v1.Chopstick@16faf7d6

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况