线程活跃性问题
死锁
死锁是指两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。下面一段代码就是一个死锁,t1先获得了A的锁,t2先获得了B的锁,随后,t1想要获得B的锁,t2想要获得A的锁,synchronized 修饰的代码块由于获得不到锁,会导致程序一直卡在互相等待双方等待锁的释放,从而导致程序无法正常运行下去。
@Slf4j(topic = "c.DealLock")
public class DealLock {
static Object A = new Object();
static Object B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
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) {
e.printStackTrace();
}
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
运行结果:
可以看到程序一直卡死再互相等待对方线程释放自己想要的锁,下面是示意图:
我们可以使用jpd查看Java进程,如:
然后我们可以使用“jstack 进程号”来查看是否具有死锁,如:
活锁
所谓活锁,就是两个以上的线程在执行的时候,因为相互谦让资源,结果都拿不到资源,没法运行程序。或者说,活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如下面的代码:
@Slf4j(topic = "c.LiveLock")
public class LiveLock {
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(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
其中一个线程count++,另一个线程count—,从结果可以看到一直在很长一段时间都在13左右徘徊,导致程序因为他们的“谦让”而一直运行。当然,死锁出现的很少,即使出现了,大部分活锁都会被系统自动解开,只是需要耗费一些时间,就好比上面的例子,在13徘徊了很久,但是还是去到了16。当然,等待系统自动解开活锁是一种消极的做法,因此我们写程序尽量避免活锁,比如上面的例子,两个线程都是“Thread.sleep(200)”,我们只需让两个线程sleep不同的时间就不会产生活锁了。
饥饿
如果一个线程的cpu执行时间都被其他线程抢占了,导致得不到cpu执行,这种情况就叫做“饥饿”,这个线程就会出现饥饿致死的现象,因为永远无法得到cpu的执行。解决饥饿现象的方法就是实现公平,保证所有线程都公平的获得执行的机会。
ReentrantLock介绍
简介
ReentrantLock重入锁,是实现Lock接口的一个类,是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。相比于synchronized关键字,ReentrantLock 具有 synchronized 所拥有的全部功能。此外,还有更多比 synchronized 关键字强大的功能,更加灵活,更加适合复杂的并发场景。其优势主要体现在以下几个方面:
- 可通过 Condition 类实现多路通知功能,即支持多个条件变量(可以理解为多个 WaitSet)
- 提供多个便利的方法,如判断是否有线程在排队等待锁
- 可设置所为公平锁或非公平锁
- 可以响应中断请求
- 带超时的获取锁尝试
特点介绍
可重入性
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁;但是如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {
/**
* 创建一个 ReentrantLock 对象
*/
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();
}
}
}
上面代码中,main线程一共获取了3次lock,可以证明 ReentrantLock 是可重入的。
中断性
synchronized 关键字的锁是不能中断的,如果线程获取不到锁,就会一直等待,不具有中断处理的能力,而 ReentrantLock 则具有一直中断机制。但是 ReentrantLock 的 lock() 方法不具有打断能力,需要调用 lockInterruptibly() 。lockInterruptibly()方法能够中断等待获取锁的线程。当两个线程同时获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。举个例子:
@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {
/**
* 创建一个 ReentrantLock 对象
*/
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
//main 线程先获得lock
lock.lock();
log.debug("main获得了锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
log.debug("执行打断");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//main释lock
lock.unlock();
}
}
}
锁超时
ReentrantLock 类提供了一个尝试获取锁的方法“isTry()”方法,如果获取锁成功则返回true,否则返回false。如果该方法不带参数,则是立刻返回结果,如果带参数,则是在指定时间内获取锁,如果过了指定时间还未获取,就返回false,或期间获取到了就返回 true 。测试例子:
@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {
/**
* 创建一个 ReentrantLock 对象
*/
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
//尝试获取锁,如果无法获取,立刻返回tryLock方法的结果
if (!lock.tryLock()) {
log.debug("获取立刻失败,返回");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
测试结果:
下面我们看一个在指定时间内尝试获取锁的,修改代码为:
@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {
/**
* 创建一个 ReentrantLock 对象
*/
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("启动...");
//尝试获取锁,如果无法获取,立刻返回tryLock方法的结果
try {
if (!lock.tryLock(3, TimeUnit.SECONDS)) {
log.debug("3s内获取失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
主线程需要持有锁5s,因此t1在3s不会获得锁,所以3s后返回false,然后只需if语句里面的代码。
公平锁
ReentrantLock 锁支持公平锁,所谓公平锁就是先进入等待队列的线程会优先被执行。但是ReentrantLock 默认的锁还是非公平锁,其实公平锁一般没有必要使用,会降低并发度,后面分析原理时会讲解。如果要设置为公平锁,只需要修改其中的一个属性即可,我们在构造锁对象是参数填写为true即可,如:“ReentrantLock lock = new ReentrantLock(true);”我们先来验证默认的锁为非公平锁,如下代码:
@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {
/**
* 创建一个 ReentrantLock 对象
*/
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
lock.lock();
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "可恶的插队线程").start();
lock.unlock();
}
}
运行结果:
可以看到“可恶的插队线程”插队成功了,因此证明了默认是非公平锁。然后我们修改代码,验证公平锁:
static ReentrantLock lock = new ReentrantLock(true);
条件变量
在学习 synchronized 关键字的实现原理时,知道了它只有一个 WaitSet ,在 wait-notify 机制的学习中的“小南和小女分别等待烟和面包”的例子中可以验证。但是 ReentrantLock 却可以创建多个条件变量(即等待室),也就是多个 Condition 对象。
再来根据那个例子来说明:
@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {
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) throws InterruptedException {
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();
Thread.sleep(1000);
sendBreakfast();
Thread.sleep(1000);
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();
}
}
}
不难看出,signal 就相当于 notify,同样的 signalAll 就相当于 notifyAll ;而 await 就相当于 wait,同样,await也可以具有时间限制的等待。运行结果如下: