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

357 阅读19分钟

☆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,则进入等待状态。

源码分析链接:www.jianshu.com/p/e3afe8ab8…

img

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

img

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 唤醒 _cond 条件变量中的 Thread_0
  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:环路等待条件),而且导致整条道上的后续车辆也走不了。
​
活锁:马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车ABA比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去。
​
​
在“首堵”北京的某一天,天气阴沉,空气中充斥着雾霾和地沟油的味道,某个苦逼的临时工交警正在处理塞车,有两条道AB上都堵满了车辆,其中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 JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
12320 Jps
22816 KotlinCompileDaemon
33200 TestDeadLock // JVM 进程
11508 Main
28468 Launcher
​
二使用jstack 查看进程下的线程
cmd > jstack 33200
Picked up JAVA_TOOL_OPTIONS: -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 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition
[0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=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被柏拉图持有
柏拉图等待33被亚里士多德持有
亚里士多德等待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