6.java并发编程之park&unpark与ReentrantLock

☆1.Park & Unpark

基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程 LockSupport.park(); // 恢复t1线程的运行 LockSupport.unpark(t1)

//先 park 再 unpark:先暂停再继续往下执行 Thread t1 = new Thread(() -> { log.debug("start..."); sleep(1); log.debug("park..."); LockSupport.park(); log.debug("resume..."); } ,"t1"); t1.start(); sleep(2); log.debug("unpark..."); LockSupport.unpark(t1); 输出: 18:42:52.585 c.TestParkUnpark [t1] - start... 18:42:53.589 c.TestParkUnpark [t1] - park... 18:42:54.583 c.TestParkUnpark [main] - unpark... 18:42:54.583 c.TestParkUnpark [t1] - resume...

//先 unpark 再 park:发现park还是继续往下执行 Thread t1 = new Thread(() -> { log.debug("start..."); sleep(2); log.debug("park..."); LockSupport.park(); log.debug("resume..."); } , "t1"); t1.start(); sleep(1); log.debug("unpark..."); LockSupport.unpark(t1); 18:43:50.765 c.TestParkUnpark [t1] - start... 18:43:51.764 c.TestParkUnpark [main] - unpark... 18:43:52.769 c.TestParkUnpark [t1] - park... 18:43:52.769 c.TestParkUnpark [t1] - resume...

特点

与 Object 的 wait & notify 相比 ​ -wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必 ​ -park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】 ​ -park & unpark 可以先 unpark,而 wait & notify 不能先 notify

原理

每个线程都有自己的一个 Parker 对象,由三部分组成 counter ,cond 和 _mutex 。

打个比喻线程就像一个旅人,Parker 就像他随身携带的背包, cond条件变量就好比背包中的帐篷。counter 就好比背包中的备用干粮,0 为没有干粮或者干粮被耗尽,1 为干粮充足,默认是没有干粮的即0。

调用 park 的时候就是看有没有干粮。

调用 unpark,就好比令干粮充足。

1.如果备用干粮耗尽,那么钻进帐篷歇息 ​ 2.如果备用干粮充足,那么不需停留,继续前进 ​ -调用unpark,就好比令干粮充足 ​ 1.如果这时线程还在帐篷即在park状态,就唤醒让他继续前进。 ​ 2.如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进 ​ 3.因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

先调用unpark再调用park,会先补充1份干粮,然后在调用park的时候不会停留而是继续前进。

先调用park再调用unpark,会先停留,等调用unpark的时候补充1份干粮继续前进。

其实park/unpark的设计原理核心是“许可”:park是等待一个许可,unpark是为某线程提供一个许可。 如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。 ​ 有一点比较难理解的,是unpark操作可以再park操作之前。 也就是说,先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。 这其实是必须的。考虑最简单的生产者(Producer)消费者(Consumer)模型:Consumer需要消费一个资源,于是调用park操作等待;Producer则生产资源,然后调用unpark给予Consumer使用的许可。 非常有可能的一种情况是,Producer先生产,这时候Consumer可能还没有构造好(比如线程还没启动,或者还没切换到该线程)。那么等Consumer准备好要消费时,显然这时候资源已经生产好了,可以直接用,那么park操作当然可以直接运行下去。如果没有这个语义,那将非常难以操作。 ​ 但是这个“许可”是不能叠加的,“许可”是一次性的。 比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。

源码分析链接:https://www.jianshu.com/p/e3afe8ab8364

img

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0

img

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 唤醒 cond 条件变量中的 Thread0
  3. Thread_0 恢复运行
  4. 设置 _counter 为 0

img

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

多把锁

多把不相干的锁

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

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

解决方法是准备多个房间即多个对象锁。例如

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); } } } //执行 BigRoom bigRoom = new BigRoom(); new Thread(() -> { bigRoom. study (); } ,"小南").start(); new Thread(() -> { bigRoom.sleep(); } ,"小女").start();

改进以后,多个锁版本

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); } } } //执行 BigRoom bigRoom = new BigRoom(); new Thread(() -> { bigRoom. study(); } ,"小南").start(); new Thread(() -> { bigRoom.sleep(); } ,"小女").start();

将锁的粒度细分

好处,是可以增强并发度

坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

☆活跃性问题

包括死锁、活锁、饥饿

