死锁/活锁/饥饿/ReentrantLock锁
多把锁
在有些场景中,我们需要写一下双重锁的操作:
synchronized(objA){
synchronized(objB){
...
}
}
简单理解就是一种尝试性的操作:拥有一把锁了,再去拿一把锁。
死锁
在并发中,多把锁的情况下,容易产生死锁,例如:各自拥有一把锁的情况下,还需要拿别人正在使用的锁,而别人也在等着你释放锁。
小示例:
public class Demo {
static final Object left = new Object();
static final Object right = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(()->{
// 锁住left
synchronized (left){
// 睡一会
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 拿另外一个锁
synchronized (right){
System.out.println("SUCCESS");
}
}
});
Thread thread2 = new Thread(()->{
// 锁住right
synchronized (right){
// 睡一会
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 拿另外一个锁
synchronized (left){
System.out.println("SUCCESS");
}
}
});
thread1.start();
thread2.start();
}
}
定位死锁的方法
- jconsole工具;(在搜索中搜索jconsole即可)
- 使用jps定位进程id,再用jstack定位死锁。(如果有死锁会提示:
Found one Java-level deadlock
)
使用方法一,检测之前代码中的死锁:
哲学家就餐问题
问题描述:有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会后就吃饭,吃完饭就思考;
- 吃饭需要以两根筷子吃饭,桌上一共五根筷子;
- 如果筷子被身边的人拥有,自己就需要等待。
死锁产生情况
每个哲学都同时占有一把筷子,还需要等别人释放筷子,则发生死锁。
活锁
线程在某些条件下不断尝试重新执行某个操作,但由于条件没有改变或其他原因导致操作不成功,线程无法取得进展。与死锁不同,线程在活锁中并不被阻塞,它们可以继续执行,但是执行却是一种循环无法终止。
饥饿
部分线程一直拿不到锁(可能是优先级太低或者是同步加锁一直取不到锁),得不到CPU去调度,也无法结束该线程,称之为饥饿。
以上的问题可以使用ReentrantLock锁解决。
ReentrantLock锁
对比synchronized:
- 可中断(避免死锁,被动的方式)
- 可以设置抢锁的超时时间(主动的方式)
- 可以设置为公平锁
- 支持多个条件变量(根据条件可进入不同的
waitSet
中,而synchronized只有一个)- 都支持可重入(反复对同一个对象加锁)
语法:
private static ReentrantLock reentrantLock = new ReentrantLock();
// 加锁
reentrantLock.lock();
try{
// ...临界区资源操作
} finally{
// 释放锁
reentrantLock.unlock();
}
可以被打断
- 加锁的时候使用:
reentrantLock.lockInterruptibly();
设置可打断锁 - 其它线程调用:
thread.interrupt();
打断thread
的等待
public class Demo {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
//reentrantLock.lock(); // 无法被打断
// 如果没有竞争那么此方法就会获取 lock 对象锁
// 如果有竞争就进入阻塞队列,可以被其它线程用interrupt方法打断
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("没有获得锁");
return; // 没有获得锁
}
try {
System.out.println("获取到锁");
} finally {
reentrantLock.unlock();
}
}, "thread1");
reentrantLock.lock(); // 主线程先锁起来
thread.start(); // 此时其它线程无法获得锁,进入阻塞队列,但是lockInterruptibly是可以被打断的
TimeUnit.SECONDS.sleep(1);
thread.interrupt(); // 打断
}
}
锁超时
同时也支持其它线程被动打断
- 尝试获取锁:
reentrantLock.tryLock()
,获取不到锁立刻结束等待- 返回值:true 获得到锁
- 返回值:false 没有获得到锁
根据返回值可以再做逻辑判断,比如说:return;
让其直接返回。
- 带参数的:
reetrantLock.tryLock(long,TimeUnit)
,尝试获取锁(在long,单位TimeUnit中)
公平锁
synchronized
是一种不公平的锁,当其它线程抢不到锁时会进入waitSet
中,锁释放时,不会根据进入waitSet
的先后而拿锁,是一种随机抢锁的过程,故称为:不公平锁。
ReentrantLock 默认也是不公平锁
构造方法中,传入一个布尔值,改变其公平性。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
按先入先得的方式获得锁(会降低并发度,影响性能)。
条件变量
形象的比喻:ReentrantLock拥有多个休息室。
// 创建一个休息室(waitSet)
Condition condition = reentrantLock.newCondition();
// 必须先获得锁
reentrantLock.lock();
// 进入休息室
condition.await(); // 会释放锁
其它线程唤醒:
// 先获得锁
// 对标 notify
condition.signal();
// 对标 notifyAll
confotion.signalAll();
使用方式
使用synchronized
时,通常是对一个普通对象进行加锁,而使用ReentrantLock锁时,可以让普通对象继承ReentrantLock类。
应用案例:
- 解决哲学家就餐问题
转载自:https://juejin.cn/post/7238917665863139387