《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)(三)

简介: 《JUC并发编程 - 高级篇》03 - 共享对象之管程 下篇(Monitor | wait&notify | Park&Unpark | 线程状态转换 | 活跃性 | ReentrantLock)

3.13 多把锁

多把不相干的锁


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

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

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

例如

public class TestMultiLock {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            bigRoom.study();
        },"小南").start();
        new Thread(() -> {
            bigRoom.sleep();
        },"小女").start();
    }
}
@Slf4j(topic = "c.BigRoom")
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);
        }
    }
}

67dd0de2ca6fc0e74d5673290fd3930b.png

改进:

@Slf4j(topic = "c.BigRoom")
class BigRoom {
  //使用多把锁,相当于把房间分成了两块,允许两个没有关联的动作同时进行
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}


559f3c6acfdced2cdf5dc57cbc8e1782.png

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

3.14 活跃性

3.14.1 死锁

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

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

代码演示:

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    public static void main(String[] args) {
        test1();
    }
    private static void test1() {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

结果:

15:13:19.339 c.TestDeadLock [t2] - lock B
15:13:19.339 c.TestDeadLock [t1] - lock A

3.14.2 定位死锁

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

  • 使用jps+jstack定位死锁
F:\自学课程\底层课程\Java并发编程\concurrent>jps //查看正在运行的Java进程
11188 Launcher
17828 Jps
15192 TestDeadLock
19404
4684 RemoteMavenServer36
F:\自学课程\底层课程\Java并发编程\concurrent>jstack 15192//查看进程的详细信息
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.192-b12 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000002e13800 nid=0x428c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
//以下是t2线程的详细信息
"t2" #12 prio=5 os_prio=0 tid=0x000000001ed35800 nid=0x290 waiting for monitor entry [0x000000001f37f000]
   java.lang.Thread.State: BLOCKED (on object monitor)//阻塞状态
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
        - waiting to lock <0x000000076ec5d160> (a java.lang.Object)//正在等待的锁...
        - locked <0x000000076ec5d170> (a java.lang.Object)//已经获得的锁
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
//以下是t1线程的详细信息
"t1" #11 prio=5 os_prio=0 tid=0x000000001ed35000 nid=0xcd4 waiting for monitor entry [0x000000001f27f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
        - waiting to lock <0x000000076ec5d170> (a java.lang.Object)
        - locked <0x000000076ec5d160> (a java.lang.Object)
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
//省略部分输出
Found one Java-level deadlock: //发现一个Java级别的死锁
=============================
"t2":
  waiting to lock monitor 0x0000000002f0c0c8 (object 0x000000076ec5d160, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x0000000002f0b9e8 (object 0x000000076ec5d170, a java.lang.Object),
  which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2"://出现死锁的行号信息
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
        - waiting to lock <0x000000076ec5d160> (a java.lang.Object)
        - locked <0x000000076ec5d170> (a java.lang.Object)
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/1792845110.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"t1":
        at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
        - waiting to lock <0x000000076ec5d170> (a java.lang.Object)
        - locked <0x000000076ec5d160> (a java.lang.Object)
        at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/897913732.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
  • 使用Jconsole工具

20ae828913f6a404b8d7e6ef3a8be9b2.png

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待。对于这种情况 linux 下可以通过 jps+jstack方式进行排查。


3.14.3 哲学家就餐问题

4bacad75ae68ec2b1a7d0ea02f5749df.png

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

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待
//就餐测试类
public class TestDeadLock {
    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("阿基米德", c1, c5).start();
    }
}
//哲学家类
@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);
    }
}
//筷子类
class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

执行不多会,就执行不下去了

12:33:15.575 [苏格拉底] c.Philosopher - eating...
12:33:15.575 [亚里士多德] c.Philosopher - eating...
12:33:16.580 [阿基米德] c.Philosopher - eating...
12:33:17.580 [阿基米德] c.Philosopher - eating...
// 卡在这里, 不向下运行

使用 jconsole 检测死锁,发现

-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
  - 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)


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

3.14.4 活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

**解决办法:**让两个线程的执行时间交错,具体方案是通过增加随机睡眠时间。


3.14.5 饥饿


很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。


下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题


d3bd8e97143f1e7932f51619ec59aa1f.png


顺序加锁的解决方案(线程1,2获取锁的顺序是相同的)

39263f2c5c1079527566d31b5abf7ef8.png



案例演示:

71dddf3c85156948744d28048dc81e03.png



相关文章
|
27天前
|
Java 开发者
Java多线程教程:使用ReentrantLock实现高级锁功能
【4月更文挑战第6天】`ReentrantLock`是Java并发编程中一个强大的同步工具,比`synchronized`提供更丰富功能。它支持可响应性、可中断性、公平性选择及条件变量。通过示例展示了创建、公平性设置、可中断锁定、尝试锁定及条件变量的使用。`ReentrantLock`使线程同步更灵活,适用于高性能应用,但使用需谨慎,理解其原理并恰当使用。
|
4月前
|
存储 设计模式 安全
Java 编程问题:十、并发-线程池、可调用对象和同步器2
Java 编程问题:十、并发-线程池、可调用对象和同步器
33 0
|
4月前
|
缓存 安全 Java
Java 编程问题:十、并发-线程池、可调用对象和同步器1
Java 编程问题:十、并发-线程池、可调用对象和同步器
44 0
|
4月前
|
数据处理
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
44 1
|
3天前
多线程售票demo,用ReentrantLock实现
多线程售票demo,用ReentrantLock实现
|
2月前
|
存储 开发框架 安全
【C++ 线程】深入理解C++线程管理:从对象生命周期到线程安全
【C++ 线程】深入理解C++线程管理:从对象生命周期到线程安全
92 0
|
2月前
|
存储 安全 Java
调用链跨线程传递 ThreadLocal 对象对比
说起本地线程专属变量,大家首先会想到的是 JDK 默认提供的 ThreadLocal,用来存储在整个链路中都需要访问的数据,并且是线程安全的。由于在落地全链路压测的过程中,一个基本并核心的功能需求是流量标记需要在整个链路中进行传递,那么线程上下文环境成为解决这个问题最合适的技术。
45 2
调用链跨线程传递 ThreadLocal 对象对比
|
2月前
|
Java
在多线程中sleep()和wait()的区别(详细)
在多线程中sleep()和wait()的区别(详细)
|
3月前
|
Java 调度
多线程(初阶五:wait和notify)
多线程(初阶五:wait和notify)
34 0
|
4月前
|
Java API 索引
Java 编程问题:十、并发-线程池、可调用对象和同步器4
Java 编程问题:十、并发-线程池、可调用对象和同步器
24 0