《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



相关文章
|
8天前
|
安全 Java
Java的线程同步与通信:深入理解wait、notify和synchronized
Java的线程同步与通信:深入理解wait、notify和synchronized
14 0
|
11天前
|
Java API 调度
Java 多线程系列Ⅲ(wait+notify+notifyAll)
Java 多线程系列Ⅲ(wait+notify+notifyAll)
|
11天前
|
安全 Java API
多线程(JUC, ReentrantLock, 原子类, 线程池, 信号量 Semaphore, CountDownLatch)
多线程(JUC, ReentrantLock, 原子类, 线程池, 信号量 Semaphore, CountDownLatch)
28 4
|
12天前
|
安全 Java 程序员
Java多线程基础-17:简单介绍一下JUC中的 ReentrantLock
ReentrantLock是Java并发包中的可重入互斥锁,与`synchronized`类似但更灵活。
32 0
|
12天前
|
Java
在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()
这篇内容讨论了如何在Java中通过多线程控制特定顺序的打印任务。
19 0
|
12天前
|
Java
Java多线程基础-7:wait() 和 notify() 用法解析
这篇内容探讨了Java中的`wait()`和`notify()`方法在多线程编程中的使用。
19 0
|
24天前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
20 3
|
3天前
|
缓存 Java 测试技术
Java性能优化(八)-多线程调优-线程池大小设置
Java性能优化(八)-多线程调优-线程池大小设置
5 0
|
4天前
|
安全 Java 容器
多线程(进阶四:线程安全的集合类)
多线程(进阶四:线程安全的集合类)
11 0
|
5天前
|
开发框架 监控 Java
【.NET Core】多线程之线程池(ThreadPool)详解(二)
【.NET Core】多线程之线程池(ThreadPool)详解(二)
27 3