目录
Object类中的wait()和notify()方法实现线程的等待和唤醒
Condition接口中的await()后signal()方法实现线程的等待和唤醒
LockSupport类中的park()等待和unpack()唤醒
LockSupport与线程中断
线程中断机制
什么是中断机制?
首先,一个线程不应该由其他线程强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运(所以,Thread的stop()、suspend()、resume()都已经废弃了)
其次,在Java中没有办法立即停止一条线程,然而停止线程又显得那么重要(比如需要取消一个耗时/错误操作)。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制。
中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完成完全要求程序员自己实现。若要中断一个线程,需要手动调用该线程的interrupt()方法,该方法也仅仅是将线程对象的中断标识设成true,接着按自己的需要,写代码不断监测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要自己写代码实现。
每个线程对象中都有一个中断标识位,用于表示线程是否被中断:该标识位为true标识中断,为false表示未中断。通过调用线程对象的interrupt方法将该线程的标识位设为true,可以在别的线程中带调用,也可以在自己的线程中调用。
与中断相关的3个API
- public void interrupt():设置线程的中断状态为true,发起一个协商而不会立刻停止线程
- public static boolean isInterrupted():通过检查中断标识位判断线程是否被中断并清除当前中断状态(返回当前线程的中断状态+重置中断状态为false)
- public boolean isInterrupted():通过检查中断标识位判断当前线程是否被中断
如何停止中断运行中的线程?
- 通过volatile变量实现(线程间的可见性)
- 通过AtomicBoolean实现
执行结果如下图:
- 通过Thread类自带的中断API实例方法实现 在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程
-
- interrupt() 中断线程
我们可以看到interrupt0()是一个native方法
除非线程正在中断(始终允许),否则将带调用此线程的checkAccess()方法(可能还会导致抛出SecurityException)
public class InterruptDemo {
static volatile boolean isStop = false;
public static void main(String[] args) {
Thread tA = new Thread(() -> {
while (true) {
if (isStop) {
System.out.printf(Thread.currentThread().getName() + " isStop 被修改为true!");
break;
}
System.out.println("-------------------->");
}
}, "tA");
tA.start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
isStop = true;
}, "B").start();
}
}
执行结果如下图:
public class InterruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
Thread tA = new Thread(() -> {
while (true) {
if (atomicBoolean.get()) {
System.out.printf(Thread.currentThread().getName() + " atomicBoolean 被修改为true!");
break;
}
System.out.println("-------------------->");
}
}, "tA");
tA.start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
atomicBoolean.set(true);
}, "B").start();
}
}
package com.aqin.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @Description
* @Author aqin1012 AQin.
* @Date 10/9/23 11:29 AM
* @Version 1.0
*/
public class InterruptDemo {
public static void main(String[] args) {
Thread tA = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.printf(Thread.currentThread().getName() + " isInterrupted() 被修改为true!程序停止!");
break;
}
System.out.println("-------------------->");
}
}, "tA");
tA.start();
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
tA.interrupt();
}, "B").start();
}
}
执行效果如下图:
当前线程的中断标识为true,是不是线程就会立刻停止?
不会,具体来说,对一个线程,调用interrupt()时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true(仅此而已),被设置中断标志的线程将继续正常运行,不受影响。所以,interrupt()并不是真正的中断线程,需要被调用的线程自己进行配合才行。如果线程处于被阻塞状态(如sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt()方法时,那么线程将会立即退出被阻塞状态,并抛出一个InterruptedException异常。
如何理解静态方法Thread.interrupted()
这个方法是判断线程是否中断并清除当前中断状态的,简单来说就是“复位”。
主要做了两件事:
- 返回当前线程的中断状态,测试当前线程是否已经被中断
- 将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
那么问题来了,这个静态方法interrupted()跟实例方法isInterrupted()有什么区别呢?
我们举个简单的例子对比下执行结果
静态方法interrupted()
执行结果如下:
实例方法isInterrupted()
执行结果如下
来看它俩的源码对比
可以看到其实它们调用的是同一个方法,只不过,在对于是否需要清理这个参数的传递上,静态方法传递的是true,而实例方法传递的是false,简单来讲就是多了一步“复位”操作。因此,才会出现上面两段示例代码执行的结果不一致,在当前线程未执行中断方法interrupt()时,当前线程的中断标识位本身就是初始值false,因此连续调用两次静态方法或者实例方法的执行结果都是false;当当前线程执行了中断方法interrupt()时,第一次调用静态方法和实例方法的返回值同样都是true,但是此时静态方法多做了一步“复位”操作,把当前线程的中断标识位重置回了初始值false,而实例方法则没有这步操作,因此,当第二次调用时,实例方法的示例中当前线程的中断标识位仍然是true,因此仍然返回true,而静态方法的示例代码中当前线程的中断标识位已经被重置回了false,于是就返回了false。
LockSupport是什么
LockSupport是java.util.concurrent.locks中的一个类,用于创建锁和其他同步类的基本线程阻塞原语。
线程等待和唤醒机制
3种让线程等待唤醒的方法
- 使用Object中的wait()方法让线程等待,使用notify()方法唤醒线程
- 使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
- 使用LockSupport类中的park()和unpark()方法阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait()和notify()方法实现线程的等待和唤醒
示例代码:
public static void main(String[] args) {
Object objectLock = new Object();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + " 进入--->");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");
}
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
}
System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");
}, "B").start();
}
执行结果如下:
问题:
- 必须要先持有锁,否则会报错
- 必须先wait()再notify(),否则会卡死
Condition接口中的await()后signal()方法实现线程的等待和唤醒
示例代码:
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace();}
lock.lock();
System.out.println(Thread.currentThread().getName() + " 进入--->");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");
}, "A").start();
try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace();}
new Thread(() -> {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");
}, "B").start();
}
执行结果如下:
问题:
- 必须要先持有锁,否则会报错
在lock、unlock对里面才能正确调用condition中线程等待和唤醒的方法。
- 必须先await()再signal(),否则会卡死
上述两个对象Object和Condition使用的限制条件
- 线程先要获得并持有锁,必须在锁块(synchronized/lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park()等待和unpack()唤醒
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每一个线程都有一个Permit(许可),但与Semaphore不同的是,许可的累加上限是1(最多一个许可)。
使用示例:
public static void main(String[] args) {
Thread A = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 进入--->");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 被唤醒^ ^");
}, "A");
A.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
LockSupport.unpark(A);
System.out.println(Thread.currentThread().getName() + " 发出唤醒通知( ̄∇ ̄)/");
}, "B").start();
}
执行结果如下:
可以看到,使用LockSupport进行线程阻塞/唤醒不需要在锁对块(synchronized/lock)中,所以上面Object类和Condition接口的第一个问题解决了,然后我们看看第二个问题:加锁/解锁的先后顺序。
如上图可以看出,park()和unpack()执行的先后顺序不会影响结果。
总结
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport是一个线程阻塞工具,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞后也有对应的唤醒方法,归根结底,LockSupport调用的是Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程,每个使用LockSupport的线程都一个Permit(许可)与LockSupport相关联,每一个线程都都一个相关的Permit,并且最多一个,重复调用unpark()也不会积累Permit。
换句话讲,线程阻塞需要消耗凭证,并且这个凭证最多1个
当调用park()方法时:
- 如果有Permit,会消耗这个Permit,然后正常退出
- 如果没有Permit,会阻塞当前线程等待获取凭证
当调用unpark()方法时:
- 会增加一个Permit
- 调用多次也只会有一个Permit(不累加)
最后,我们思考2个问题:
- 为什么LockSupport提供park()和unpark()方法可以突破Object和Condition的调用先后顺序的限制? 因为调用unpark()会获得1个Permit,之后再调用park()会消耗这个Permit,park()和unpark()方法的执行顺序不会影响线程的唤醒,即便先发放了Permit仍然不会阻塞线程。
- 唤醒两次(调用两次park()方法)后阻塞两次(调用两次unpark)方法),会发生什么情况? 线程会被阻塞,因为Permit不会累加,即使调用了两次unpark(),仍然只会有一个Permit,后面接着的调用两次park(),在调用第一次park()时,就会把这个Permit消耗掉,第二次调用时就没有Permit了,因而会被阻塞。
搞定( ̄∇ ̄)/🎉🎉🎉~~~~~~~~~~