8.活跃度
死锁发生的条件
定位死锁的方法
死锁的典型问题–哲学家问题
我们检测下死锁
就比如,我们把随后一个哲学家阿基米德拿筷子的顺序更改,就不会发生死锁。
变成:
new Philosopher("阿基米德", c5, c1).start();
但是这种解锁方法会引起饥饿
。使得阿基米德哲学家很难吃饭。
更好的解决办法后面有叙述。。。
活锁
活锁出现在两个线程互相改变对方的结束条件,后谁也无法结束。
避免活锁的方法
在线程执行时,中途给予不同的间隔时间即可。
死锁与活锁的区别
- 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。
- 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。
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();
}
}
饥饿
某些线程因为优先级太低,导致一直无法获得资源的现象。,在使用顺序加锁时,可能会出现饥饿现象。如上面我说的更改阿基米德拿筷子的顺序。
原来的死锁问题分析:
顺序加锁解决方案
9.ReentrantLock
对于上述的问题,使用ReentrantLock
类会有更好的解决办法。
下面我们来详细说说它的方法。
基本语法:
//获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
try {
//需要执行的代码
}finally {
//释放锁
lock.unlock();
}
ReentrantLock的特点
ReentrantLock和synchronized相比具有的的特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁 (先到先得)
- 支持多个条件变量( 具有多个waitset)
相同点:可重入
可重入
- 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
- 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
假如不具有可重入性,在method2
时就没办法获得锁。
可中断-lock.lockInterruptibly()
如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败。
简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(()-> {
try {
//加锁,可打断锁
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
//被打断,返回,不再向下执行
return;
}finally {
//释放锁
lock.unlock();
}
});
lock.lock();
try {
t1.start();
Thread.sleep(1000);
//打断
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
可以设置超时时间(锁超时)-lock.tryLock
使用lock.tryLock方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。
并且tryLock方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中timeout为最长等待时间,TimeUnit为时间单位
简而言之就是:获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行
不设置等待时间
使用方法
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(()-> {
//未设置等待时间,一旦获取失败,直接返回false
if(!lock.tryLock()) {
System.out.println("获取失败");
//获取失败,不再向下执行,返回
return;
}
System.out.println("得到了锁");
lock.unlock();
});
lock.lock();
try{
t1.start();
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
设置等待时间
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(()-> {
try {
//判断获取锁是否成功,最多等待1秒
if(!lock.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("获取失败");
//获取失败,不再向下执行,直接返回
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
//被打断,不再向下执行,直接返回
return;
}
System.out.println("得到了锁");
//释放锁
lock.unlock();
});
lock.lock();
try{
t1.start();
//打断等待
t1.interrupt();
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
使用 tryLock 解决哲学家就餐问题
可以设置为公平锁
在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。
//默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);
这样就是先进入entryList的线程等锁释放就先运行。
公平锁一般没有必要,会降低并发度,后面分析原理时会讲解。
测试
支持多个条件变量(多个等待区)
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执
有了多个等待区,我们就可以避免虚假唤醒,从而简化while-wait:
我们回到很久之前的外卖和烟的问题
使用了两个等待区,就不用担心错误的唤醒了(虚假唤醒)。
10.同步模式之顺序控制
我们在工作中有可能遇到这样一种情况,线程一要完成1操作,线程二要完成 2操作,但是规定2 操作必须在 1操作之前。
我们要如何保证呢?
wait notify 解决
// 用来同步的对象
static Object obj = new Object();
// t2 运行标记, 代表 t2 是否执行过
static boolean t2runed = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
// 如果 t2 没有执行过
while (!t2runed) {
try {
// t1 先等一会
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}}}
System.out.println(1);
});
Thread t2 = new Thread(() -> {
System.out.println(2);
synchronized (obj) {
// 修改运行标记
t2runed = true;
// 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
obj.notifyAll();
}
});
t1.start();
t2.start();
}
Park Unpark解决
可以看到,实现上很麻烦:
- 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
- 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
- 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个
我们使用park来解决
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
LockSupport.park();
System.out.println("1");
});
Thread t2 = new Thread(() -> {
System.out.println("2");
// 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
LockSupport.unpark(t1);
});
t1.start();
t2.start();
注意,不要用join
因为先唤醒的如果是2线程,就起不到作用。
11.交替输出问题
在生产过程中,我们也可能遇到这样的问题,三个线程,按顺序依次进行操作,我们必须保证操作顺序符合,同时也要保证执行了一定次数。
wait notify解决
public class Test4 {
static Symbol symbol = new Symbol();
public static void main(String[] args) {
new Thread(()->{
symbol.run("a", 1, 2);
}).start();
new Thread(()->{
symbol.run("b", 2, 3);
}).start();
new Thread(()->{
symbol.run("c", 3, 1);
}).start();
}
}
class Symbol {
public synchronized void run(String str, int flag, int nextFlag) {
for(int i=0; i<loopNumber; i++) {
while(flag != this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(str);
//设置下一个运行的线程标记
this.flag = nextFlag;
//唤醒所有线程
this.notifyAll();
}
}
/**
* 线程的执行标记, 1->a 2->b 3->c
*/
private int flag = 1;
private int loopNumber = 5;
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
public int getLoopNumber() {
return loopNumber;
}
public void setLoopNumber(int loopNumber) {
this.loopNumber = loopNumber;
}
}
await/signal解决
public class Test5 {
static AwaitSignal awaitSignal = new AwaitSignal();
static Condition conditionA = awaitSignal.newCondition();
static Condition conditionB = awaitSignal.newCondition();
static Condition conditionC = awaitSignal.newCondition();
public static void main(String[] args) {
new Thread(()->{
awaitSignal.run("a", conditionA, conditionB);
}).start();
new Thread(()->{
awaitSignal.run("b", conditionB, conditionC);
}).start();
new Thread(()->{
awaitSignal.run("c", conditionC, conditionA);
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
//唤醒一个等待的线程
conditionA.signal();
}finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock{
public void run(String str, Condition thisCondition, Condition nextCondition) {
for(int i=0; i<loopNumber; i++) {
lock();
try {
//全部进入等待状态
thisCondition.await();
System.out.print(str);
nextCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
private int loopNumber=5;
public int getLoopNumber() {
return loopNumber;
}
public void setLoopNumber(int loopNumber) {
this.loopNumber = loopNumber;
}
}
Park Unpark解决
class SyncPark {
private int loopNumber;
private Thread[] threads;
public SyncPark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void setThreads(Thread... threads) {
this.threads = threads;
}
public void print(String str) {
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.print(str);
LockSupport.unpark(nextThread());
}
}
private Thread nextThread() {
Thread current = Thread.currentThread();
int index = 0;
for (int i = 0; i < threads.length; i++) {
if(threads[i] == current) {
index = i;
break;
}
}
if(index < threads.length - 1) {
return threads[index+1];
} else {
return threads[0];
}
}
public void start() {
for (Thread thread : threads) {
thread.start();
}
LockSupport.unpark(threads[0]);
}
}
SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {
syncPark.print("a");
});
Thread t2 = new Thread(() -> {
syncPark.print("b");
});
Thread t3 = new Thread(() -> {
syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();