☆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…
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _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 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被柏拉图持有
柏拉图等待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