承接上文
https://www.cnblogs.com/wkfvawl/p/15489569.html
睡觉、学习,互不相干
。并发度很低。
小南获得锁之后, 学完习之后, 小女才能进来睡觉。@Slf4j(topic = "c.BigRoom") public class BigRoomTest { public static void main(String[] args) { BigRoom bigRoom = new BigRoom(); new Thread(() -> bigRoom.sleep(), "小南").start(); new Thread(() -> bigRoom.study(), "小女").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); } } }
改进方法是准备多个房间(多个对象锁)
小南, 小女
获取不同的锁即可
@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); } } }
将锁的粒度细分
增强并发度
容易发生死锁
因为某种原因,使得代码一直无法执行完毕,这样的现象叫做 活跃性
活跃性相关的一系列问题都可以用 ReentrantLock 进行解决。
有这样的情况:一个线程需要 同时获取多把锁,这时就容易发生死锁
如:线程1获取A对象锁, 线程2获取B对象锁; 此时线程1又想获取B对象锁, 线程2又想获取A对象锁; 它们都等着对象释放锁, 此时就称为死锁
public static void main(String[] args) { final Object A = new Object(); final Object B = new Object(); new Thread(()->{ synchronized (A) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { } } }).start(); new Thread(()->{ synchronized (B) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (A) { } } }).start(); }
互斥条件
在一段时间内,一种资源只能被一个进程所使用
请求和保持条件
进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源
不可抢占条件
进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放
循环等待条件
发生死锁时,必然存在一个进程——资源的循环链。
在Java控制台中的Terminal中输入 jps 指令可以查看正在运行中的进程ID,使用 jstack 进程ID 可以查看进程状态。
打开jconsole,连接到死锁程序的线程
有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 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("阿基米德", c5, c1).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 + '}'; } }
通过jps, jstack 进程id
查看死锁原因
Found one Java-level deadlock:
发现了一个Java级别的死锁
Found one Java-level deadlock: ============================= "阿基米德": waiting to lock monitor 0x000000001fd941a8 (object 0x000000076b735028, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "苏格拉底" "苏格拉底": waiting to lock monitor 0x000000001ccd33c8 (object 0x000000076b735068, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "柏拉图" "柏拉图": waiting to lock monitor 0x000000001ccd3318 (object 0x000000076b7350a8, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "亚里士多德" "亚里士多德": waiting to lock monitor 0x000000001ccd0a88 (object 0x000000076b7350e8, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "赫拉克利特" "赫拉克利特": waiting to lock monitor 0x000000001ccd0b38 (object 0x000000076b735128, a cn.itcast.n4.deadlock.v1.Chopstick), which is held by "阿基米德" Java stack information for the threads listed above: =================================================== "阿基米德": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b735028> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b735128> (a cn.itcast.n4.deadlock.v1.Chopstick) "苏格拉底": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b735068> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b735028> (a cn.itcast.n4.deadlock.v1.Chopstick) "柏拉图": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b7350a8> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b735068> (a cn.itcast.n4.deadlock.v1.Chopstick) "亚里士多德": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b7350e8> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b7350a8> (a cn.itcast.n4.deadlock.v1.Chopstick) "赫???克利特": at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41) - waiting to lock <0x000000076b735128> (a cn.itcast.n4.deadlock.v1.Chopstick) - locked <0x000000076b7350e8> (a cn.itcast.n4.deadlock.v1.Chopstick) Found 1 deadlock.
活锁
出现在两个线程 互相改变对方的结束条件
,谁也无法结束。
@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(); } }
在线程执行时,中途给予 不同的间隔时间, 让某个线程先结束即可。
顺序加锁
时,可能会出现饥饿现象
相对于synchronized,ReentrantLock 所具备的特点
支持锁重入
可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此 有权利再次获取这把锁
可中断
lock.lockInterruptibly() : 可以被其他线程打断的中断锁
可以设置超时时间
lock.tryLock(时间) : 尝试获取锁对象, 如果超过了设置的时间, 还没有获取到锁, 此时就退出阻塞队列, 并释放掉自己拥有的锁
可以设置为公平锁
(先到先得) 默认是非公平, true为公平 new ReentrantLock(true)
支持多个条件变量( 有多个waitset)
(可避免虚假唤醒) - lock.newCondition()创建条件变量对象; 通过条件变量对象调用 await/signal方法, 等待/唤醒
synchronized是关键字级别的加锁,ReentrantLock则是对象级别的,基本语法如下:
//获取ReentrantLock对象 private ReentrantLock lock = new ReentrantLock(); //加锁 lock.lock(); try { //需要执行的代码 }finally { //释放锁 lock.unlock(); }
同一个线程如果首次获得了这把锁
,那么因为它是这把锁的拥有者
,因此 有权利再次获取这把锁@Slf4j(topic = "c.TestReentrant") public class TestReentrant { static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { method1(); } public static void method1() { lock.lock(); try { log.debug("execute method1"); method2(); } finally { lock.unlock(); } } public static void method2() { lock.lock(); try { log.debug("execute method2"); method3(); } finally { lock.unlock(); } } public static void method3() { lock.lock(); try { log.debug("execute method3"); } finally { lock.unlock(); } } }
(针对于lockInterruptibly()方法获得的中断锁) 直接退出阻塞队列, 获取锁失败
synchronized 和 reentrantlock.lock() 的锁, 是不可被打断的; 也就是说别的线程已经获得了锁, 我的线程就需要一直等待下去. 不能中断 可被中断的锁, 通过lock.lockInterruptibly()获取的锁对象, 可以通过调用阻塞线程的interrupt()方法
如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败
处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行
可中断的锁, 在一定程度上可以被动的减少死锁的概率, 之所以被动, 是因为我们需要手动调用阻塞线程的interrupt方法;
测试使用lock.lockInterruptibly()可以从阻塞队列中,打断
private static void test1() { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("启动..."); try { lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("等锁的过程中被打断"); return; } try { log.debug("获得了锁"); } finally { lock.unlock(); } }, "t1"); //主线程上锁 lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(1); t1.interrupt(); log.debug("执行打断"); } finally { lock.unlock(); } }
直接退出阻塞队列, 获取锁失败
防止无限制等待, 减少死锁
获取锁的过程中, 如果超过等待时间, 或者被打断, 就直接从阻塞队列移除, 此时获取锁就失败了, 不会一直阻塞着 ! (可以用来实现死锁问题)
不设置等待时间, 立即失败
@Slf4j(topic = "c.ReentrantTest") public class ReentrantTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("尝试获得锁"); // 此时肯定获取失败, 因为主线程已经获得了锁对象 if (!lock.tryLock()) { log.debug("获取立刻失败,返回"); return; } try { log.debug("获得到锁"); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("获得到锁"); t1.start(); // 主线程2s之后才释放锁 sleep(2); log.debug("释放了锁"); lock.unlock(); } }
设置等待时间, 超过等待时间还没有获得锁, 失败, 从阻塞队列移除该线程
@Slf4j(topic = "c.ReentrantTest") public class ReentrantTest { private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { log.debug("尝试获得锁"); try { // 设置等待时间, 超过等待时间 / 被打断, 都会获取锁失败; 退出阻塞队列 if (!lock.tryLock(1, TimeUnit.SECONDS)) { log.debug("获取锁超时,返回"); return; } } catch (InterruptedException e) { log.debug("被打断了, 获取锁失败, 返回"); e.printStackTrace(); return; } try { log.debug("获得到锁"); } finally { lock.unlock(); } }, "t1"); lock.lock(); log.debug("获得到锁"); t1.start(); // t1.interrupt(); // 主线程2s之后才释放锁 sleep(2); log.debug("main线程释放了锁"); lock.unlock(); } }
超时的打印
中断的打印
通过lock.tryLock()
来解决, 哲学家就餐
问题 (重点
)
lock.tryLock(时间)
: 尝试获取锁对象, 如果超过了设置的时间, 还没有获取到锁, 此时就退出阻塞队列, 并释放掉自己拥有的锁
@Override public void run() { while (true) { // 获得了left左手边筷子 (针对五个哲学家, 它们刚开始肯定都可获得左筷子) if (left.tryLock()) { try { // 此时发现它的right筷子被占用了, 使用tryLock(), // 尝试获取失败, 此时它就会将自己左筷子也释放掉 // 临界区代码 if (right.tryLock()) {//尝试获取右手边筷子, 如果获取失败, 则会释放左边的筷子 try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } }
ReentrantLock默认是非公平锁, 可以指定为公平锁。
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。一般不设置ReentrantLock为公平的, 没必要,会降低并发度
Synchronized底层的Monitor锁就是不公平的, 和谁先进入阻塞队列是没有关系的。
//默认是不公平锁,需要在创建时指定为公平锁 ReentrantLock lock = new ReentrantLock(true);
公平锁 (new ReentrantLock(true))
非公平锁 (synchronized, new ReentrantLock())
所以公平和非公平的区别 : 线程执行同步代码块时, 是否回去尝试获取锁, 如果会尝试获取锁, 那就是非公平的, 如果不会尝试获取锁, 直接进入阻塞队列, 再等待被唤醒, 那就是公平的
如果不进如队列呢? 线程一直尝试获取锁不就行了?
一直尝试获取锁, 在synchronized轻量级锁升级为重量级锁时, 做的一个优化, 叫做自旋锁, 一般很消耗资源, cpu一直空转, 最后获取锁也失败, 所以不推荐使用。在jdk6对于自旋锁有一个机制, 在重试获得锁指定次数就失败等等
(可避免虚假唤醒) - lock.newCondition()创建条件变量对象; 通过条件变量对象调用await/signal
方法, 等待/唤醒
使用要点:
ReentrantLock可以设置多个条件变量(多个休息室), 相对于synchronized底层monitor锁中waitSet
@Slf4j(topic = "c.ConditionVariable") public class ConditionVariable { private static boolean hasCigarette = false; private static boolean hasTakeout = false; private static final ReentrantLock lock = new ReentrantLock(); // 等待烟的休息室 static Condition waitCigaretteSet = lock.newCondition(); // 等外卖的休息室 static Condition waitTakeoutSet = lock.newCondition(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { log.debug("有烟没?[{}]", hasCigarette); while (!hasCigarette) { log.debug("没烟,先歇会!"); try { // 此时小南进入到 等烟的休息室 waitCigaretteSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("烟来咯, 可以开始干活了"); } finally { lock.unlock(); } }, "小南").start(); new Thread(() -> { lock.lock(); try { log.debug("外卖送到没?[{}]", hasTakeout); while (!hasTakeout) { log.debug("没外卖,先歇会!"); try { // 此时小女进入到 等外卖的休息室 waitTakeoutSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("外卖来咯, 可以开始干活了"); } finally { lock.unlock(); } }, "小女").start(); sleep(1); new Thread(() -> { lock.lock(); try { log.debug("送外卖的来咯~"); hasTakeout = true; // 唤醒等外卖的小女线程 waitTakeoutSet.signal(); } finally { lock.unlock(); } }, "送外卖的").start(); sleep(1); new Thread(() -> { lock.lock(); try { log.debug("送烟的来咯~"); hasCigarette = true; // 唤醒等烟的小南线程 waitCigaretteSet.signal(); } finally { lock.unlock(); } }, "送烟的").start(); } }
里面一些代码细节参见之前的博客,wait/notify的正确使用:https://www.cnblogs.com/wkfvawl/p/15489569.html#scroller-5
@Slf4j(topic = "c.Test25") public class Test25 { //定义锁对象 static final Object lock = new Object(); // 表示 t2 是否运行过 static boolean t2runned = false; public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock) { //使用while循环来解决虚假唤醒 while (!t2runned) { try { // 进入等待(waitset), 会释放锁 lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1"); } }, "t1"); Thread t2 = new Thread(() -> { synchronized (lock) { log.debug("2"); t2runned = true; lock.notify(); } }, "t2"); t1.start(); t2.start(); } }
@Slf4j(topic = "c.SyncPrintWaitTest") public class SyncPrintWaitTest { public static final ReentrantLock lock = new ReentrantLock(); public static Condition condi tion = lock.newCondition(); // t2线程释放执行过 public static boolean t2Runned = false; public static void main(String[] args) { Thread t1 = new Thread(() -> { lock.lock(); try { // 临界区 while (!t2Runned) { try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("1"); } finally { lock.unlock(); } }, "t1"); Thread t2 = new Thread(() -> { lock.lock(); try { log.debug("2"); t2Runned = true; condition.signal(); } finally { lock.unlock(); } }, "t2"); t1.start(); t2.start(); } }
@Slf4j(topic = "c.SyncPrintWaitTest") public class SyncPrintWaitTest { public static void main(String[] args) { Thread t1 = new Thread(() -> { // 暂停 LockSupport.park(); log.debug("1"); }, "t1"); t1.start(); new Thread(() -> { log.debug("2"); // 唤醒t1 LockSupport.unpark(t1); }, "t2").start(); } }
需求
@Slf4j(topic = "c.Test27") public class Test27 { public static void main(String[] args) { // 最开始的等待标记是1 循环次数5次 WaitNotify wn = new WaitNotify(1, 5); new Thread(() -> { wn.print("a", 1, 2); }).start(); new Thread(() -> { wn.print("b", 2, 3); }).start(); new Thread(() -> { wn.print("c", 3, 1); }).start(); } } /* 输出内容 等待标记 下一个标记 a 1 2 b 2 3 c 3 1 */ class WaitNotify { // 打印 a 1 2 public void print(String str, int waitFlag, int nextFlag) { for (int i = 0; i < loopNumber; i++) { synchronized (this) { while(flag != waitFlag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(str); // 修改等待标记 让下一个线程打印 flag = nextFlag; // 唤醒等待线程 this.notifyAll(); } } } // 当前等待标记 private int flag; // 循环次数 private int loopNumber; public WaitNotify(int flag, int loopNumber) { this.flag = flag; this.loopNumber = loopNumber; } }
@Slf4j(topic = "c.TestWaitNotify") public class TestAwaitSignal { public static void main(String[] args) throws InterruptedException { AwaitSignal awaitSignal = new AwaitSignal(5); Condition a_condition = awaitSignal.newCondition(); Condition b_condition = awaitSignal.newCondition(); Condition c_condition = awaitSignal.newCondition(); new Thread(() -> { awaitSignal.print("a", a_condition, b_condition); }, "a").start(); new Thread(() -> { awaitSignal.print("b", b_condition, c_condition); }, "b").start(); new Thread(() -> { awaitSignal.print("c", c_condition, a_condition); }, "c").start(); Thread.sleep(1000); System.out.println("==========开始========="); awaitSignal.lock(); try { a_condition.signal(); //首先唤醒a线程 } finally { awaitSignal.unlock(); } } } class AwaitSignal extends ReentrantLock { private final int loopNumber; public AwaitSignal(int loopNumber) { this.loopNumber = loopNumber; } // 参数1 打印内容;参数2 进入那一间休息室;参数3 下一间休息室 public void print(String str, Condition condition, Condition next) { for (int i = 0; i < loopNumber; i++) { //加锁 继承自ReentrantLock lock(); try { try { //进入休息室等待 condition.await(); //System.out.print("i:==="+i); System.out.print(str); // 唤醒下一个休息室的线程 next.signal(); } catch (InterruptedException e) { e.printStackTrace(); } } finally { //解锁 unlock(); } } } }
park和unpark没有对象锁的概念了,停止和恢复线程的运行都是以线程自身为单位的,所以实现更为简单。
@Slf4j(topic = "c.TestWaitNotify") public class TestParkUnpark { static Thread a; static Thread b; static Thread c; public static void main(String[] args) { ParkUnpark parkUnpark = new ParkUnpark(5); a = new Thread(() -> { parkUnpark.print("a", b); }, "a"); b = new Thread(() -> { parkUnpark.print("b", c); }, "b"); c = new Thread(() -> { parkUnpark.print("c", a); }, "c"); a.start(); b.start(); c.start(); //主线程先唤醒a LockSupport.unpark(a); } } class ParkUnpark { private final int loopNumber; public ParkUnpark(int loopNumber) { this.loopNumber = loopNumber; } public void print(String str, Thread nextThread) { for (int i = 0; i < loopNumber; i++) { //当前线程先暂停 LockSupport.park(); System.out.print(str); //唤醒下一个线程 LockSupport.unpark(nextThread); } } }
本章我们需要重点掌握的是
分析多线程访问共享资源时,哪些代码片段属于临界区
使用 synchronized 互斥解决临界区的线程安全问题
掌握 synchronized 锁对象语法
掌握 synchronzied 加载成员方法和静态方法语法
掌握 wait/notify 同步方法
使用 lock 互斥解决临界区的线程安全问题
掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
学会分析变量的线程安全性、掌握常见线程安全类的使用
了解线程活跃性问题:死锁、活锁、饥饿
应用方面
互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
原理方面
monitor、synchronized 、wait/notify 原理
synchronized 进阶原理
park & unpark 原理
模式方面
同步模式之保护性暂停
异步模式之生产者消费者
同步模式之顺序控制