1. 线程中断机制
面试题:
- Java.lang.Thread下的三个方法的用法和区别
- 如何中断一个运行中的线程?
- 如何停止一个运行中的线程?
1.1 什么是中断机制
- 首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,所以
Thread.stop
, Thread.suspend
, Thread.resume
都已经被废弃了
- 其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制
- 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自行实现。若要中断一个线程,你需要手动调用该线程 interrupt 方法,该方法也仅仅是将该线程对象的中断标识设置为true,接着你需要自己写代码不断检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟应该做什么需要你自己写代码实现
- 每个线程对象都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断
- 通过调用线程对象的 interrupt 方法将该线程的标识位设置为true;可以在别的线程中调用,也可以在自己的线程中调用
1.2 中断API
1.3 面试题中断机制考点
如何停止运行中的线程?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| static volatile boolean isStop = false;
public static void main(String[] args) { new Thread(() -> { while (true) { if (isStop) { System.out.println(Thread.currentThread().getName() + " isStop的值被改为true,t1程序停止"); break; } System.out.println("-----------hello volatile"); } }, "t1").start(); try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> isStop = true, "t2").start(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) { new Thread(() -> { while (true) { if (atomicBoolean.get()) { System.out.println(Thread.currentThread().getName() + " atomicBoolean的值被改为true,t1程序停止"); break; } System.out.println("-----------hello atomicBoolean"); } }, "t1").start(); try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> atomicBoolean.set(true), "t2").start(); }
|
- 通过Thread类自带的中断api实例方法实现
- 在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑stop线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + " isInterrupted()的值被改为true,t1程序停止"); break; } System.out.println("-----------hello isInterrupted()"); } }, "t1"); t1.start();
try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> t1.interrupt(), "t2").start(); }
|
当前线程中断标识为true,是不是线程就立刻停止
答案是不立刻停止,具体来说,当对一个线程调用interrupt时:
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响,所以interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行,对于不活动的线程没有任何影响。
- 如果线程处于阻塞状态(例如sleep, wait, join等),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断标志也将被清除),并抛出一个InterruptedException异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| public static void main(String[] args) { Thread t1 = new Thread(() -> { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + " 中断标志位为:true 程序停止"); 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(); }
|
静态方法Thread.interrupted()
, 谈谈你的理解
1 2 3
| public static boolean interrupted() { return currentThread().isInterrupted(true); }
|
- 返回当前线程中断标志位,并且清空中断标志位
- 也就是说,如果连续两次调用该方法,第二次调用将返回false
- 除非当前线程在第一次和第二次调用之间再次被 interrupt
2. 线程等待唤醒机制
三种实现方式:
- 关键字
synchronized
与 wait()/notify()
一起使用可以实现等待/通知模式
Lock
接口中的 newContition()
方法返回 Condition 对象,Condition 类使用 await()/signal()
也可以实现等待/通知模式
- LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
2.1 synchronized实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Share{ private int number = 0;
public synchronized void incr() throws InterruptedException { while (number != 0) { this.wait(); } number++; System.out.print(Thread.currentThread().getName()+"::"+number); this.notifyAll(); }
public synchronized void decr() throws InterruptedException { while (number != 1) { this.wait(); } number--; System.out.println(Thread.currentThread().getName()+"::"+number); this.notifyAll(); } }
|
注意
上述代码中进行判断时必须使用 while 而不是 if,否则会产生虚假唤醒问题,即唤醒后不需要再进行判断而可以直接执行
2.2 Lock实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class Share { private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition();
public void incr() throws InterruptedException { lock.lock(); try { while (number != 0) { condition.await(); } number++; System.out.print(Thread.currentThread().getName() + "::" + number + "--->"); condition.signalAll(); } finally { lock.unlock(); } }
public 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(); } } }
|
定制化通信
案例:启动三个线程,按照如下要求:
AA打印5次,BB打印10次,CC打印15次,一共进行10轮
具体思路:每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程。创建一个可重入锁与三个开锁通知
该案例被称为单标志法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| class Share{ private int flag = 1;
private Lock lock = new ReentrantLock(); Condition c1 = lock.newCondition(); Condition c2 = lock.newCondition(); Condition c3 = lock.newCondition();
public void Aprint(int loop) throws InterruptedException { lock.lock(); try{ while(flag!=1) { c1.await(); } for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环"); } flag = 2; c2.signal(); } finally { lock.unlock(); } } public void Bprint(int loop) throws InterruptedException { lock.lock(); try{ while(flag!=2) { c2.await(); } for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环"); } flag = 3; c3.signal(); } finally { lock.unlock(); } }
public void Cprint(int loop) throws InterruptedException { lock.lock(); try{ while(flag!=3) { c3.await(); } for (int i = 1; i <= 15; i++) { System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环"); } flag = 1; c1.signal(); } finally { lock.unlock(); } } }
|
单标志法的问题
- 该算法可确保每次只允许一个进程进入临界区,但进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区
2.3 LockSupport实现
上述两种方法使用都有限制
- 线程需要先获得并持有锁,必须在锁块(synchronized 或 lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。其中的 park()
和 unpark()
的作用分别是阻塞线程和解除阻塞线程
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),但与Semaphore不同的是,许可证不会累积,最多只有一个
主要方法(全部都是静态方法, 底层使用 UNSAFE
类实现)
- 阻塞:
- Permit许可证默认没有不能放行,所以一开始调
park()
方法当前线程就会阻塞,直到别的线程给当前线程的发放 Permit,park 方法才会被唤醒。
park()
: 阻塞当前线程
- 唤醒:
- 调用
unpark(thread)
方法后,就会发放 thread 线程的许可证,会自动唤醒 park 线程,即之前阻塞中的 LockSuppot.park()
方法会立即返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public static void main(String[] args) {
Thread t1 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t ----------come in"); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t ----------被唤醒"); }, "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(); }
|
3. 总结
面试题