锁的活跃性
1.死锁
1.1介绍
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。
t1线程获得A对象锁,接下来想获取B对象的锁, t2线程获得B对象锁,接下来想获取A对象的锁。
1.2死锁代码
static final Object A = new Object();
static final Object B = new Object();
@Test
public void testDeadLock() {
//线程1获得A锁,又想获得B锁
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("获得A锁...");
synchronized (B) {
log.debug("获得B锁...");
}
}
}, "t1");
t1.start();
//线程2获得B锁,又想获得A锁
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("获得B锁...");
synchronized (A) {
log.debug("获得A锁...");
}
}
}, "t2");
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
1.3死锁定位
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
方法1:使用jps,jstack
方法2:使用jconsole
死锁总结
- 避免死锁要注意加锁顺序
- 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
经典死锁问题-哲学家进餐问题
有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
如果筷子被身边的人拿着,自己就得等待。
package com.concurrent.p4;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
/**
* 哲学家就餐问题
*/
public class TestQuestionPhilosopher {
@Test
public void test1() throws InterruptedException {
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();
while (true) ;
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
//左筷子
private Chopstick left;
//右筷子
private 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();
}
}
}
}
public void eat() {
log.debug("eat...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 筷子类
*/
class Chopstick {
private String name;
public Chopstick(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
jconsole分析:
-------------------------------------------------------------------------
名称: 阿基米德
状态: 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)
2.活锁
活锁出现在两个线程虽然谁都没占用对方的锁,但是互相改变对方的结束条件,最后谁也无法结束。
static volatile int count = 10;
static final Object lock = new Object();
@Test
public void t1() {
//期望减到0退出循环
new Thread(() -> {
while (count > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.debug("count={}", count);
}
}, "t1").start();
//期望加到20退出循环
new Thread(() -> {
while (count < 20) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.debug("count={}", count);
}
}, "t2").start();
while (true) ;
}
解决活锁:
设置不同的间隔时间,增加随机睡眠时间。
3.饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题。
可以采用顺序加锁的方式解决死锁,
但是采取顺序加锁的方式可能会导致线程饥饿,
@Test
public void test1() throws InterruptedException {
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();
while (true) ;
}