死锁:迎面开来的汽车A和汽车B过马路,汽车A得到了半条路的资源(满足死锁发生条件1:资源访问是排他性的,我占了路你就不能上来,除非你爬我头上去),汽车B占了汽车A的另外半条路的资源,A想过去必须请求另一半被B占用的道路(死锁发生条件2:必须整条车身的空间才能开过去,我已经占了一半,尼玛另一半的路被B占用了),B若想过去也必须等待A让路,A是辆兰博基尼,B是开奇瑞QQ的屌丝,A素质比较低开窗对B狂骂:快给老子让开,B很生气,你妈的,老子就不让(死锁发生条件3:在未使用完资源前,不能被其他线程剥夺),于是两者相互僵持一个都走不了(死锁发生条件4:环路等待条件),而且导致整条道上的后续车辆也走不了。 ​ 活锁:马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车A和B,A比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去。 ​ ​ 在“首堵”北京的某一天,天气阴沉,空气中充斥着雾霾和地沟油的味道,某个苦逼的临时工交警正在处理塞车,有两条道A和B上都堵满了车辆,其中A道堵的时间最长,B相对相对堵的时间较短,这时,前面道路已疏通,交警按照最佳分配原则,示意B道上车辆先过,B道路上过了一辆又一辆,A道上排队时间最长的确没法通过,只能等B道上没有车辆通过的时候再等交警发指令让A道依次通过,这也就是ReentrantLock显示锁里提供的不公平锁机制(当然了,ReentrantLock也提供了公平锁的机制,由用户根据具体的使用场景而决定到底使用哪种锁策略),非公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿。

活锁

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(); } }

死锁

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

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();

结果: 12:22:06.962 [t2] c.TestDeadLock - lock B 12:22:06.962 [t1] c.TestDeadLock - lock A

定位死锁

``` 定位死锁 1.检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id, 2.用 jstack 定位死锁. ​ 一、使用jps查看进程 cmd > jps Picked up JAVATOOLOPTIONS: -Dfile.encoding=UTF-8 12320 Jps 22816 KotlinCompileDaemon 33200 TestDeadLock // JVM 进程 11508 Main 28468 Launcher ​ 二、使用jstack 查看进程下的线程 cmd > jstack 33200 Picked up JAVATOOLOPTIONS: -Dfile.encoding=UTF-8 2018-12-29 05:51:40 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode): "DestroyJavaVM" #13 prio=5 osprio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Thread-1" #12 prio=5 osprio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object) - locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry [0x000000001f44f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object) - locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) // 略去部分输出

Found one Java-level deadlock:

"Thread-1": waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), which is held by "Thread-1"

Java stack information for the threads listed above:

"Thread-1": at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) - waiting to lock <0x000000076b5bf1c0> (a java.lang.Object) - locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0": at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) - waiting to lock <0x000000076b5bf1d0> (a java.lang.Object) - locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock. ```

哲学家就餐问题:死锁

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

他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。

吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。

如果筷子被身边的人拿着,自己就得等待

``` class Chopstick {    String name;    public Chopstick(String name) {    this.name = name;   }    @Override    public String toString() {    return "筷子{" + name + '}';   } } ​ class Philosopher extends Thread { Chopstick left; Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name); this.left = left; this.right = right; } private void eat() { log.debug("eating..."); Sleeper.sleep(1); } @Override    public void run() { while (true) { // 获得左手筷子 synchronized (left) { // 获得右手筷子 synchronized (right) { // 吃饭 eat(); } // 放下右手筷子 } // 放下左手筷子 } } } ​ ​ 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(); ​ 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... // 卡在这里, 不向下运行

名称: 阿基米德 状态: 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) ​ 分析发现: 阿基米德等待1,1被苏格拉底持有 苏格拉底等待2,2被柏拉图持有 柏拉图等待3,3被亚里士多德持有 亚里士多德等待4,4被赫拉克利特持有 赫拉克利特等待5,5被阿基米德持有 即: 苏格拉底持有1,等待2 柏拉图持有2,等待3 亚里士多德持有3,等待4 赫拉克利特持有4,等待5 阿基米德持有5,等待1 ​ 每个人持有一根筷子,即一把锁,5个人互相等待。这也是死锁的表现。 ```

死锁解决:顺序加锁

先来看看使用顺序加锁的方式解决之前的死锁问题

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 (A) { log.debug("lock A"); sleep(0.5); synchronized (B) { log.debug("lock B"); log.debug("操作..."); } } } , "t2"); t1.start(); t2.start();

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();

分析: ​ 苏格拉底持有1,等待2 ​ 柏拉图持有2,等待3 ​ 亚里士多德持有3,等待4 ​ 赫拉克利特持有4,等待5 ​ 阿基米德持有1,等待5 ​ 由于5没有人持有,赫拉克利特先吃上饭释放了 4、5线程得以执行,但是又有新的问题,即饥饿。

饥饿

synchronized是非公平锁,没有浪费线程唤醒阶段的时间,执行新调用的方法,增加吞吐量,缺点是可能会造成饥饿。

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

