从JDK源码角度看java并发线程的中断

本文涉及的产品
文档翻译,文档翻译 1千页
语种识别,语种识别 100万字符
图片翻译,图片翻译 100张
简介:         线程的定义给我们提供了并发执行多个任务的方式,大多数情况下我们会让每个任务都自行执行结束,这样能保证事务的一致性,但是有时我们希望在任务执行中取消任务,使线程停止。

        线程的定义给我们提供了并发执行多个任务的方式,大多数情况下我们会让每个任务都自行执行结束,这样能保证事务的一致性,但是有时我们希望在任务执行中取消任务,使线程停止。在java中要让线程安全、快速、可靠地停下来并不是一件容易的事,java也没有提供任何可靠的方法终止线程的执行。

        线程调度策略中有抢占式和协作式两个概念,与之类似的是中断机制也有协作式和抢占式。

        历史上Java曾经使用stop()方法终止线程的运行,他们属于抢占式中断。但它引来了很多问题,早已被JDK弃用。调用stop()方法则意味着

        ①将释放该线程所持的所有锁,而且锁的释放不可控。

        ②即刻将抛出ThreadDeath异常,不管程序运行到哪里,但它不总是有效,如果存在被终止线程的锁竞争;

        第一点将导致数据一致性问题,这个很好理解,一般数据加锁就是为了保护数据的一致性,而线程停止伴随所持锁的释放,很可能导致被保护的数据呈现不一致性,最终导致程序运算出现错误。第二点比较模糊,它要说明的问题就是可能存在某种情况stop()方法不能及时终止线程,甚至可能终止不了线程。看如下代码会发生什么情况,看起来线程mt因为执行了stop()方法将停止,按理来说就算execut方法是一个死循环,只要执行了stop()方法线程将结束,无限循环也将结束。其实不会,因为我们在execute方法使用了synchronized修饰,同步方法表示在执行execute时将对mt对象进行加锁,另外,Thread的stop()方法也是同步的,于是在调用mt线程的stop()方法前必须获取mt对象锁,但mt对象锁被execute方法占用,且不释放,于是stop()方法永远获取不了mt对象锁,最后得到一个结论,使用stop()方法停止线程不可靠,它未必总能有效终止线程。

 

public class ThreadStop {
public static voidmain(String[] args) {
           Thread mt= new MyThread();
           mt.start();
           try {
                    Thread.currentThread().sleep(100);
           } catch(InterruptedException e) {
                    e.printStackTrace();
           }
           mt.stop();
}
 
static classMyThread extends Thread {
           publicvoid run() {
                    execute();
           }
           privatesynchronized void execute() {
                    while(true) {
                    }
           }
}
}


 

        经历了很长时间的发展,Java最终选择用一种协作式的中断机制实现中断。协作式中断的原理很简单,其核心是先对中断标识进行标记,某线程设置某线程的中断标识位,被标记了中断位的线程在适当的时间节点会抛出异常,捕获异常后做相应的处理。实现协作中断有三个要点需要考虑:①是在Java层面实现轮询中断标识还是在JVM中实现;②轮询的颗粒度的控制,一般颗粒度要尽量小周期尽量短以保证响应的及时性;③轮询的时间节点的选择,其实就是在哪些方法里面轮询,例如JVM将Thread类的wait()、sleep()、join()等方法都实现中断标识的轮询操作。

        中断标识放在哪里?中断是针对线程实例而言,从Java层面上看,标识变量放到线程中肯定再合适不过了,但由于由JVM维护,所以中断标识具体由本地方法维护。在Java层面仅仅留下几个API用于操作中断标识,如下,

