中断机制
什么是中断机制?
首先
一个线程不应该由其他线程来强制中断或停止,而是应该有线程自己自行停止,自己来决定自己的命运。
所以,Thread.stop, Thead.suspend, Thead.resumer都已经被废弃了。
其次
在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
因此,Java提供了一种用于停止线程的协商机制–中断,即中断标识协商机制。
中断只是一种协商协作机制,Java中没有给中断增加任何语法,中断的过程完全需要程序员自己实现。
若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断表示设置成true
接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断
每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
通过调用线程对象的interrupt方法将该线程的标识为设为true; 可以在别的线程中调用,也可以在自己的线程中调用。
通过volatile变量实现
通过AtomicBoolean实现
通过Thread类自带的中断api实例方法实现
public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { if(Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName()+"\t isInterrupted()被修改为true,程序停止"); break; } System.out.println("t1 -----hello interrupt api"); } }, "t1"); t1.start(); System.out.println("-----t1的默认中断标志位:"+t1.isInterrupted()); //暂停毫秒 try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } //t2向t1发出协商,将t1的中断标志位设为true希望t1停下来 new Thread(() -> { t1.interrupt(); },"t2").start(); //t1.interrupt(); } static AtomicBoolean atomicBoolean = new AtomicBoolean(false); private static void m2_atomicBoolean() { new Thread(() -> { while (true) { if(atomicBoolean.get()) { System.out.println(Thread.currentThread().getName()+"\t atomicBoolean被修改为true,程序停止"); break; } System.out.println("t1 -----hello atomicBoolean"); } },"t1").start(); //暂停毫秒 try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { atomicBoolean.set(true); },"t2").start(); } static volatile boolean isStop = false; private static void m1_volatile() { new Thread(() -> { while (true) { if(isStop) { System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序停止"); break; } System.out.println("t1 -----hello volatile"); } },"t1").start(); //暂停毫秒 try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { isStop = true; },"t2").start(); }
总结:当对一个线程,调用interrupt方法时:
① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true, 仅此而已。
被设置中断标志的线程将继续正常运行,不受影响。
所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。
②如果线程处于阻塞状态(例如处理sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个interruptedException异常。
public static void main(String[] args) { //实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程 Thread t1 = new Thread(() -> { for (int i = 1; i <=300; i++) { System.out.println("-----: "+i); } System.out.println("t1线程调用interrupt()后的的中断标识02:"+Thread.currentThread().isInterrupted()); }, "t1"); t1.start(); System.out.println("t1线程默认的中断标识:"+t1.isInterrupted());//false //暂停毫秒 try { TimeUnit.MILLISECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } t1.interrupt();//true System.out.println("t1线程调用interrupt()后的的中断标识01:"+t1.isInterrupted());//true try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1线程调用interrupt()后的的中断标识03:"+t1.isInterrupted()); //????---false中断不活动的线程不会产生任何影响。 //ps: 两秒后线程已经执行完 }
中断协商案例深度解析
public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { if(Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName()+"\t " + "中断标志位:"+Thread.currentThread().isInterrupted()+" 程序停止"); break; } try { Thread.sleep(200); } catch (InterruptedException e) { // Thread.currentThread().interrupt();//没有它,程序不会停止,为什么要在异常处,再调用一次?? e.printStackTrace(); } System.out.println("-----hello InterruptDemo3"); } }, "t1"); t1.start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> t1.interrupt(),"t2").start(); } /** * 1 中断标志位,默认false * 2 t2 ----> t1发出了中断协商,t2调用t1.interrupt(),中断标志位true * 3 中断标志位true,正常情况,程序停止,^_^ * 4 中断标志位true,异常情况,InterruptedException,将会把中断状态将被清除,并且将收到InterruptedException 。中断标志位false * 导致无限循环 * * 5 在catch块中,需要再次给中断标志位设置为true,2次调用停止程序才OK */
中断只是一种协商机制,修改中断标识位仅此而已,不是立刻stop打断
public static void main(String[] args) { //测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态, // 第二次再调用时中断状态已经被清除,将返回一个false。 System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted()); //false System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());//false System.out.println("----1"); Thread.currentThread().interrupt();// 中断标志位设置为true System.out.println("----2"); System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());//true System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());//false LockSupport.park(); Thread.interrupted();//静态方法 Thread.currentThread().isInterrupted();//实例方法 }
静态方法interrupted将会清楚中断状态(源码传入的ClearInterrupted为true)
实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)
总结:
线程中断相关的方法:
public void interrupt(); interrupt()方法是一个实例方法
它通知目标线程中断,也仅仅是设置目标线程的中断标志位为true.
public boolean isInterrupted(); isInterrupted()方法也是一个实例方法
它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志
public static boolean interrupted(), Thread类的静态方法interrupted()
返回当前线程的中断状态真实值(boolean类型)后会将当前线程的中断状态设为false, 此方法调用之后会清除当前线程的中断标志位的状态(将中断标志位置为false了),返回当前值并清零置false
线程等待和唤醒
LockSupport是用来创建和其他同步类的基本线程阻塞原语
LockSupport中的 park() 和 unpark() 的作用分别是阻塞线程和解除被阻塞的线程
三种线程等待唤醒的方式
使用Object的wait()方法让线程等待,使用 Object中的notify()方法唤醒线程
使用JUC包中Condition的await方法让线程等待,使用signal()方法唤醒线程
LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object
private static void syncWaitNotify() { Object objectLock = new Object(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (objectLock){ System.out.println(Thread.currentThread().getName()+"\t ----come in"); try { objectLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t ----被唤醒"); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { synchronized (objectLock){ objectLock.notify(); System.out.println(Thread.currentThread().getName()+"\t ----发出通知"); } },"t2").start(); }
wait()和notify()必须放在同步代码块或同步方法中,并且成对出现
必须现wait()在notify(),否则程序无法执行,无法唤醒
Condition
private static void lockAwaitSignal() { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t ----come in"); condition.await(); System.out.println(Thread.currentThread().getName()+"\t ----被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } },"t1").start(); //暂停几秒钟线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName()+"\t ----发出通知"); }finally { lock.unlock(); } },"t2").start(); }
Condtion中的线程等待和唤醒方法,需要先获取锁
一定要先await再signal
LockSupport
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit)
但与Semaphores不同,许可证不会累积,最多只有一个
park()/park(Object blocker):阻塞,permit许可证默认没有不能方向,所以一开始调用park()方法当前线程就会阻塞,直到别的线程给当前线程发放permit,park方法才会被唤醒
unpark(Thread thread):唤醒,调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回
public static void main(String[] args) { Thread t1 = new Thread(() -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t ----come in"+System.currentTimeMillis()); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t ----被唤醒"+System.currentTimeMillis()); }, "t1"); t1.start(); //暂停几秒钟线程 //try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { LockSupport.unpark(t1); System.out.println(Thread.currentThread().getName()+"\t ----发出通知"); },"t2").start(); }
LockSupport 天生无锁块要求
之前错误的先唤醒后后等待,LockSupport照样支持,先unpark再park相当于提前有了通行证unpark,park时就没有拦截。park和unpark必须一一对应,因为许可证不会累积,最多只有一个
总结
LockSupport是一个线程阻塞工具类,所有的方法都是静态的,可以让线程在任意位置阻塞,阻塞之后也有对于的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码(native标识的方法即调用底层C++、C代码)。
LockSupport提供的park() 和 unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。
每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会累加凭证。
理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有一个。
当调用park方法时
如果有凭证,则会直接消耗掉这个凭证然后正常退出;
如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反,它会增加一个凭证,但凭证最多只能有一个,累加无效。
为什么可以突破wait/notify的原有调用顺序?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的靠凭证消费,故不会阻塞。
先发放了凭证后续可以畅通无阻。
为什么唤醒两次后阻塞两次,但最终结果还是会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够不能放行。