一.线程中断机制
假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
1.1如何停止中断运行中的线程?
通过一个volatile变量实现
在多线程编程中,可见性是指当一个线程修改了共享变量的值时,其他线程能够立即看到这个修改。在 Java 中,由于线程之间存在本地缓存,为了确保可见性,我们可以使用 volatile 关键字。
使用 volatile 关键字能够告诉 JVM 不要对这个变量进行本地缓存优化,而是每次都从主内存中读取变量的值。这样,当一个线程修改了 isStop 的值时,其他线程能够立即看到这个修改,确保了可见性。
使用原子变量类
通过Thread类自带的中断API方法实现
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
Java提供了一种用于停止线程的协商机制——中断。 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
- 若要中断一个线程,你需要手动调用该线程的 interrupt 方法,该方法也仅仅是将线程对象的中断标识设成 true,并不是真正立刻停止线程;
- 接着你需要自己写代码不断地检测当前线程的标识位,如果为 true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
Thread类定义了如下关于中断的方法:
1.2Thread类的三大API说明
实例方法interrupt(),没有返回值
当对一个线程,调用 interrupt() 时:
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
- 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么当前线程将立即退出被阻塞状态,并抛出一个InterruptedException异常,并且会清除它的中断状态,即false。
- 中断一个不活动的线程不会产生任何影响。
实例方法isInterrupted(),返回布尔值
测试此线程是否已中断。这个实例方法的底层调用了一个native方法,传入了一个布尔值,而这个值就是 是否清除中断标识位,false表示不清除,true表示清除(即将线程的中断标识位清除重新设置为false)。
静态方法 interrupted(),返回布尔值
Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
- 返回当前线程的中断状态
- 将当前线程的中断状态设为 false
二.线程之间的通信(等待唤醒机制)
方法一:Object类中的wait和notifyAll方法
需要使用synchronized关键字
- notify:唤醒队列中第一个等待线程(等待时间最长的线程),使其从wait()方法返回,而返回的前提时该线程获取到对象的锁。
- notifyAll:通知所有等待在该对象上的线程。notify()/notifyAll() 只能唤醒等待在同一把锁上的线程。
- wait:调用此方法的线程进入阻塞等待状态,并且会被加入到一个等待队列,只有等待另外线程的通知或者被中断才会返回,调用wait方法会释放对象的锁
均是Object的方法,均只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStageException
//第一步 创建资源类,定义属性和操作方法
class Share1 {//初始值private int number = 0;//+1的方法public synchronized void incr() throws InterruptedException {//第二步 判断 干活 通知if (number != 0) { //判断number值是否是0,如果不是0,等待this.wait(); //在哪里睡,就在哪里醒}//如果number值是0,就+1操作number++;System.out.println(Thread.currentThread().getName() + " :: " + number);//通知其他线程this.notifyAll();}//-1的方法public synchronized void decr() throws InterruptedException {//判断if (number != 1) {this.wait();}//干活number--;System.out.println(Thread.currentThread().getName() + " :: " + number);//通知其他线程this.notifyAll();}
}public class ThreadDemo1 {//第三步 创建多个线程,调用资源类的操作方法public static void main(String[] args) {Share1 share = new Share1();//创建线程new Thread(() -> {for (int i = 1; i <= 10; i++) {try {share.incr(); //+1} catch (InterruptedException e) {e.printStackTrace();}}}, "AA").start();new Thread(() -> {for (int i = 1; i <= 10; i++) {try {share.decr(); //-1} catch (InterruptedException e) {e.printStackTrace();}}}, "BB").start();}
}
虚假唤醒问题
上面的例子中是两个线程,我此时再创建一个线程 cc
接下来我们来分析一下这段代码为什么会出现负数的问题。
- 假设某一时刻,number 为 0 ,B、C两个消费者线程按顺序(因为加锁的缘故)调用 decrement 都发现 number 为 0,就都会调用 wait 方式进行释放锁进行等待;
- 然后线程A也调用 increment,判断是0,不满足调用wait条件,然后将 number 加成1之后,调用notifyAll方法同时唤醒B、C线程,A执行完代码,释放了锁;
- B、C被唤醒之后,假设B抢到锁,C没抢到,C继续阻塞,B从wait方法那继续往下走,将number 减1,此时number 变为 0
- B执行完释放了锁之后C这时抢到了锁,也从wait方法那继续执行代码,然后也将number 减1,这下出现问题了,线程B减完之后就是0了,线程C又将number=0减1,那不就变成-1了,所以这就产生的负数的情况。
虚假唤醒就是由于把所有线程都唤醒了,但是只有其中一部分是有用的唤醒操作,其余的唤醒都是无用功,对于不应该被唤醒的线程而言,便是虚假唤醒。
解决方法
很简单,在等待方执行的逻辑中,一定要用while循环来判断等待条件。
因为执行notify/notifyAll方法时只是让等待线程从wait方法返回,而非重新进入临界区
方法二:Condition类中的await和signalAll方法
需要使用Lock锁
在等待方执行的逻辑中,一定要用while循环来判断等待条件。
//第一步 创建资源类,定义属性和操作方法
class Share2 {private int number = 0;//创建Lockprivate Lock lock = new ReentrantLock();private Condition condition = lock.newCondition();//+1public void incr() throws InterruptedException {//上锁lock.lock();try {//判断while (number != 0) {condition.await();}//干活number++;System.out.println(Thread.currentThread().getName() + " :: " + number);//通知condition.signalAll();} finally {//解锁lock.unlock();}}//-1public void decr() throws InterruptedException {lock.lock();try {while (number != 1) {condition.await();}number--;System.out.println(Thread.currentThread().getName() + " :: " + number);condition.signalAll();} finally {lock.unlock();}}
}public class ThreadDemo2 {public static void main(String[] args) {Share2 share = new Share2();new Thread(() -> {for (int i = 1; i <= 10; i++) {try {share.incr();} catch (InterruptedException e) {e.printStackTrace();}}}, "AA").start();new Thread(() -> {for (int i = 1; i <= 10; i++) {try {share.decr();} catch (InterruptedException e) {e.printStackTrace();}}}, "BB").start();new Thread(() -> {for (int i = 1; i <= 10; i++) {try {share.incr();} catch (InterruptedException e) {e.printStackTrace();}}}, "CC").start();new Thread(() -> {for (int i = 1; i <= 10; i++) {try {share.decr();} catch (InterruptedException e) {e.printStackTrace();}}}, "DD").start();}}
Condition是一个接口,可以使用 lock.newCondition() 来创建实例,Condition的方法如下:
均只能在 lock锁块 中使用,否则会抛出异常IIIegalMonitorStageException
方法三:LockSupport类中的park等待和unpark唤醒
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。可以把许可看成是一种 (0,1) 信号量(Semaphore),但与 信号量(Semaphore)不同的是,许可的累加上限是1。
- park() /park(Object blocker) :如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用。
- unpark(Thread thread) :如果给定线程尚不可用,则为其提供许可。但凭证最多只能有1个,累加无效。
优点:
- 不需要获取锁: LockSupport的阻塞和唤醒不需要先获得锁。传统的synchronized和Lock都是基于锁的,线程必须先获得锁才能调用相应的阻塞或唤醒方法。而LockSupport不依赖于任何锁,可以在任意时刻调用。
- 不会抛出异常: LockSupport的阻塞和唤醒操作不会抛出中断异常,因此避免了因为中断而引入的异常处理逻辑。在传统的wait()和await()方法中,线程在等待时可能会被中断,需要捕获InterruptedException,而LockSupport避免了这一点。