上面虽然解决了死锁的问题,但是存在某一个线程很长的一段时间里都得不到执行的问题。

可以使用ReentrantLock的公平锁解决。

使用非公平锁时可能会出现饥饿效应,这会导致某些线程一直不能获取到锁来进行后续任务的执行,这是生产环境不想看到的问题,如何解决?

饥饿效应产生的根本原因是线程在获取锁时在排队,而非公平锁使用了插队的方式来减少唤醒线程的CPU开销,插队导致后面的线程一直等待。这是饱效应的根本原因。

根治的方法是让等待时间过长的线程有重新获取锁的机会,可以给每一个等待的线程一个超时时间,超过某一时间后可以重新获取一次锁,线程在获取锁的过程中加一个带有超时时间、自旋间隔的自旋逻辑。

☆公平锁与非公平锁

前提: 线程等待时会被挂起,轮到他时会被唤醒。

公平锁:新进程发出请求,如果此时一个线程正持有锁,或有其他线程正在等待队列中等待这个锁,那么新的线程将被放入到队列中被挂起。相当于一堆嗜睡的低血糖病人排队看医生,进去的病人门一关,外面的人便排队候着打瞌睡,轮到他时再醒醒进去。

非公平锁: 新进程发出请求,如果此时一个线程正持有锁,新的线程将被放入到队列中被挂起,但如果发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。相当于排队看医生,进去的病人门一关,外面的人便排队候着打瞌睡,这时新人来了,碰巧门一开,外面的人还没完全醒来,他就乘机冲了进去。

☆ReentrantLock

相对于 synchronized,它具备如下特点

-可中断

-可以设置超时时间

-可以设置为公平锁

-支持多个条件变量

-与 synchronized 一样,都支持可重入

基本语法

// 获取锁 reentrantLock.lock(); try { // 临界区 } finally { // 释放锁 reentrantLock.unlock(); }

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

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(); } } ​ 输出: 17:59:11.862 [main] c.TestReentrant - execute method1 17:59:11.865 [main] c.TestReentrant - execute method2 17:59:11.865 [main] c.TestReentrant - execute method3

可打断

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 {    //睡眠1秒 sleep(1);    //中断t1获取锁的操作 t1.interrupt(); log.debug("执行打断"); } finally { lock.unlock(); }

18:02:40.520 [main] c.TestInterrupt - 获得了锁 18:02:40.524 [t1] c.TestInterrupt - 启动... 18:02:41.530 [main] c.TestInterrupt - 执行打断 java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17) at java.lang.Thread.run(Thread.java:748) 18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断

注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> {   log.debug("启动...");    //lock.lock();代替lock.lockInterruptibly();   lock.lock();    try {   log.debug("t1获得了锁");   } finally {   lock.unlock();   } }, "t1"); ​ lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(1); t1.interrupt(); log.debug("执行打断"); sleep(1); } finally { log.debug("释放了锁"); lock.unlock(); } ​ 输出: 18:06:56.261 [main] c.TestInterrupt - 获得了锁 18:06:56.265 [t1] c.TestInterrupt - 启动... 18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁 18:06:58.267 [main] c.TestInterrupt - 释放了锁 18:06:58.267 [t1] c.TestInterrupt - t1获得了锁

锁超时

ReentrantLock lock = new ReentrantLock(); 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(); try { sleep(2); } finally { lock.unlock(); } ​ 由于主线程先获取到锁,t1线程不到锁会立刻返回。     //输出: 18:15:02.918 [main] c.TestTimeout - 获得了锁 18:15:02.921 [t1] c.TestTimeout - 启动... 18:15:02.921 [t1] c.TestTimeout - 获取锁失败,立刻返回

超时失败

ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("启动..."); try { if (!lock.tryLock(1, TimeUnit.SECONDS)) { log.debug("获取等待 1s 后失败,返回"); return; } } catch (InterruptedException e) { e.printStackTrace(); } try { log.debug("获得了锁"); } finally { lock.unlock(); } } , "t1"); lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(2); } finally { lock.unlock(); }

18:19:40.537 [main] c.TestTimeout - 获得了锁 18:19:40.544 [t1] c.TestTimeout - 启动... 18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败,返回

公平锁

ReentrantLock 默认是不公平的锁,synchronize默认也是不公平的锁。

所谓的公平指得是按照进入阻塞队列的顺序先来先得获取锁。

Synchronize是在锁释放以后,阻塞队列的锁一拥而上去抢占锁。

