文章目录
废弃的做法-stop/suspend/resume
JDK1.0中定义了stop/suspend/resume方法,用于中止一个正在运行的线程;其中,stop用于彻底停止当前运行的线程,suspend是暂停当前运行线程,一直阻塞到其他线程调用resume方法。
但是,JDK1.2开始,这几个方法都被废弃了,原因如下:
-
stop方法会立即终止当前运行的线程,从而导致业务逻辑执行不完整。
-
stop方法会立即释放独占锁资源,但是无法保证锁内代码块的原子性。
案例如下所示:
public class ThreadTest { private Object object = new Object(); private Integer i = 0; @Test public void test1() throws InterruptedException { Thread t1 = new Thread(()->{ synchronized (object) { i++; System.out.println("i = " + i); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } i--; System.out.println("i = " + i); } }); t1.start(); TimeUnit.SECONDS.sleep(2); t1.stop(); } }
上述代码的逻辑很简单,即主线程中开启新的线程t1,t1中休眠10s,主线程休眠2s后调用stop方法,输出的结果为:
i = 1;
这对于业务来说是无法忍受的,如果stop方法之后要执行的逻辑是释放资源等清理性的工作,那么这些工作将永远无法被执行;而且对于使用同步机制的业务来说,自然是想保证共享数据的一致性,但是由于stop方法显然破坏了这一点。
-
suspend和resume需要成对出现,否则极容易出现死锁。线程在执行suspend方法后,并不会释放锁,此时如果另外一个线程需要先获取该锁资源再去resume目标线程,那么就会发生死锁。(如果想实现暂停-唤醒的逻辑,可以通过设置一个标志位(volatile),表示该线程是应该运行还是挂起,如果是挂起,那么就调用wait方法使其等待;如果该运行,即恢复,则调用notify重新启动线程。wait方法与suspend不同的是,wait方法会释放锁资源,再被notify后需要重新参与锁的竞争~)
中断线程的方法
自定义标志位
通过自定义变量作为状态位,定期检查该变量,符合条件则继续执行,否则退出。该方法适应于正在运行的线程
将一个大任务分割为多个小任务,每次小任务执行完成后都会去校验一个状态标志位是否为true,如果非true,那么线程终止,不再继续执行任务。(状态标志位为volatile,保证可见性)
public class ThreadTest2 {
private static volatile boolean isRun = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
int batch = 0;
while (isRun) {
System.out.println("==========" + batch++);
}
}).start();
TimeUnit.SECONDS.sleep(1);
//终止线程
stop();
TimeUnit.SECONDS.sleep(10000);
}
public static void stop() {
isRun = false;
}
}
Interrupt()方法
该方法适应于非正在运行线程的退出,对于大部分阻塞线程的方法,通过Thread.interrupt()可以立刻退出等待,抛出InterruptedException,包含sleep、join、wait和Lock.lockInterruptibly()、NIO阻塞等;
如果你用了线程池,并使用了Future对象,可以使用future.cancel(boolean)方法取消正在执行的任务,false会等待正在执行的任务执行完成,但是会取消还没开始执行的任务;true表示会中断正在执行且能够响应中断异常的任务!!
案例
case1:一直运行的线程无法被中断
@Test
public void test0() throws InterruptedException {
Thread t1 = getThread0();
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
System.out.println("thread isInterrupted = " + t1.isInterrupted());
TimeUnit.SECONDS.sleep(10);
}
public Thread getThread0() {
return new Thread(() -> {
int count = 0;
while (true) {
System.out.println("=======" + count++);
}
});
}
运行结果:一直运行,直至10s后test1方法结束;
case2:线程被中断
@Test
public void test1() throws InterruptedException {
Thread t2 = getThread1();
t2.start();
TimeUnit.SECONDS.sleep(2);
t2.interrupt();
System.out.println("thread isInterrupted = " + t2.isInterrupted());
TimeUnit.SECONDS.sleep(10);
}
public Thread getThread1() {
return new Thread(() -> {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("==========" + count++);
}
System.out.println(Thread.currentThread().getName() + " is interrupted");
});
}
运行结果:调用完interrupt方法后,线程中断结束;
Interrupt实现机制
探寻其实现机制,首先要从源码入手~
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
首先判断执行中断的线程是不是自身被中断的线程,如果是由其他线程进行中断,会调用checkAccess()方法进行校验其他线程是否有权限对当前线程进行修改,源码如下:
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
SecurityManager是java提供的安全管理器,目的是在运行阶段检查应用访问资源的权限,保护系统免受恶意操作攻击。可以通过jvm配置或者编码方式启动安全管理器,若没有指定policy文件,那么会加载默认的策略文件。在策略文件中,我们可以定义系统文件读取权限、序列化权限等。
正常情况下,应用是没有启动安全管理器的,所以System.getSecurityManager() == null,直接跳过检查;
根据Interruptible b 是否为null,分为两部分逻辑处理,但是都会调用本地方法interrupt0;下面分开说明两部分的逻辑。
Interruptible b != null
blocker为Thread的成员变量,并提供了blockedOn()方法设置blocker的值;
void blockedOn(Interruptible b) {
synchronized (blockerLock) {
blocker = b;
}
}
传统IO在读写时,虽然是阻塞的,但是无法被中断;NIO支持在读写操作时进行中断,channel若实现了InterruptiableChannel接口,则表示支持中断。如常用的FileChannel,SocketChannel,DatagramChannel等都实现了该接口。
在其子类AbstractInterruptibleChannel中,定义了实现可中断IO机制的方法begin和end;NIO规定,在阻塞IO的语句前后,需要调用begin和end方法,为了保证end方法一定被调用,要求放在finally块中;
begin方法如下:
protected final void begin() {
if (interruptor == null) {
//初始化中断处理对象,中断处理对象中提供回调机制
interruptor = new Interruptible() {
public void interrupt(Thread target) {
synchronized (closeLock) {
if (!open)
return;
//设置标志位
open = false;
interrupted = target;
try {
//关闭channel
AbstractInterruptibleChannel.this.implCloseChannel();
} catch (IOException x) { }
}
}};
}
//将中断处理对象注册到当前线程
blockedOn(interruptor);
Thread me = Thread.currentThread();
//当前线程如果被中断,则注册的中断处理对象可能没有被执行,手动触发一下
if (me.isInterrupted())
interruptor.interrupt(me);
}
总的来说,就是在Thread的中断逻辑中,挂载自定义的中断处理对象,这样Thread对象被中断时,会执行中断处理对象的回调,在回调中执行关闭channel操作,这样就实现了对线程中断的响应。
Thread添加中断处理逻辑 是依赖blockedOn方法,如下:
static void blockedOn(Interruptible intr) { // package-private
sun.misc.SharedSecrets.getJavaLangAccess().blockedOn(Thread.currentThread(),
intr);
}
这里用到了SharedSecrets类,通过SharedSecrets能够访问JDK类库中因为作用域的限制而无法访问的类或者方法(和反射实现的效果一样),该方法实际上就调用thread.blockedOn(interruptible)方法。
回过来看看interrupt方法,在 b != null 时,首先调用interrupt0()方法,该方法只是设置interrupt标志位,然后调用b.interrupt方法,该方法就是上述说明的回调方法,一清二楚啦~
Interruptible b == null
对于b==null时,会直接调用本地方法interrupt0(),该方法能够中断wait,sleep和join等阻塞等待的方法;
对于java线程来说,最终都会映射为操作系统的线程。当执行interrupt()方法后,如果操作系统线程没有被中断,那么会设置操作系统线程的interrupt标志位为true,并唤醒线程的SleepEvent,随后唤醒线程的parker和ParkEvent。
ParkEvent包含了_ParkEvent变量和_SleepEvent变量,其中,_ParkEvent变量用于synchronized同步块和Object.wait()方法,_SleepEvent变量用于Thead.sleep();ParkEvent中包含了一把mutex互斥锁和一个cond条件变量,线程的阻塞和唤醒(park和unpark)通过它们实现的。
-
PlatformEvent::park() 方法会调用库函数pthread_cond_wait(_cond, _mutex)实现线程等待
- synchronized块的进入和Object.wait()的线程等待都是通过PlatformEvent::park()方法实现
- 注:Thread.join()是使用的Object.wait()实现的
-
PlatformEvent::park(jlong millis)方法会调用库函数pthread_cond_timedwait(_cond, _mutex, _abstime)实现计时条件等待
- Thread.sleep(millis)就是通过PlatformEvent::park(jlong millis)实现
-
PlatformEvent::unpark()方法会调用库函数pthread_cond_signal (_cond)唤醒上述等待的条件变量
- Thread.interrupt()就会触发其子类SleepEvent和ParkEvent的unpark()方法
- synchronized块的退出也会触发unpark()。其所在对象ObjectMonitor维护了ParkEvent数组作为唤醒队列,synchronized同步块退出时,会触发ParkEvent::unpark()方法来唤醒等待进入同步块的线程,或等待在Object.wait()的线程。
对于Synchronized等待事件,被唤醒后会尝试获取锁,如果失败则会通过循环继续park()等待,因此synchronized等待实际上不会被中断的;如果是Object.wait()事件,则会通过标记从而判断是否为notify()唤醒,如果不是则抛出InterruptedExcetion进行中断。
Parker与上述的ParkEvent类似,也持有一把mutex互斥锁和一个cond条件变量;凡是在java代码中通过unsafe.park()/unpark()的调用都会映射到Thread的_parker变量去执行。而unsafe.park()/unpark()正是由LockSupport类调用,如ReentrantLock,CountDownLatch,Semaphore,ThreadPoolExecutor,ArrayBlockingQueue等。(LockSupport.park()和unpark()类似于Object.wait和notify方法,不同的是,它不需要在同步代码块中,且unpark即便在park方法前进行调用,依然能够唤醒线程)
并非所有的阻塞方法都抛出 InterruptedException
。输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出 InterruptedException
,而且在被中断的情况下也不会提前返回。然而,对于套接字 I/O,如果一个线程关闭套接字,则那个套接字上的阻塞 I/O 操作将提前结束,并抛出一个 SocketException
。java.nio
中的非阻塞 I/O 类也不支持可中断 I/O,但是同样可以通过关闭通道或者请求 Selector
上的唤醒来取消阻塞操作。类似地,尝试获取一个内部锁的操作(进入一个 synchronized
块)是不能被中断的,但是 ReentrantLock
支持可中断的获取模式。
如何处理中断异常-InterruptedException
- 如果自己无法处理异常,可以在方法声明中向外抛出异常(throws InterruptedException),交给上层具体的业务来处理;
- 先捕获异常,做一些清理工作,然后再向外抛出异常;比如,玩家匹配游戏,当一个玩家匹配已经到来,但是另外一个玩家未到来,发生了中断,此时需要把已经到来的玩家重新放回队列中,然后再抛出异常;
public class PlayerMatcher {
private PlayerSource players;
public PlayerMatcher(PlayerSource players) {
this.players = players;
}
public void matchPlayers() throws InterruptedException {
try {
Player playerOne, playerTwo;
while (true) {
playerOne = playerTwo = null;
// Wait for two players to arrive and start a new game
playerOne = players.waitForPlayer(); // could throw IE
playerTwo = players.waitForPlayer(); // could throw IE
startNewGame(playerOne, playerTwo);
}
}
catch (InterruptedException e) {
// If we got one player and were interrupted, put that player back
if (playerOne != null)
players.addFirst(playerOne);
// Then propagate the exception
throw e;
}
}
}
- 如果自己知道发生异常后如何进行处理,那么不应该向外抛出异常。比如当由Runnable定义的任务调用了一个可中断的方法时,那么当发生中断异常时是ok的,但是并不意味着捕获异常然后什么都不做,因为java中在检测到中断并抛出InterruptedException时,会清除中断状态(保证只能被中断一次);因此需要保留中断发生的证据,以便调用栈中更高层的代码能够知道中断,并对中断做出响应;可以通过interrupt()方法进行 重新中断 来完成。
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
- 吞掉中断异常或者log一下,这也是最常见的处理方式,不推荐;
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException swallowed) {
/* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
}
}
}
- 如果不能重新抛出
InterruptedException
,不管您是否计划处理中断请求,仍然需要重新中断当前线程,因为一个中断请求可能有多个 “接收者”。标准线程池 (ThreadPoolExecutor
)worker 线程实现负责中断,因此中断一个运行在线程池中的任务可以起到双重效果,一是取消任务,二是通知执行线程线程池正要关闭。如果任务生吞中断请求,则 worker 线程将不知道有一个被请求的中断,从而耽误应用程序或服务的关闭。 - 对于不可取消的任务,需要在合适的时候重新设置中断状态。有些任务拒绝被中断,这使得它们是不可取消的。但是,即使是不可取消的任务也应该尝试保留中断状态,以防在不可取消的任务结束之后,调用栈上更高层的代码需要对中断进行处理。清单 6 展示了一个方法,该方法等待一个阻塞队列,直到队列中出现一个可用项目,而不管它是否被中断。为了方便他人,它在结束后在一个 finally 块中恢复中断状态,以免剥夺中断请求的调用者的权利。(它不能在更早的时候恢复中断状态,因为那将导致无限循环 ——
BlockingQueue.take()
将在入口处立即轮询中断状态,并且,如果发现中断状态集,就会抛出InterruptedException
。)
public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
// fall through and retry
}
}
} finally {
if (interrupted)
Thread.currentThread().interrupt();
}
}
Interrupted()和isInterrupted()
public static boolean interrupted() {
return currentThread().isInterrupted(true);//本地方法,true清除中断状态
}
public boolean isInterrupted() {
return isInterrupted(false); //本地方法,false不会清除中断状态
}
两个方法都能查看线程的中断状态,区别在于,interrupted()是static方法,调用后会清除线程的中断状态;而isInterrupted()是普通方法,调用后不会清除线程的中断状态。
线程池优雅关闭
该部分内容会在下个章节-优雅停机 部分进行讲解,敬请期待~
参考