摆烂了两天重新启程
Park和Unpark
与 Object 的 wait & notify 相比wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
park & unpark 可以先 unpark,而 wait & notify 不能先 notify,小例子如下
@Slf4j(topic = "c.TestUnpark")
public class TestUnpark {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
log.debug("start...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
Park和Unpark的原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex
打个比喻 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
调用 park 就是要看需不需要停下来歇息
如果备用干粮耗尽,那么钻进帐篷歇息
如果备用干粮充足,那么不需停留,继续前进
调用 unpark,就好比令干粮充足
如果这时线程还在帐篷,就唤醒让他继续前进
如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
1. 当前线程调用 Unsafe.park() 方法
2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
3. 线程进入 _cond 条件变量阻塞
4. 设置 _counter = 0
1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 唤醒 _cond 条件变量中的 Thread_0
3. Thread_0 恢复运行
4. 设置 _counter 为 0
1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 当前线程调用 Unsafe.park() 方法
3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
4. 设置 _counter 为 0
线程状态转换(加星)
1.NEW->RUNNABLE
调用t.start()方法
2.RUNNABLE<-->WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 WAITING --> RUNNABLE
竞争锁失败,t 线程从 WAITING --> BLOCKED
3.RUNNABLE <--> WAITING
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
注意是当前线程在t 线程对象的监视器上等待
t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
4.RUNNABLE <--> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->
RUNNABLE
5.RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
6.RUNNABLE <--> TIMED_WAITING
当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
注意是当前线程在t 线程对象的监视器上等待
当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE
7.RUNNABLE <--> TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
8.RUNNABLE <--> TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE
9.RUNNABLE <--> BLOCKED
t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
10.RUNNABLE <--> TERMINATED
当前线程所有代码运行完毕,进入TERMINATED
多把锁
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁),下面是例子,可以看到二者互不干扰
@Slf4j(topic = "c.Test18")
public class Test18 {
public static void main(String[] args) {
BigRoom bigRoom=new BigRoom();
new Thread(()->{
try {
bigRoom.study();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"小南").start();
new Thread(()->{
try {
bigRoom.sleep();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"小女").start();
}
}
@Slf4j(topic = "c.BigRoom")
class BigRoom{
private final Object studyRoom=new Object();
private final Object bedRoom=new Object();
public void sleep() throws InterruptedException {
synchronized (bedRoom){
log.debug("睡觉2小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (studyRoom){
log.debug("学习1小时");
Thread.sleep(1000);
}
}
}
21:27:04 [小女] c.BigRoom - 睡觉2小时
21:27:04 [小南] c.BigRoom - 学习1小时
将锁的粒度细分
好处,是可以增强并发度
坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
线程的活跃性
死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得 A对象 锁,接下来想获取 B对象的锁
t2 线程 获得 B对象 锁,接下来想获取 A对象的锁 例:
@Slf4j(topic = "c.Test19")
public class Test19 {
public static void main(String[] args) {
test1();
}
public static void test1(){
Object A=new Object();
Object B=new Object();
Thread t1=new Thread(()->{
synchronized (A){
log.debug("lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B){
log.debug("lock B");
log.debug("操作");
}
}
},"t1");
Thread t2=new Thread(()->{
synchronized (B){
log.debug("lock B");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A){
log.debug("lock A");
log.debug("操作...");
}
}
},"t2");
t1.start();
t2.start();
}
}
定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
如果筷子被身边的人拿着,自己就得等待
例子:
@Slf4j(topic = "c.PhilosophersEat")
public class PhilosophersEat {
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();
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
@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;
}
private void eat() throws InterruptedException {
log.debug("eating...");
Thread.sleep(1000);
}
@Override
public void run() {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
try {
eat();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如,一个有点意思的事情,就是这两个线程的睡眠时间如果不一样就不会发生活锁
@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) {
try {
Thread.sleep(400);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
try {
Thread.sleep(400);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
饥饿
先来看看使用顺序加锁的方式解决之前的死锁问题
顺序加锁的解决方案,都按先获取A再获取B的顺序去获取锁,不过这就可能出现饥饿的问题
这个方法也可以简单解决一下刚才的哲学家就餐问题
@Slf4j(topic = "c.PhilosophersEat")
public class PhilosophersEat {
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();
new Philosopher("阿基米德", c1, c5).start();
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
@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;
}
private void eat() throws InterruptedException {
log.debug("eating...");
Thread.sleep(1000);
}
@Override
public void run() {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
try {
eat();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 放下右手筷子
}
// 放下左手筷子
}
}
}
按顺序拿他就不会死锁,但是结果还是有问题的,看这里
21:59:27 [亚里士多德] c.Philosopher - eating...
21:59:28 [亚里士多德] c.Philosopher - eating...
21:59:29 [赫拉克利特] c.Philosopher - eating...
21:59:30 [亚里士多德] c.Philosopher - eating...
21:59:31 [赫拉克利特] c.Philosopher - eating...
21:59:32 [赫拉克利特] c.Philosopher - eating...
21:59:33 [赫拉克利特] c.Philosopher - eating...
21:59:34 [赫拉克利特] c.Philosopher - eating...
21:59:35 [赫拉克利特] c.Philosopher - eating...
21:59:37 [赫拉克利特] c.Philosopher - eating...
21:59:38 [赫拉克利特] c.Philosopher - eating...
21:59:39 [赫拉克利特] c.Philosopher - eating...
21:59:40 [赫拉克利特] c.Philosopher - eating...
21:59:41 [赫拉克利特] c.Philosopher - eating...
21:59:42 [赫拉克利特] c.Philosopher - eating...