文章目录
- 死锁(Deadlock)
- 通过 Visualvm 等工具排查死锁
- 活锁
- park & unpark
- 与 wait & notify 的区别
- park & unpark 实现:点外卖
- Lock 对象
- ReentrantLock 可重入锁
- 可重入
- lockInterruptibly 方法上锁(可打断)
- tryLock方法获取锁(锁超时)
- 公平锁
- 条件变量 Condition
死锁(Deadlock)
两个或多个线程互相等待对方释放资源,从而导致它们永远无法继续执行。死锁通常涉及多个锁,线程之间在等待对方释放锁时都会被阻塞
比如:t1线程已经获得A锁,进而请求B锁,t2线程已经获得B锁,进而请求A锁。导致两个线程永久阻塞。
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DeadlockTest {
public static void main(String[] args) {
final Object A = new Object();
final Object B = new Object();
new Thread(()->{
synchronized (A){
log.debug("成功获取 A 锁准备获取 B 锁");
synchronized (B){
log.debug("执行成功");
}
}
},"t1").start();
new Thread(()->{
synchronized (B){
log.debug("成功获取 B 锁准备获取 A 锁");
synchronized (A){
log.debug("执行成功");
}
}
},"t2").start();
}
}
通过 Visualvm 等工具排查死锁
可使用顺序加锁的方式来解决此死锁问题(不要交错获取锁,不然容易形成死锁)。
注:在 wait 或 join 方法上无限等待的线程,既不是死锁也不是活锁,因为我们可以通过其他线程调用 interrupt 方法来打断此无限等待的情况。如下面的示例:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class WaitTest {
private static Thread t1,t2;
public static void main(String[] args) {
Object obj = new Object();
// 线程t1和t2的结束条件都是等对方先执行
t1 = new Thread(() -> {
synchronized (obj) {
try {
log.debug("t1 准备让 t2 执行");
t2.join();
log.debug("t1 执行完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t1");
t2 = new Thread(() -> {
synchronized (obj) {
try {
log.debug("t2 准备让 t1 执行");
t1.join();
log.debug("t2 执行完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t2");
t1.start();
t2.start();
// t1.interrupt(); // 调用 interrupt 方法可打断
}
}
活锁
线程的状态不断的被改变(自身或其他线程修改),导致该线程一直在执行且无法结束。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class AliveLockTest {
private static Integer count = 100;
public static void main(String[] args) {
// count 大于 0 执行线程 t1,并 count--
// count 小于 50 执行线程 t2,并 count++
new Thread(()->{
synchronized (count) {
while (count > 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count--;
}
}
log.debug("线程 t1 执行结束");
},"t1").start();
new Thread(()->{
synchronized (count) {
while (count < 50) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
}
}
log.debug("线程 t2 执行结束");
},"t2").start();
}
}
死锁和活锁都会导致线程无法停止,但死锁是程序被阻塞不执行,活锁是程序一直执行不结束。
活锁解决通常让其中一个线程先执行完,则另一个线程就能执行完了。
注:此示例如果不加 sleep 有很大概率可以执行结束,但其一定会经历大量的循环执行
- 饥饿(Starvation)
某些线程一直无法获得所需的资源(由于资源抢占的不公平性),导致一直无法执行。
解决线程饥饿问题通常采用公平锁的方式来解决。
park & unpark
- LockSupport.park():在某一个线程中调用,表示暂停当前线程。
- LockSupport.unpark(Thread thread):恢复已暂停的线程
park & unpark 方法底层都是使用的 sun.misc.Unsafe UNSAFE 对象,对应为 native 方法。
与 wait & notify 的区别
- wait & notify 必须与 synchronized 关键字一起使用,park & unpark 没有这个限制(这也导致相应的代码块是没有同步的)
- notify 唤醒的线程是随机的,而 unpark 可以指定要唤醒的线程
- unpark 可以在 park 之前调用,且会生效,后调用的 park 方法也会被取消暂停。而 notify 不能先于 wait 方法调用。
park & unpark 实现:点外卖
import java.util.concurrent.locks.LockSupport;
/**
* 点外卖:
* 线程1:商家
* 线程2:买家
* 线程3:骑手
* 使用 park & unpark 实现
*/
public class ParkUnparkTest {
/**
* 0:未点餐
* 1:已点餐,未制作
* 2:制作完成,骑手未送货
* 3:骑手送货成功,可以开始干饭了
*/
private static int state = 0;
private static Thread t1,t2,t3;
public static void main(String[] args) {
// 商家
t1 = new Thread(()->{
while (state < 1){// 没人点餐就等待(需要循环等待,使用 if 会导致虚假唤醒问题(notifyAll 同时唤醒了骑手和买家,但是买家抢到了锁))
System.out.println(Thread.currentThread().getName()+":没人点餐,等待中");
LockSupport.park();
}
// 有人点餐就制作
System.out.println(Thread.currentThread().getName()+":已接单,制作中");
try {
// 模拟商家制作时间
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+":制作成功");
state = 2;
LockSupport.unpark(t3);// 通知骑手接单(这里可以精确指定骑手线程)
},"商家");
// 买家
t2 = new Thread(()->{
long start = System.currentTimeMillis();
if(state == 0){
state = 1; // 下单
System.out.println(Thread.currentThread().getName()+":下单成功,等待送餐");
LockSupport.unpark(t1);// 通知卖家线程制作
}
LockSupport.park();// 等待送餐
while (state < 3){// 没有送到,就等待
System.out.println(Thread.currentThread().getName()+":外卖没送到,等待中");
LockSupport.park();// 等待送餐
}
// 送到了,就开始干饭
System.out.println(Thread.currentThread().getName()+":外卖送到了,开始干饭");
// 买家结束(无需再次唤醒商家和卖家)
System.out.println("下单到就餐耗时:" + (System.currentTimeMillis() - start));
},"买家");
// 骑手
t3 = new Thread(()->{
while (state < 1){// 没人点餐,等待
System.out.println(Thread.currentThread().getName()+":没人点餐,等待中");
LockSupport.park();// 没人点餐等待
}
while (state < 2){// 没有制作完成,等待
System.out.println(Thread.currentThread().getName()+":没有制作完成,等待中");
LockSupport.park();// 没有制作完成,等待
}
// 制作完成了开始送货
System.out.println(Thread.currentThread().getName()+":接到外卖,送货中");
try {
// 模拟骑手送货
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+":送货完成");
state = 3;
LockSupport.unpark(t2);// 通知买家收货
},"骑手");
t1.start();
t2.start();
t3.start();
}
}
可能的一个结果:
商家:没人点餐,等待中
买家:下单成功,等待送餐 <---- 1
骑手:没人点餐,等待中 <----- 2
商家:已接单,制作中
商家:制作成功
骑手:接到外卖,送货中
骑手:送货完成
买家:外卖送到了,开始干饭
下单到就餐耗时:20021
这里 1 和 2 的打印顺序出现了问题,因为 park & unpark 只是暂停&取消暂停,并没有代码同步所以会有此情况出现。
Lock 对象
java.util.concurrent.locks.Lock 是一个类似于synchronized 块的线程同步机制。但是 Lock比 synchronized 块更加灵活。Lock是个接口,它有多种实现类。其使用方式如下:
Lock lock = ...;// Lock的实现类
lock.lock();// 加锁
try {
// 访问受此锁保护的资源
} finally {
lock.unlock();//需要手动调用解锁(解锁操作需要在 finally 块中调用)
}
主要方法
/**
* 加锁(不可打断)
* 如果当前无法获得锁,则阻塞,直到获取到锁且不可被打断(容易死锁)
*/
void lock();
/**
* 加锁(可打断)
* 如果当前无法获得锁,则阻塞,直到获取到锁或者被打断
*
* @throws InterruptedException 被打断时抛出异常
*/
void lockInterruptibly() throws InterruptedException;
/**
* 直接尝试获得锁,如果成功返回 true,如果不成功返回 false
* 其写法如下:
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // 受保护的代码
* } finally {
* // 解锁
* lock.unlock();
* }
* } else {
* // 未获得锁的时候的操作,这里没有获得锁,不用解锁
* }
*
* @return 获得锁返回true,未获得锁返回false
*/
boolean tryLock();
/**
* 尝试获取锁,但其会等待参数所设置的时间,在该时间内如果获得锁返回 true,如果没有获得锁返回 false
* @param time 等待锁的最长时间
* @param unit 时间参数的单位
* @return 如果获取了锁,则为true;如果在获取锁之前经过了等待时间,则为false
* @throws InterruptedException 如果当前线程在获取锁时被打断抛出此异常
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
* 注:如果释放的是非当前线程拥有的锁,将抛出 unchecked 异常 IllegalMonitorStateException
*/
void unlock();
/**
* 创建条件对象
*/
Condition newCondition();
ReentrantLock 可重入锁
ReentrantLock 是 Lock 接口的一个实现类
可重入
每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
简单的说就是当持有该锁的线程再次尝试获取该锁时,不会被阻塞(因为他已经持有该锁),而另外的线程尝试获取该锁时将被阻塞。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class ReentrantLockTest1 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(()->{
test1();
},"t1").start();
}
private static void test1(){
lock.lock();
try{
log.debug("进入第1层");
test2();
}finally {
lock.unlock();
}
}
private static void test2(){
lock.lock();
try{
log.debug("进入第1层");
test3();
}finally {
lock.unlock();
}
}
private static void test3(){
lock.lock();
try{
log.debug("进入第1层");
}finally {
lock.unlock();
}
}
}
lockInterruptibly 方法上锁(可打断)
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockInterruptiblyTest {
private static ReentrantLock lock = new ReentrantLock();
private static Thread t1,t2;
public static void main(String[] args) {
t1 = new Thread(()->{
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
log.debug("被打断表示未获得锁");
}
try{
log.debug("成功获得锁");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁");
}
},"t1");
t1.start();
t2 = new Thread(()->{
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
log.debug("被打断表示未获得锁");
// 本示例 t2 线程会被打断,进入此块
// 这里未获取锁,要么重试,要么结束,否则会执行下面的代码,且没有加锁
// 这样的话最后调用 unlock 方法时会抛出异常 IllegalMonitorStateException
// 可打断的意义在于,避免 lock 方法的死等,避免死锁
}
try{
log.debug("成功获得锁");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁");
}
},"t2");
t2.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 2 秒后执行打断线程 t2 的操作
// 由于 t1 获得锁后睡眠 5 秒,所以此处打断时,打断的是 t2 线程的 lockInterruptibly 方法,导致 t2 线程没有获得锁
t2.interrupt();
}
}
注:注意查看代码中的注释
tryLock方法获取锁(锁超时)
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class ReentrantLockTest {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
new Thread(()->{
if(reentrantLock.tryLock()){
try{
// 持锁线程,持有锁2秒
TimeUnit.SECONDS.sleep(2);
log.debug("try 无时间");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
},"t1").start();
try {
// 睡眠10毫秒,以希望第一个线程先执行获得锁(这样做并不能保证第一个线程一定获得锁,此处只为验证 tryLock 的特性)
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
try {
if(reentrantLock.tryLock(5,TimeUnit.SECONDS)){
try{
// 会打印,因为当前线程会在5秒时间内尝试获取锁,第一个线程持有锁的时间为2秒
log.debug("try 有时间");
} finally {
reentrantLock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
new Thread(()->{
if(reentrantLock.tryLock()){
try{
// 不会打印,因为第一个线程持有所,当前线程尝试直接返回false
log.debug("try 第二个无时间");
}finally {
reentrantLock.unlock();
}
}
},"t3").start();
}
}
执行结果:
15:37:12.409 [t1] DEBUG com.yyoo.thread.ReentrantLockTest - try 无时间
15:37:12.411 [t2] DEBUG com.yyoo.thread.ReentrantLockTest - try 有时间
t1 线程优先获得锁,所以会打印,t2 线程尝试获得锁且等待5秒,t1线程在2秒后就会释放锁,所以t2获得了锁。t3线程直接尝试获得锁,此时t1还没有释放锁,所以t3没有获得锁。
注:这里只是判断是否可以获得锁的示例所以这里使用的判断为 if 条件,请根据自身情况选择 if 还是 while
tryLock(long timeout, TimeUnit unit) 方法是可以被打断的,所以在 tryLock 等待时间内,如果被打断,依然会抛出 InterruptedException
公平锁
ReentrantLock 对象中有个 Sync 对象,其有两个实现:NonfairSync(不公平锁)、FairSync(公平锁),ReentrantLock 的两个构造函数定义如下:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁是按照先进入阻塞队列,先获得锁的思想来实现的。可以用于解决线程饥饿的问题。但也会带来并发度降低的问题。一般都不使用。
Sync 继承自 AbstractQueuedSynchronizer 也就是大名鼎鼎的 AQS,关于 AQS 我们会在后续文章中详解,此处先了解即可,我们先了解怎么用,再来说原理
条件变量 Condition
我们还是用前面送外卖的示例来说明。synchronized 关键字实际上表示一个条件变量,Condition 是可以支持多条件变量,控制粒度更细致。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockConditionTest {
// 外卖订单
private static ReentrantLock order = new ReentrantLock();
// 买家条件
private static Condition buyer = order.newCondition();
// 卖家条件
private static Condition seller = order.newCondition();
// 骑手条件
private static Condition rider = order.newCondition();
/**
* 0:未点餐
* 1:已点餐,未制作
* 2:制作完成,骑手未送货
* 3:骑手送货成功,可以开始干饭了
*/
private static int state = 0;
public static void main(String[] args) {
new Thread(()->{
order.lock();
try{
if(state == 0) {
log.debug("没人点餐,等待!");
seller.await();
}
if(state == 1){
log.debug("开始制作");
TimeUnit.SECONDS.sleep(5);// 模拟制作时间
state = 2;
log.debug("制作成功");
rider.signal();// 通知骑手送货
}
} catch (InterruptedException e) {
// await 方法可以被打断
throw new RuntimeException(e);
} finally {
order.unlock();
}
},"卖家").start();
new Thread(()->{
order.lock();
try{
if(state == 0) {
log.debug("没人点餐,等待!");
rider.await();
}
if(state == 1){
log.debug("外卖制作中,等待!");
rider.await();
}
if(state == 2){
log.debug("取到外卖,开始送货");
TimeUnit.SECONDS.sleep(5);// 模拟送货时间
state = 3;
log.debug("外卖送到");
buyer.signal();// 通知买家收货
}
} catch (InterruptedException e) {
// await 方法可以被打断
throw new RuntimeException(e);
} finally {
order.unlock();
}
},"骑手").start();
new Thread(()->{
order.lock();
try{
log.debug("开始点餐");
state = 1;
log.debug("点餐成功");
seller.signalAll();// 通知卖家接单制作
log.debug("等待收外卖");
buyer.await();// 等待收外卖
log.debug("收到外卖,开始干饭");
} catch (InterruptedException e) {
// await 方法可以被打断
throw new RuntimeException(e);
} finally {
order.unlock();
}
},"买家").start();
}
}
执行结果
17:08:44.346 [卖家] DEBUG com.yyoo.thread.LockConditionTest - 没人点餐,等待!
17:08:44.348 [骑手] DEBUG com.yyoo.thread.LockConditionTest - 没人点餐,等待!
17:08:44.348 [买家] DEBUG com.yyoo.thread.LockConditionTest - 开始点餐
17:08:44.348 [买家] DEBUG com.yyoo.thread.LockConditionTest - 点餐成功
17:08:44.348 [买家] DEBUG com.yyoo.thread.LockConditionTest - 等待收外卖
17:08:44.348 [卖家] DEBUG com.yyoo.thread.LockConditionTest - 开始制作
17:08:49.363 [卖家] DEBUG com.yyoo.thread.LockConditionTest - 制作成功
17:08:49.363 [骑手] DEBUG com.yyoo.thread.LockConditionTest - 取到外卖,开始送货
17:08:54.368 [骑手] DEBUG com.yyoo.thread.LockConditionTest - 外卖送到
17:08:54.368 [买家] DEBUG com.yyoo.thread.LockConditionTest - 收到外卖,开始干饭
await 和 signal 以及 signalAll 方法和 wait & notify & notifyAll 方法类似,await & signal 必须要有与之关联的锁,wait & notify 只能在 synchronized 块中使用。
点外卖这个示例,其实就是实际中的线程执行的顺序性问题的解决方案之一。