public class Thread{
    public voidinterrupt() {……}
    public BooleanisInterrupted() {……}
    public static Booleaninterrupted() {……}
}


        上面三个方法依次用于设置线程为中断状态、判断线程状态是否中断、清除当前线程中断状态并返回它之前的值。通过interrupt()方法设置中断标识,假如在非阻塞线程则仅仅只是改变了中断状态,线程将继续往下运行,但假如在可取消阻塞线程中,如正在执行sleep()、wait()、join()等方法的线程则会因为被设置了中断状态而抛出InterruptedException异常,程序对此异常捕获处理。

        上面提到的三个要点,第一是轮询在哪个层面实现,这个没有特别的要求,在实际中只要不出现逻辑问题,在Java层面或JVM层面实现都是可以的,例如常用的线程睡眠、等待等操作是通过JVM实现,而java并发框架工具里面的中断则放到Java实现,不管在哪个层面上去实现,在轮询过程中都一定要能保证不会产生阻塞。第二是要保证轮询的颗粒度尽可能的小周期尽可能短,这关系到中断响应的速度。第三点是关于轮询的时间节点的选取。

        针对三要点来看看java并发框架中是如何支持中断的,主要在等待获取锁的过程中提供中断操作,下面是伪代码。只需增加加红加粗部分逻辑即可实现中断支持,在循环体中每次循环都对当前线程中断标识位进行判断,一旦检查到线程被标记为中断则抛出InterruptedException异常,高层代码对此异常捕获处理即完成中断处理。总结起来就是java并发工具获取锁的中断机制是在Java层面实现的,轮询时间节点选择在不断做尝试获取锁操作过程中,每个循环的颗粒度比较小,响应速度得以保证,且循环过程不存在阻塞风险,保证中断检测不会失效。

if(尝试获取锁失败) {
    创建node
    使用CAS方式把node插入到队列尾部
    while(true){
    if(尝试获取锁成功并且 node的前驱节点为头节点){
把当前节点设置为头节点
    跳出循环
}else{
    使用CAS方式修改node前驱节点的waitStatus标识为signal
    if(修改成功){
        挂起当前线程
        if(当前线程中断位标识为true)
           抛出InterruptedException异常
}
}
}


 

        判断线程是否处于中断状态其实很简单,只需使用Thread.interrupted()操作,如果为true则说明线程处于中断位,并清除中断位。至此java并发工具实现了支持中断的获取锁操作。

        本文从java发展过程分析了抢占式中断及协作式中断,由于抢占式存在一些缺陷现在已不推荐使用,而协作式中断作为推荐做法,尽管在响应时间较长,但其具有无可比拟的优势。协作式中断我们可以在JVM层面实现,同样也可以在Java层面实现,例如JDK并发工具的中断即是在Java层面实现,不过如果继续深究是因为Java留了几个API供我们操作线程的中断标识位,这才使Java层面实现中断操作得以实现。            对于java的协作式中断机制有人肯定有人批评,批评者说java没有抢占式中断机制,且协作式中断机制迫使开发者必须维护中断状态,迫使开发者必须处理InterruptedException。但肯定者则认为,虽然协作式中断机制推迟了中断请求的处理,但它为开发人员提供更灵活的中断处理策略,响应性可能不及抢占式,但程序健壮性更强。



====广告时间,可直接跳过====

鄙人的新书《Tomcat内核设计剖析》已经在京东预售了,有需要的朋友可以到https://item.jd.com/12185360.html 进行预定。感谢各位朋友。

=========================


欢迎关注:



目录
相关文章
|
8天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
48 17
|
19天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
4天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
21天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
21天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
21天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
45 3
|
21天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
116 2
|
安全 Java
Java并发编程笔记之CopyOnWriteArrayList源码分析
并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝数组(快照)上进行的,也就是写时拷贝策略。
19561 0
|
Java 安全
Java并发编程笔记之读写锁 ReentrantReadWriteLock 源码分析
我们知道在解决线程安全问题上使用 ReentrantLock 就可以,但是 ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 ReentrantLock 满足不了需求,所以 ReentrantReadWriteLock 应运而生,ReentrantReadWriteLock 采用读写分离,多个线程可以同时获取读锁。
3143 0
|
Java
Java并发编程笔记之FutureTask源码分析
FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
4302 0