```

//ReentrantLock lock = new ReentrantLock(); 此处默认是false //相当于 //ReentrantLock lock = new ReentrantLock(false); ​ ReentrantLock lock = new ReentrantLock(false); lock.lock(); for (int i = 0; i < 500; i++) { new Thread(() -> {        //这里不是 锁重入        //而是主线程创建了 500个线程        //这500个线程一直被阻塞在这里        //等待主线程释放锁 这500个线程才能执行 lock.lock(); try { System.out.println(Thread.currentThread().getName() + " running..."); } finally { lock.unlock(); } } , "t" + i).start(); } // 1s 之后去争抢锁 Thread.sleep(1000); new Thread(() -> { System.out.println(Thread.currentThread().getName() + " start...");    //同上 lock.lock(); try { System.out.println(Thread.currentThread().getName() + " running..."); } finally { lock.unlock(); } } , "强行插入").start(); lock.unlock(); ```

强行插入线程有机会在中间输出。即没有按照顺序,最后来的最后才能获取锁,而是最后来到的插队获取到了锁。注意:该实验不一定总能复现。

t39 running... t40 running... t41 running... t42 running... t43 running... 强行插入 start... 强行插入 running... t44 running... t45 running... t46 running... t47 running... t49 running...

改为公平锁后

ReentrantLock lock = new ReentrantLock(true); //强行插入,总是在最后输出,最后来的最后才能获取锁。 t465 running... t464 running... t477 running... t442 running... t468 running... t493 running... t482 running... t485 running... t481 running... //强行插入 running..

公平锁一般没有必要,会降低并发度,后面分析原理时会讲解

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待。

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比synchronized 是那些不满足条件的线程都在一间休息室等消息。

而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。

使用要点:

``` -await 前需要获得锁,这一点和synchronize一样

-await 执行后,会释放锁,进入 conditionObject 等待

-await 的线程被唤醒(或打断、或超时)后会重新竞争 lock 锁,竞争 lock 锁成功后,从 await 后继续执行 ```

static ReentrantLock lock = new ReentrantLock(); static Condition waitCigaretteQueue = lock.newCondition(); static Condition waitbreakfastQueue = lock.newCondition(); static volatile Boolean hasCigrette = false; static volatile Boolean hasBreakfast = false; public static void main(String[] args) { new Thread(() -> { try { lock.lock(); while (!hasCigrette) { try { waitCigaretteQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的烟"); } finally { lock.unlock(); } } ).start(); new Thread(() -> { try { lock.lock(); while (!hasBreakfast) { try { waitbreakfastQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的早餐"); } finally { lock.unlock(); } } ).start(); sleep(1); sendBreakfast(); sleep(1); sendCigarette(); } private static void sendCigarette() { lock.lock(); try { log.debug("送烟来了"); hasCigrette = true; waitCigaretteQueue.signal(); } finally { lock.unlock(); } } private static void sendBreakfast() { lock.lock(); try { log.debug("送早餐来了"); hasBreakfast = true; waitbreakfastQueue.signal(); } finally { lock.unlock(); } }

18:52:27.680 [main] c.TestCondition - 送早餐来了 18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐 18:52:28.683 [main] c.TestCondition - 送烟来了 18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟

对比synchronized:

//1.工作线程相当于早餐 synchronized(lock) { while(早餐条件不成立) { lock.wait(); } // 干活 } //2.工作线程相当于烟 synchronized(lock) { while(烟条件不成立) { lock.wait(); } // 干活 } //3.条件变量线程: synchronized(lock) { 修改条件 lock.notifyAll(); }

tryLock解决哲学家就餐问题

class Chopstick extends ReentrantLock { String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } }

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) { // 尝试获得左手筷子 if (left.tryLock()) { try { // 尝试获得右手筷子 if (right.tryLock()) { try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } } private void eat() { log.debug("eating..."); Sleeper.sleep(1); } }

``` 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();

饥饿的根源在于 synchronized 获取左手筷子,一直获取不到右手筷子。trylock获取到左手筷子,获取不到右手筷子就会释放左手筷子。

如果将Chopstick c1 = new Chopstick(true);//改为公平锁解决问题

使用非公平锁时可能会出现饥饿效应,这会导致某些线程一直不能获取到锁来进行后续任务的执行,这是生产环境不想看到的问题,如何解决?

饥饿效应产生的根本原因是线程在获取锁时在排队,而非公平锁使用了插队的方式来减少唤醒线程的CPU开销,插队导致后面的线程一直等待。这是饱效应的根本原因,根治的方法是让等待时间过长的线程有重新获取锁的机会,可以给每一个等待的线程一个超时时间,超过某一时间后可以重新获取一次锁,线程在获取锁的过程中加一个带有超时时间、自旋间隔的自旋逻辑。 ———————————————— 版权声明:本文为CSDN博主「码农杰森」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/bklydxz/article/details/117903146 ```

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值