一、多把锁(P114)
一间大屋子有两个功能:睡觉、学习,互不相干。现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁)
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") public class BigRoom { public void sleep() throws InterruptedException { synchronized (this) { log.debug("sleeping 2 小时"); Thread.sleep(2); } } public void study() throws InterruptedException { synchronized (this) { log.debug("study 1 小时"); Thread.sleep(1); } } }
改进@Slf4j(topic = "c.BigRoom") public class BigRoom { private final Object studyRoom = new Object(); private final Object bedRoom = new Object(); public void sleep() throws InterruptedException { synchronized (bedRoom) { log.debug("sleeping 2 小时"); Thread.sleep(2000); } } public void study() throws InterruptedException { synchronized (studyRoom) { log.debug("study 1 小时"); Thread.sleep(1000); } } }
将锁的粒度细分:
好处:增强并发度。
坏处:如果一个线程需要同时获得多把锁,就容易发生死锁。
二、死锁
一个线程需要同时获取多把锁,这时就容易发生死锁。
【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(); } }
三、定位死锁
检测死锁可以使用 jconsole工具,
或者使用 jps 定位进程 id,再用 jstack 定位死锁:
(1)避免死锁要注意加锁顺序(2)另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top - Hp 进程 id 来定位是哪个线程,最后再用 jstack 排查
四、哲学家就餐问题
有五位哲学家,围坐在圆桌旁。(1)他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。(2)吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。(3)如果筷子被身边的人拿着,自己就得等待
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 + '}'; } }
这种线程没有按预期结束,执行不下去的情况,归类为【 活跃性 】问题,除了死锁以外,还有活锁和饥饿者两种情况
五、活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
@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(); } }
六、饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,又不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁可以解决死锁,但是容易造成饥饿。