目录
多把锁
多把锁的优缺点
活跃性
死锁
手写死锁
死锁的四个必要条件
定位死锁
jconsole运行命令
jps 定位进程 id,再用 jstack 定位死锁
死锁的三种场景
一个线程一把锁
两个线程两把锁
多个线程多把锁
解决死锁
活锁
饥饿
多把锁
现在有一个场景,有一个大屋子,小南想去睡觉,小女想去学习,两个人做的事毫不相关,如果他们同时想去用一间屋子的话,那么并发度就会很低
我们看一下代码 :
@Slf4j(topic = "c.BigRoom") public class BigRoom { public void studying() { synchronized (this) {//对这个大房间加锁 log.debug("study2000ms...."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void sleeping() { synchronized (this) {//对这个大房间加锁 log.debug("sleeping1000ms...."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } class TestMain { public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(()->{ bigRoom.studying(); },"小南").start(); new Thread(()->{ bigRoom.sleeping(); }).start(); } }
也就是说他们做的事情都要对同一个房间加锁,也就是只有一个人用完了这个房间,另一个人才能用.
这和我们以前两个线程同时访问一个共享的东西时一样的.
但是现在小南和小女一个要去学习,一个要取睡觉,两个人做的事情毫不相干,就可以去大房子的不同房间去做事.
也就是我们现在使用多把锁->当多个线程干的事情毫不相干就可以使用多把锁.
@Slf4j(topic = "c.BigRoom") public class BigRoom { private static final Object studyingRoom = new Object(); private static final Object sleepingRoom = new Object(); public void studying() { synchronized (studyingRoom) { log.debug("study2000ms...."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void sleeping() { synchronized (sleepingRoom) { log.debug("sleeping1000ms...."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } class TestMain { public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(()->{ bigRoom.studying(); },"小南").start(); new Thread(()->{ bigRoom.sleeping(); },"小女").start(); } }
我们这时候改了一下代码,小南要去学习就去学习房间,小女要去睡觉就去睡觉房间,两者干的事情毫不相关
这时候,就会提高并发度,让锁的粒度变得更细.
多把锁的优缺点
- 优点 : 可以提高并发度,让锁的粒度变得更细.
- 缺点 : 如果一个线程同时要获得多把锁,容易发生死锁.
活跃性
死锁
当一个线程同时要获取多把锁,这个时候就容易发生死锁.
就比如 t1线程获得A对象的锁,t2线程获得了B对象的锁,
而此时t1线程又想获取B对象的锁,等待t2线程释放B对象的锁,t2线程又想获取A对象的锁,等待t1线程释放A对象的锁.
也就是双方各自有一把锁,还想获取到对方的锁,这就是死锁现象
手写死锁
@Slf4j(topic = "c.DeadLock") public class DeadLock { private static final Object A = new Object(); private static final Object B = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (A) { log.debug("lock A"); //线程t1获取了A对象的锁,1s之后,又要获取B对象的锁 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { log.debug("lockB"); log.debug("操作..."); } } },"t1"); Thread t2 = new Thread(() -> { synchronized (B) { log.debug("lock B"); //线程t2获取了B对象的锁,1s之后,又要获取A对象的锁 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (A) { log.debug("lock A"); log.debug("操作..."); } } },"t2"); t1.start(); t2.start(); } }
死锁的四个必要条件
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。
定位死锁
发生了死锁现象我们怎么来定位死锁呢 ?
我们最经常使用的是jconsole命令工具检测死锁, 也可以使用使用 jps 定位进程 id,再用 jstack 定位死锁
jconsole运行命令
比如我运行刚才死锁的程序.然后利用jconsole来检测死锁.
点击连接.
t1线程也同理
从这里可以看见想获取的锁的持有者,还能看见具体发生死锁现象的代码行数,然后对其进行分析修改打破死锁现象即可.
jps 定位进程 id,再用 jstack 定位死锁
同样也可以从这里可以看见想获取的锁的持有者,还能看见具体发生死锁现象的代码行数,然后对其进行分析修改打破死锁现象即可.
死锁的三种场景
一个线程一把锁
如果是不可重入锁,一个线程加锁两次,就会导致第一个锁的释放依赖第二个锁加锁成功,而第二个加锁成功又依赖第一个锁的释放,导致死锁.
两个线程两把锁
线程A持有资源1,线程B持有资源2,两个线程又同时想得到对方的资源,导致死锁
多个线程多把锁
哲学家就餐问题
最经典的导致死锁的问题,那就是哲学家就餐问题.
哲学家就餐问题说的就是有5位哲学家(相当于5个线程)正在吃饭,但是只有五根筷子(筷子就相当于这5个线程所共享的资源),每位哲学家左边有一个筷子,右边有一根筷子,哲学家只有拿到左边筷子有拿到右边筷子才能吃饭,如果筷子被别的哲学家拿到了,自己只能等待那个哲学家吃完,自己才能吃.
很容易发生死锁的现象,就是五个哲学家同时各拿一根筷子,当哲学家同时又要拿另一根筷子的时候,就会发生死锁,也就是自己持有了一个资源,想获取另一个资源而又等待对方去释放资源.
这样5个哲学家就构成了循环等待的这种关系.
代码实现
import lombok.extern.slf4j.Slf4j; @Slf4j(topic = "c.Philosophers") public class Philosophers extends Thread{ private Chopstick left;//左手筷子 private Chopstick right;//右手筷子 public Philosophers(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();//然后吃饭 } } } } private void eat() { log.debug("eating..."); try { Thread.sleep(1000);//思考1s钟 } catch (InterruptedException e) { e.printStackTrace(); } } }
public class Chopstick { private String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "Chopstick{" + "name='" + name + '\'' + '}'; } }
public class Main { 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 Philosophers("苏格拉底",c1,c2).start(); new Philosophers("柏拉图",c2,c3).start(); new Philosophers("亚里士多德",c3,c4).start(); new Philosophers("赫拉克利特",c4,c5).start(); new Philosophers("阿基米德",c5,c1).start(); } }
这就会导致死锁,五位哲学家构成了循环等待的这种关系
要想打破死锁,可以使用ReentrantLock,或者打破死锁的必要条件之一,最重要的是循环等待必要条件
解决死锁
我们首先看一下产生死锁的必要条件 :
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件 :一次性申请所有的资源。
- 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放.(最最重要)
最最重要的就是破坏循环等待条件,
针对每一把锁进行编号,约定在获取多把锁的时候,明确获取锁的顺序(升序还是降序都可以),所有线程都遵守这样的顺序,同一顺序拿到锁,就会反序释放锁,就会打破循环等待
我们按照顺序在执行一下哲学家就餐问题.
每一个哲学家左右有两个筷子,我们按顺序规定从小到大的顺序来拿筷子(比如1和2先拿1号,2和3先拿2),最后,只有一个哲学家同时拿到两根筷子,等到它吃完了,然后在放下筷子,别的哲学家就可以吃,从小到大的拿取筷子,最后就从大到小的放下筷子.
我们分析一下上面的代码为什么避免了死锁的发生?
我们按照一定顺序加锁后当线程1获得了resource1的监事锁,线程2就获取不到了.然后线程1再去获取resource2的监事锁,可以获取到,然后线程1释放了对resource1,resource2的监事锁的占用,线程2获取到就可以执行了,这样就破坏了循环等待条件.
对于代码层面,我们还可以使用ReentrantLock来解决
活锁
活锁 : 两个线程互相改变对方的结束条件,最终谁也没有结束
解决活锁: 执行时间有一定的交错-->让睡眠的时间是随机数
能让其交错开,第一个线程马上要运行完了,第二个线程就没有机会改变对方的结束条件了
import lombok.extern.slf4j.Slf4j; /** * 活锁 : 两个线程互相改变对方的结束条件,最终谁也没有结束 * 解决活锁: 执行时间有一定的交错-->让睡眠的时间是随机数 * 能让其交错开,第一个线程马上要运行完了,第二个线程就没有机会改变对方的结束条件了 */ @Slf4j(topic = "c.TestLiveLock") public class TestLiveLock { private static int count = 10; public static void main(String[] args) { /** * t1线程想将count=10减到0 */ new Thread(() -> { while (count>0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } count--; log.debug("count : {}",count); } },"t1").start(); /** * t2线程想将count=10减到0 */ new Thread(() -> { while (count<20) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } count++; log.debug("count : {}",count); } },"t2").start(); } }
解决活锁: 执行时间有一定的交错-->让睡眠的时间是随机数
能让其交错开,第一个线程马上要运行完了,第二个线程就没有机会改变对方的结束条件了
饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
还是拿刚才哲学家就餐问题:
虽然打破了死锁的局面,但是发现有的线程执行次数太少,都被其他线程抢去锁了,这就是一种饥饿现象.(拿不到筷子吃不上饭了,都被别人抢去了)
可以通过ReentrantLock来解决.