目录
一、为什么ConcurrentHashMap中key不允许为null
考察目标
问题解析
回答
二、ThreadLocal会出现内存泄漏吗?
考察目的
问题解析
回答
三、什么是CompletableFuture?
问题分析
问题解答
四、什么条件下会产出死锁,如何避免死锁?
1、什么是死锁?
2、产生死锁的原因
3、如何避免死锁?
五、ConcurrentHashMap是如何保证线程安全的?
1、JDK1.7实现原理
2、JDK1.8优化内容
六、ThreadLocal真的会造成内存泄漏吗?
1、ThreadLocal的基本原理
2、四种对象引用
3、造成内存泄漏的原因
4、如何避免内存泄漏?
七、为什么ConcurrentHashMap不允许插入null值?
1、探寻源码
2、歧义问题
3、作者回复
4、总结
八、synchronized 和 Lock 的区别
九、如何安全地中断一个正在运行的线程?
1、什么是线程?
2、如何操作如何安全中断运行中的线程?
这是一个基础问题,主要考察1到3年经验的开发人员,ConcurrentHashMap在实际应用中使用频率较高,考察这个问题的目的,是了解求职者的基本功。
所以为了表现更好,可以从ConcurrentHashMap的设计角度去回答。
打开ConcurrentHashMap的源码
在put方法里面,可以看到这样一段代码(如图)
如果key或者value为空,则抛出空指针异常。
但是为什么ConcurrentHashMap不允许key或者value为空呢?
简单来说,就是为了避免在多线程环境下出现歧义问题。
所谓歧义问题,就是如果key或者value为null,当我们通过get(key)获取对应的value的时候,如果返回的结果是null,我们没办法判断,它是put(k,v)的时候,value本身为null值,还是这个key本身就不存在。
比如在这样一种情况下(如图),线程t1调用containsKey方法判断key是否存在,假设当前这个key不存在,本来应该返回false。
但是在T1线程返回之前,正好有一个T2线程插入了这个key,但是value为null。这就导致原本T1线程返回的结果有可能是true,有可能是false,取决于T1和T2线程的执行顺序。
这种现象我们可以认为是线程安全性问题,而ConcurrentHashMap又是一个线程安全的集合,所以自然就不允许key或者value为null。
而HashMap中是允许存null的,因为它不需要考虑到线程安全性问题。
所以这个问题的核心本质还是ConcurrentHashMap这个并发安全性集合的特性。当然。Doug Lea还认为,不管是否是并发安全的集合,它都不应该允许存储null。
ConcurrentHashMap这么设计的原因是为了避免在多线程并发场景下的歧义问题。也就是说,当一个线程从ConcurrentHashMap获取某个key,如果返回的结果是null的时候。
这个线程无法确认,这个null表示的是确实不存在这个key,还是说存在key,但是value为空。
这种不确定性会造成线程安全性问题,而ConcurrentHashMap本身又是一个线程安全的集合,所以才这么设计!
这是并发编程里面的知识,所以考察的还是技术基础。
Java基础是每个公司必然都会考察的,不管你是工作1年还是工作10年。
因为所有的应用框架和中间件,都是在Java基础上构建出来的。
基本功扎实的人,不仅仅写的代码更加可靠,而且学习新技术也更加容易。
ThreadLocal是一个用来解决线程安全性问题的工具。
它相当于让每个线程都开辟一块内存空间,用来存储共享变量的副本。
然后每个线程只需要访问和操作自己的共享变量副本即可,从而避免多线程竞争同一个共享资源。
它的工作原理很简单(如图)
每个线程里面有一个成员变量ThreadLocalMap。
当线程访问用ThreadLocal修饰的共享数据的时候,这个线程就会在自己成员变量ThreadLocalMap里面保存一份数据副本。
key指向ThreadLocal这个引用,并且是弱引用关系,而value保存的是共享数据的副本。
因为每个线程都持有一个副本,所以就解决了线程安全性问题。
这个问题考察的是内存泄漏,所以必然和对象引用有关系。
ThreadLocal中的引用关系如图所示(如图),Thread中的成员变量ThreadLocalMap,它里面的可以key指向ThreadLocal这个成员变量,并且它是一个弱引用。
所谓弱引用,就是说成员变量ThreadLocal允许在这种引用关系存在的情况下,被GC回收。
一旦被回收,key的引用就变成了null,就会导致这个内存永远无法被访问,造成内存泄漏。
那到底ThreadLocal会不会存在内存泄漏呢?
从ThreadLocal本身的设计上来看,是一定存在的。
如果这个线程被回收了,那线程里面的成员变量都会被回收。就不会存在内存泄漏问题啊?
这样理解没问题,但是在实际应用中,我们一般都是使用线程池,而线程池本身是重复利用的,所以还是会存在内存泄漏的问题。
除此之外啊,ThreadLocal为了避免内存泄漏问题,当我们在进行数据的读写时,ThreadLocal默认会去尝试做一些清理动作,找到并清理Entry里面key为null的数据。
但是,它仍然不能完全避免,有同学就问了,那怎么办啊!!!
有两个方法可以避免:
1、每次使用完ThreadLocal以后,主动调用remove()方法移除数据
2、把ThreadLocal声明称全局变量,使得它无法被回收
ThreadLocal本身的设计并不复杂,要想深入了解,建议大家去看看源码!
不恰当的使用ThreadLocal,会造成内存泄漏问题。
主要原因是,线程的私有变量ThreadLocalMap里面的key是一个弱引用。
弱引用的特性,就是不管是否存在直接引用关系,
当成员ThreadLocal没用其他的强引用关系的时候,这个对象会被GC回收掉。从而导致key可能变成null,造成这块内存永远无法访问,出现内存泄漏的问题。规避内存泄漏的方法有两个:
1、通过扩大成员变量ThreadLoca的作用域,避免被GC回收
2、每次使用完ThreadLocal以后,调用remove方法移除对应的数据
第一种方法虽然不会造成key为null的现象,但是如果后续线程不再继续访问这个key。也会导致这个内存一直占用不释放,最后造成内存溢出的问题。
CompletableFuture是Java8引入的一个类,可以解决异步执行任务和处理异步任务的结果。
CompletableFuture确实是一个很实用的组件,面试官想通过这个问题去考察候选人是否熟悉异步编程的概念,是否能够编写异步代码。
另外,CompletableFuture可以优化系统的性能和响应速度,所以,面试官想要考察候选人是否能够设计和优化高性能、高并发的系统,并且能够使用CompletableFuture等工具来实现优化。
CompletableFuture是Java8中引入的一个组件,它提供了一种简洁而强大的方式来处理异步任务和处理异步任务的结果。
在CompletableFuture出现之前,我们只能使用Callable/Future的机制来获取异步线程的执行结果,但Future的是通过阻塞等待的方式来实现的,对性能不是很友好。
而使用CompletableFuture可以让我们将一个耗时的任务提交给线程池进行异步执行,然后可以继续执行其他的任务,等到异步任务执行结束后会出发一个回调方法,我们可以在回调方法中处理异步任务的执行结果。
相当于优化了Future阻塞等待的问题。
CompletableFuture提供了一些便捷的方法,例如thenApply、thenAccept、thenRun等,可以让我们以链式的方式处理异步任务的结果,从而更加灵活地编写异步代码。
死锁,简单来说就是两个或者两个以上的线程在执行过程中,去争夺同一个共享资源导致相互等待的现象。如果没有外部干预,线程会一直处于阻塞状态,无法往下执行。这样一直等待处于阻塞状态的线程,被称为死锁线程。
产生死锁需要同时满足以下四个条件:
第一个:互斥条件,共享资源a和b只能被一个线程占用;
第二个:请求和保持条件,线程T1已经获取共享资源a,在等待共享资源b的时候,不释放共享资源a;
第三个:不可抢占条件,其他线程不能强行抢占线程T1占有的资源;
第四个:循环等待条件,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,这形成了循环等待。
线程产生死锁之后,只能通过外部干预来解决问题,比如重启程序,或者Kill线程。所以,我们只能在写代码时规避死锁的产生。那么如何避免死锁产生呢?根据产生死锁的四个必要条件,我们只需要破坏其中任何一个条件就可以解决。
第一个:互斥条件是没有办法被破坏的,因为它是互斥锁的基本约束。其他三个条件都可以通过人工干预来破坏。比如请求保持条件,我们可以在首次执行一次性申请所有的资源,这样就不存在等待锁的问题了。
第二个:对于不可抢占条件来说,占用部分资源的线程在进一步申请其他资源的时候如果申请不到,我们可以主动释放它占有的资源。这样不可抢占这个条件就被破坏了。
第三个:对于循环等待条件来说,可以通过按序申请资源来预防死锁的产生。所谓按序申请,就是给资源编号,所有线程可以按照线性化的序号顺序去申请共享资源,先申请需要序号小的,再申请序号大的,这样循环等待自然就不存在了。
ConcurrentHashMap相当于是HashMap的多线程版本,它的功能本质上和HashMap没什么区别。因为HashMap在并发操作的时候会出现各种问题,比如死循环问题、数据覆盖等问题。而这些问题,只要使用ConcurrentHashMap就可以完美地解决。那问题来到了,ConcurrentHashMap它是如何保证线程安全的呢?
首先,我们来看JDK1.7中ConcurrentHashMap的底层结构,它基本延续了HashMap的设计,采用的是数组加链表的形式。和HashMap不同的是,ConcurrentHashMap中的数组设计分为大数组Segment和小数组HashEntry,来着这张图。
大树组Segment可以理解为一个数据库,而每个数据库(Segment)中又有很多张表(HashEntry),每个HashEntry中又有很多条数据,这些数据是用链表连接的。了解了ConcurrentHashMap的基本结构设计,我们再来看它的线程安全实现,就比较简单了。
接下来我们来对照JDK1.7中ConcurrentHashMap的put()方法源码实现。
因为Segment本身是基于ReentrantLock重入锁实现的加锁和释放锁的操作,这样就能保证多个线程同时访问ConcurrentHashMap时,同一时间只能有一个线程能够操作相应的节点,这样就保证了ConcurrentHashMap的线程安全。
也就是说ConcurrentHashMap的线程安全是建立在Segment加锁的基础上的,所以,我们称它为分段锁或者片段锁,如图中所示。
那JDK1.8又是如何实现的呢?
在JDK1.7中,ConcurrentHashMap虽然是线程安全的,但因为它的底层实现是数组加链表的形式,所以在数据比较多情况下,因为要遍历整个链表,会降低访问性能。所以,JDK1.8以后采用了数组加链表加红黑树的方式优化了ConcurrentHashMap的实现,具体实现如图所示:
当链表长度大于8,并且数组长度大于64时,链表就会升级为红黑树的结构。JDK1.8中的ConcurrentHashMap虽然保留了Segment的定义,但这,仅仅是为了保证序列化时的兼容性,不再有任何结构上的用处了。
那在JDK1.8中ConcurrentHashMap的源码是如何实现的呢?它主要是使用了CAS加volatile或者synchronized的方式来保证线程安全。
我们可以从源码片段中看到,添加元素时首先会判断容器是否为空,如果为空则使用 volatile 加 CAS 来初始化,如果容器不为空 ,则根据存储的元素计算该位置是否为空。
如果根据存储的元素计算结果为空则利用 CAS 设置该节点;
如果根据存储的元素计算为空不为空,则使用synchronized,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树。这样就能保证并发访问时的线程安全了。
如果把上面的执行用一句话归纳的话,就相当于是ConcurrentHashMap通过对头结点加锁来保证线程安全的。
这样设计的好处是,使得锁的粒度相比Segment来说更小了,发生hash冲突和加锁的频率也降低了,在并发场景下的操作性能也提高了。而且,当数据量比较大的时候,查询性能也得到了很大的提升。
3、总结
最后,我们来总结一下:
1、ConcurrentHashMap在JDK1.7中使用的数组加链表的结构,其中数组分为两类,大树组Segment和小数组HashEntry,而加锁是通过给Segment添加ReentrantLock重入锁来保证线程安全的。
2、ConcurrentHashMap在JDK1.8中使用的是数组加链表加红黑树的方式实现,它是通过CAS或者synchronized来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。
ConcurrentHashMap中有很多设计思想是值得我们去学习和借鉴的,比如说锁的粒度控制、分段锁的设计等等,都可以应用在实际的业务开发场景中。我们通过学习这些底层原理从中获取很多的设计思路,帮助我们更高效地去解决实际问题。
在多线程并发访问同一个共享变量的情况下,如果不做同步控制的话,就可能会导致数据不一致的问题,所以,我们需要使用synchronized加锁来解决。
而ThreadLocal换了一个思路来处理多线程的情况:
ThreadLocal本身并不存储数据,它使用了线程中的threadLocals属性,threadLocals的类型就是在ThreadLocal中的定义的ThreadLocalMap对象,当调用ThreadLocal的set(T value)方法时,ThreadLocal将自身的引用也就是this作为Key,然后,把用户传入的值作为Value存储到线程的ThreadLocalMap中,这就相当于每个线程的读写操作都是基于线程自身的一个私有副本,线程之间的数据是相互隔离的,互不影响。
这样一来基于ThreadLocal的操作也就不存在线程安全问题了。它相当于采用了用空间来换时间的思路,从而提高程序的执行效率。
在ThreadLocalMap内部,维护了一个Entry数组table的属性,用来存储键值对的映射关系,来看这样一段代码片段:
static class ThreadLocalMap {
private Entry[] table;
static class Entry implements WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
...
}
Entry将ThreadLocal作为Key,值作为Value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。有的小伙伴可能对「弱引用」不太熟悉,这里再介绍一下Java的四种引用关系。
在JDK1.2之后,Java对引用的概念做了一些扩充,将引用分为“强”、“软”、“弱”、“虚”四种,由强到弱依次为:
强引用:指代码中普遍存在的赋值行为,如:Object o = new Object() ,只要强引用 关系还在,对象就永远不会被回收。
软引用:还有用处,但不是必须存活的对象,JVM 会在内存溢出前对其进行回收,例如:缓存。
弱引用:非必须存活的对象 ,引用关系 比软引用还弱,不管内存是否够用,下次 GC 一定回收。
虚引用:也称“幽灵引用”、“幻影引用”,最弱的引用关系,完全不影响对象的回收,等同于没有引用,虚引用的唯一的目的是对象被回收时会收到一个系统通知。
这个描述还是比较官方的,简单总结一下,大家应该都追过剧:
强引用:就好比是男主角,怎么都死不了。
软引用:就像女主角,虽有一段经历,还是没走到最后。
弱引用:就是男二号,注定用来牺牲的。
虚引用:就是路人甲了。
内存泄漏和ThreadLocalMap中定义的Entry类有非常大的关系。
以上图完整地展示了ThreadLocal中对象引用的关系。
由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为空(null),如果这时Value外部也没有强引用指向它,那么Value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用Value,导致Value无法被回收,这时「内存泄漏」就发生了,Value成了一个永远也无法被访问,但是又无法被回收的对象。
Entry对象属于ThreadLocalMap,ThreadLocalMap又属于Thread,如果线程本身的生命周期很短,短时间内就会被销毁,那么「内存泄漏」立刻就会得到解决,只要线程被销毁,Value也会随之被回收。
问题是,线程本身是非常珍贵的计算机资源,很少会去频繁的创建和销毁,一般都是通过线程池来使用,这就将线程的生命周期大大拉长,「内存泄漏」的影响也会越来越大。
最后,一句话总结一下:
threadLocals对象中的Entry对象不再使用后,如果没有及时清除Entry对象,而程序自身也无法通过垃圾回收机制自动清除 ,就可能导致内存泄漏。
不要听到「内存泄漏」就不敢使用 ThreadLocal,只要规范化使用是不会有问题的。给大家支几个招:
1、每次使用完ThreadLocal都记得调用remove()方法清除数据。
2、将ThreadLocal变量尽可能地定义成static final,避免频繁创建ThreadLocal实例。这样也就保证程序一直存在ThreadLocal的强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的Value值,进而清除掉。
当然,就是使用不规范,ThreadLocal内部也做了一些优化,比如:
1、调用set()方法时,ThreadLocal会进行采样清理、全量清理,扩容时还会继续检查。
2、调用get()方法时,如果没有直接命中或者向后环形查找时也会进行清理。
3、调用remove()时,除了清理当前Entry,还会向后继续清理。
在Java语言中,给ConcurrentHashMap和Hashtable这些线程安全的集合中的Key或者Value插入null(空)值的会报空指针异常,但是单线程操作的HashMap又允许Key或者Value插入null(空)值。这到底是为什么呢?
为了找到原因,我们先来看这样一段源码片段,打开ConcurrentHashMap的putVal()方法,源码中第一句就非常明确地做了判断,如果Key或者Value为null(空)值,就直接抛出空指针异常。
我们在源码中似乎已经找到了原因,你可以这样回答面试官,说JDK源码就是这么规定的。然而,这个原因是不能说服面试官的,虽然,源码是这样设计的,我们要思考的是,这样设计背后更深层次的原因。
那到底为什么ConcurrentHashMap不允许插入null值,HashMap又允许插入呢?
因为给ConcurrentHashMap中插入null(空)值会存在歧义。我们可以假设ConcurrentHashMap允许插入null(空)值,那么,我们取值的时候会出现两种结果:
1、值没有在集合中,所以返回的结果就是null(空);
2、值就是null(空),所以返回的结果就是它原本的null(空)值。
这就产生了歧义问题。
那HashMap允许插入null(空)值,难道它就不担心出现歧义吗?这是因为HashMap的设计是给单线程使用的,所以如果取到null(空)值,我们可以通过HashMap的containsKey(key)方法来区分这个null(空)值到底是插入值是null(空),还是本就没有才返回的null(空)值。
而ConcurrentHashMap就不一样了,因为ConcurrentHashMap是在多线程场景下使用的,它的情况更加复杂。
举个例子:现在有线程T1调用了ConcurrentHashMap的containsKey(key)方法,我们期望返回的结果是false,也就是说,T1并没有往ConcurrentHashMap中put null(空)值。
但是,恰恰出了个意外,在线程T1还没有得到返回结果之前,线程T2又调用了ConcurrentHashMap的put()方法,插入了一个Key,并且存入的Value是null(空)值。那么,线程T1最终得到的返回结果就变成true了。
显然,这个结果和我们之前期望的false完全不一致。
也就是说,在多线程的复杂情况下,我们多线程的复杂情况下,到底是插入的null(空)值,还是本就没有才返回的null(空)值。也就是说,产生的歧义不能被证伪。
对于ConcurrentHashMap不允许插入null值的问题,有人问过ConcurrentHashMap的作者Doug Lea ,以下是他回复的邮件内容:
The main reason that nulls aren't allowed in ConcurrentMaps(ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if map.get(key) returns null, you can't detect whether the key explicitly maps to null vs the key isn't mapped.In a non-concurrent map, you can check this via map.contains(key),but in a concurrent one, the map might have changed between calls.
Further digressing: I personally think that allowingnulls in Maps (also Sets) is an open invitation for programsto contain errors that remain undetected untilthey break at just the wrong time. (Whether to allow nulls evenin non-concurrent Maps/Sets is one of the few design issues surroundingCollections that Josh Bloch and I have long disagreed about.)
It is very difficult to check for null keys and valuesin my entire application . Would it be easier to declare somewherestatic final Object NULL = new Object();and replace all use of nulls in uses of maps with NULL?
-Doug
以上信件的主要意思是,Doug Lea 认为这样设计最主要的原因是:不容忍在并发场景 下出现歧义!
ConcurrentHashMap 在源码中加入不允许插入 null (空) 值的设计 ,主要目的是
为了防止并发场景下的歧义问题。
最近有多位被问到synchronized和Lock,据说还是阿里一面的面试题。在分布式开发中,锁是控制线程的重要方式。Java提供了两种锁机制synchronized和Lock。接下来。
两者对比
synchronized和Lock都是Java中用来解决线程安全问题的一个工具,那么关于synchronized和Lock的区别,从以下4个方面来给大家来做一个详细的分析:
1.特性区别
synchronized是Java内置的一个线程同步关键字,而Lock是J.U.C包下面的一个接口,它有很多实现类,比如ReentrantLock就是它的一个实现类。
2、用法区别
synchronized可以写在需要同步的对象、方法或者是特定代的码块中。主要有两种写法,比如这样:
一种是把synchronized修饰在方法上
//控制方法
public synchronized void sync(){
}
一种是把 synchronized 修饰在代码块上
Object lock = new Object();
//控制代码块
public void sync(){
synchronized(lock){
}
}
用这种方式来控制锁的生命周期。而 Lock 控制锁的粒度是通过 lock() 和 unlock() 方法来实现的 ,以ReentrantLock 为例 ,来看这样一段代码:
Lock lock = new ReentrantLock();
public void sync(){
lock.lock(); //添加锁
//TODO 线程安全的代码
lock.unlock(); //释放锁
}
这种方式,是可以保证 lock()方法和 unlock()方法之间的代码是线程安全的。而锁的作用域,取决于Lock实例的生命周期。
Lock比synchronized在使用上相对来说要更加灵活一些。Lock可以自主地去决定什么时候加锁,什么时候释放锁。只需要调用lock()和unlock()这两个方法就可以了。需要注意的是,为了避免死锁,一般我们unlock()方法写在finally块中。
另外,Lock还提供了非阻塞的竞争锁的方法叫trylock(),这个方法可以通过返回true或者fasle来告诉当前线程是否已经有其他线程正在使用锁。
而synchronized是关键字,无法去扩展实现非阻塞竞争锁的方法。另外,synchronized只有代码块执行结束或者代码出现异常的时候才会释放锁,因此,它对锁的释放是被动的。
3、性能区别
synchronized和Lock在性能上差别不大。在实现上有一些区别,synchronized采用的是悲观锁机制,synchronized是托管给JVM执行的。在JDK1.6以后采用了偏向锁、轻量级锁、重量级锁及锁升级的方式进行优化。
而Lock用的是乐观锁机制。控制锁的代码由用于自定义,也采用CAS自旋锁进行了优化。
4、用途区别
二者在一般情况下没有什么区别,但是在非常复杂的同步应用中,建议使用Lock。因为synchronized只提供了非公平锁的实现,而Lock提供了公平所和非公平锁的机制。
公平锁是指线程竞争锁资源的时候,如果已经有其他线程正在排队或者等待锁释放,那么当前竞争锁的线程是无法去插队的。
而非公平锁就是不管是否线程再排队等待锁,它都会去尝试竞争一次锁。
一个位5年的去某东面试被一道并发编程的面试题给Pass了,说”如何中断一个正在运行中的线程?
回答这个问题之前,先来回顾一下什么线程?
Thread,线程是操作系统进行运算调度的最小单位。所以,线程是系统级别的概念。
而在Java里面实现的线程,最终的执行和调度都是由操作系统来决定的,JVM只是对操作系统层面的线程做了一层包装而已。
所以我们在Java里面调用start()方法启动一个线程的时候,只是告诉操作系统这个线程可以被执行,但是最终交给CPU来执行,是由操作系统的调度算法来决定的。
从理论上来说,要在Java层面去中断一个正在运行的线程,只能像类似于Linux里面的kill命令结束进程的方式一样,强制终止。
JavaThread的API里面虽然提供了一个stop()方法,可以强行终止线程,但是这种方式是不安全的,因为有可能线程的任务还没有完成,突然中断会导致出现运行结果不正确的问题。
要想安全的中断一个正在运行的线程,只能在线程内部埋下一个钩子,外部程序通过这个钩子来触发线程的中断命令。
因此,在Java Thread 里面提供了一个interrupt()方法,这个方法要配合isInterrupted()方法来使用,就可以实现安全地中断线程运行。
来看这段代码:
Runnable runnable = new Runnable(){
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()){
System.out.println("线程被中断了");
return ;
} else {
System.out.println("线程没有被中断");
}
}
}
};
Thread t = new Thread(runnable);
t.start();
Thread.sleep(500);
t.interrupt();
System.out.println("线程中断了,程序到这里了");
这种实现方法并不是强制中断,而是告诉正在运行的线程,你可以停止了。何时实际中断,取决于正在运行的线程,所以,它能够保证线程运行结果的安全性。