Java面试题3-多线程与并发
Java中常见的锁分类
常见的锁分类大致有:排它锁、共享锁、乐观锁、悲观锁、分段锁、自旋锁、公平锁、非公平锁、可重入锁等。
a.排他锁和共享锁
synchronized就是一个排他锁,ReentrantLock也是一个排它锁,而ReentrantReadWriteLock则是一个
读共享锁,写排他锁
b.乐观锁和悲观锁
乐观锁和悲观锁是一种锁的思想,乐观锁对并发数据修改持乐观态度,通过CAS非加锁方式尝试修改数据,而悲观锁
对数据修改持保守态度,修改都要加排它锁,也常用于和数据库隔离级别结合使用
c.分段锁
分段锁也是一种锁思想,对数据分段加锁已提高并发效率,比如jdk8之前的ConcurrentHashMap,jdk8后采用
CAS+synchronized。通过hashCode计算到索引后对数据分段加锁
d.自旋锁
通过不断的轮询来尝试获取锁,是一种占用CPU时间的非阻塞锁,当锁的等待时间短,效率会很高
e.公平锁和非公平锁
同步锁按照线程申请锁的顺序,非同步则不保证。synchronized和ReentrantLock都是非同步锁。ReentrantLock
可在构造方法里指定为同步锁
f.可重入锁
又叫递归锁,具体是当外部申请锁的操作获得了锁,内部申请锁的操作也会获得锁,有点类似与进程中破坏请求保持
原则来预防死锁一样,要么全部申请成功,要么全部失败
synchronized四种锁状态的升级
加锁可以使一段代码在同一时间只有一个线程可以访问,在增加安全性的同时,牺牲掉的是程序的执行性能,所以为了在一定程度上减少获得锁和释放锁带来的性能消耗,在 jdk6 之后便引入了“偏向锁”和“轻量级锁”,所以总共有4种锁状态,级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。
注意:锁可以升级但不能降级。
无锁状态
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
偏向锁状态
偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
轻量级锁状态
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。
轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
重量级锁状态
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,导致性能低下。
信号量 Semaphore
信号量相当于一个计数器,如果线程想要访问某个资源,则先要获得这个资源的信号量,并且信号量内部的计数器减1
,信号量内部的计数器大于0则意味着有可以使用的资源,当线程使用完某个资源时,必须释放这个资源的信号量。
信号量的一个作用就是可以实现指定个线程去同事访问某个资源。只需要在初始化 。
信号量在 Java中的实现是 Semaphore ,其在初始化时传入一个整型数, 用来指定同步资源最大的并发访问量
public class SemaphoreExample {
private static Semaphore semaphore = new Semaphore(2);
private String lock = "lock";
private static int count = 0;
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
MyThread thread3 = new MyThread();
thread1.start();
thread2.start();
thread3.start();
}
static class MyThread extends Thread{
public void run() {
try {
while (true) {
semaphore.acquire();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "get the lock success and run the syn code " + count++);
semaphore.release();
Thread.sleep(5000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如上所述同时只有两个线程被执行。
原子Atomic类,如何保证原子性,CAS硬件指令
CAS(Compare-and-Swap)指令
CAS指令需要3个操作数,分别是-内存位置 V(在Java中可以简单理解为变量的内存地址)、 旧的预期值 A
(进行运算前从内存中读取的值)、拟写入的值 B(运算得到的值)
当且仅当 V==A 时, 才执行V = B (将B赋给V),否则将不做任何操作。
模拟CAS操作:
public class CompareAndSwap{
private int value;
//获取内存值
public synchronized int get() {
return value;
}
/**
* 比较当前内存值和旧的预期值,只有两个值相等的情况,进行更新
* @param expectedValue 旧的预期值 - 在进行运算前从内存中读取的值
* @param newValue 拟写入的新值 - 运算得到的值,即拟写入内存的值
* @return
*/
public synchronized int compareAndSwap(int expectedValue, int newValue){
int oldValue = value;
//比较当前内存值和旧的预期值 如果相等,将更新值赋给内存值
if (oldValue == expectedValue) {
this.value = newValue;
}
return oldValue;
}
//设置
public synchronized boolean compareAndSet(int expectedValue, int newValue){
return expectedValue == compareAndSwap(expectedValue, newValue);
}
}
常见的使用情况是:线程首先从内存位置V中读取到预期值A,在执行计算前,比较当前内存值和旧的预期值A是否相等
,如果相等,计算得到的值赋给内存值。 不相等则说明,期间有其他线程修改了内存位置V的值。
当多个线程使用CAS同时更新一个变量值时,只有其中一个线程能够更新成功,其他的线程都将失败。但是,失败的
线程不会被挂起(但如果获取锁失败,线程将被挂起),而是返回失败状态,调用者线程可以选择是否需要再一次
尝试(如果是在一些竞争激烈的情况下,更好的方式是在重试之前等待一段时间或者回退,从而避免活锁问题–不断
重试,不断失败),或者执行一些恢复操作,也可以什么都不做。
JDK1.5后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和
compareAndSwapLong()等几个方法包装提供。但是Unsafe类不是提供给用户程序调用的 (Unsafe.getUnsafe()
代码限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问) 因此,如果不使用反射,只能通
过其他的java API来间接使用。 比如java.util.concurrent.atomic中的原子类。其中整数原子类有
compareAndSet() 和 getAndIncrement()方法都是用了Unsafe类的CAS操作。
CAS存在的问题:
1.它使调用者处理竞争问题(重试、回退、停止),而在锁中能自动处理竞争问题(获得锁之前一直阻塞)
2.循环时间长导致开销增大,如果程序中的CAS操作不断重试(自旋),会使得CPU消耗过多的执行资源。
3.只能保证一个变量的原子操作,对多个共享变量操作时,CAS无法保证操作的原子性,但是可以把多个变量合并成
一个共享变量来操作。如:有两个共享变量 i = 2, j = a,可以合并为 ij = 2a,然后使用CAS来操作。当然可已
使用基于CAS的原子类 AtomicReference 来保证对象间的原子性,将多个变量放在一个对象中进行操作。
4.如果一个变量V初次读取的时候是A值,并且准备赋值时检查到它仍然是A值,但是这段时间中,它的值可能被改为B
后又改了回来,这时CAS操作就会误认为它从来没有改变过。这就是CAS操作存在的一个漏洞“ABA”问题。而解决ABA
问题使用互斥同步可能会更有效。
给出一种ABA问题的解决思路:
在变量前面追加上版本号,每次变量更新,将版本号加1,A->B->A 将变成 1A->2B->3A
JDK1.5开始,atomic 包中提供了一个类 AtomicStampedReference 来解决这个问题.
L3 Cache,QPI,乐观锁
Synchronized关键字的作用
- 由于Synchronized 会修饰 代码块、类的实例方法 & 静态方法,故分为不同锁的类型
- 类的实例方法和代码块属于对象锁 静态方法属于类锁
Synchronized 的引入是为了完成原子性、可见性、有序性
Volatile关键字的作用
- 保证内存可见性
- 防止指令重排
此外需注意volatile并不保证操作的原子性
HashMap在多线程环境下使用需要注意什么
HashMap是非线程安全性的集合,多线程做rehash的时候会存在循环链表和丢失数据的问题,其中循环链表再查询
该元素会导致OOM.
最好使用ConcurrentHashMap代替HashMap
启动一个线程是用run()还是start()
start.
run是一个方法 直接调用run方法只是在当前主线程上执行方法
只有执行了start方法线程之后才能被系统分配时间片,多线程运行。
java多线程之启动线程的三种方式
继承Thread类
通过继承Thread类来创建并启动多线程步骤如下:
1、定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run方法称为线程执行体。
2、创建Thread子类的实例,即创建了线程对象
3、调用线程对象的start()方法来启动该线程
public class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "执行" + i);
}
}
}
实现Runnable接口
实现Runnable接口创建并启动多线程的步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体
2.创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。
3.调用线程对象的start()方法来启动该线程。
public class Thread2 implements Runnable{
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "执行" + i);
}
}
}
实现Runnable接口
实现Runnable接口创建并启动多线程的步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体
2.创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。
3.调用线程对象的start()方法来启动该线程。
public class Thread2 implements Runnable{
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "执行" + i);
}
}
}
请说明下面代码运行结果
public static void main(String[] args) {
Thread t = new Thread() {
public void run() {
func2();
}
};
t.run();
System.out.print("func1");
}
static void func2() {
System.out.print("func2");
}
运行结果
func2
func1
考察是Thread类中start()和run()方法的区别。start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程,进而调用run()方法来执行任务,而单独的调用run()就跟调用普通方法是一样的,已经失去线程的特性了。因此在启动一个线程的时候一定要使用start()而不是run()
什么是守护线程?有什么用
1、守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。
2、再换一种说法,如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。
3、守护线程又被称为“服务进程”“精灵线程”“后台线程”,是指在程序运行是在后台提供一种通用的线程,这种线程并不属于程序不可或缺的部分。 通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”
什么是死锁?如何避免
所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。
虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。目前处理死锁的方法可归结为以下四种:
1) 预防死锁。
这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。
2) 避免死锁。
该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。
3)检测死锁。
这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉。
4)解除死锁。
这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大
synchronized 和 ReentrantLock 区别是什么
- synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
- synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
- synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
- synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
- synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
- synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
- 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
线程和进程的差别是什么
进程是系统中正在运行的一个程序,程序一旦运行就是进程。
进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等
线程是进程的一个实体,是进程的一条执行路径。
线程是进程的一个特定执行路径。当一个线程修改了进程的资源,它的兄弟线程可以立即看到这种变化
进程和线程的选择取决以下几点:
1.需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程的代价是很大的。
2.线程的切换速度快,所以在需要大量计算,切换频繁时使用线程,还有耗时的操作时用使用线程可提高应用程序的响应。
3.因为对CPU系统的效率使用上线程更占优势,所以可能要发展到多机分布的用进程,多核分布用线程。
4.并行操作时用线程,如C/S架构的服务器端并发线程响应用户的请求。
5.需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。
Java里面的Threadlocal是怎样实现的
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找
ConcurrentHashMap的实现原理是什么
ConcurrentHashMap使用的分段锁技术。将ConcurrentHashMap容器的数据分段存储,每一段数据分配一个Segment(锁),当线程占用其中一个Segment时,其他线程可正常访问其他段数据。
sleep和wait区别
两者都可以让线程暂停一段时间,但是本质的区别是一个线程的运行状态控制,一个是线程之间的通讯的问题
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。
wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生
notify和notifyAll区别
先说两个概念:锁池和等待池
- 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
- 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
Wait/Notify通知机制解析
wait()方法可以使线程进入等待状态,而notify()可以使等待的状态唤醒。这样的同步机制十分适合生产者、消费者模式:消费者消费某个资源,而生产者生产该资源。当该资源缺失时,消费者调用wait()方法进行自我阻塞,等待生产者的生产;生产者生产完毕后调用notify/notifyAll()唤醒消费者进行消费。
两个线程如何串行执行
/**
* 串行执行多线程任务 第一种 使用线程池
*/
//任务队列。android 提供有:LinkedBlockingQueue ArrayQueue ArrayBlockQueue 区别自己百度吧。
LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
//线程工厂
ThreadFactory threadFactory = new ThreadFactory(){
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r);
}
};
//设置一个核心线程数为1,最大线程数为5,任务队列最大容量为100,闲置关闭时间为1秒的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 5, 1l, java.util.concurrent.TimeUnit.SECONDS, workQueue, threadFactory);
public void executeByExecutor(Runnable runnable){
threadPoolExecutor.execute(runnable);
}
上下文切换是什么含义
支持多任务处理是CPU设计史上最大的跨越之一。在计算机中,多任务处理是指同时运行两个或多个程序。从使用者的角度来看,这看起来并不复杂或者难以实现,但是它确实是计算机设计史上一次大的飞跃。在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成切换操作。
在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
在三种情况下可能会发生上下文切换:中断处理,多任务处理,用户态切换。在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换。对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的
可以运行时kill掉一个线程吗
一般我们把需要较长时间处理的任务放在线程中处理,为更好的用户体验,正在运行的任务最好能取消,如用户误操作(重复导入大量数据)。本文介绍如何暂停java 线程————不是简单使用Thread.stop方法,已标记为不建议使用,详细解释请参考官方文档。
使用标志位
首先我们创建一个类,其负责创建和启动线程。因为线程任务不能自己结束,所以定义结束线程的方式,这里我们使用原子类型标志(atomic)代替使用常量true进行while循环,我们使用AtomicBoolean作为while条件,现在我们可以通过设置该条件true/false实现启动或停止线程。AtomicBoolean可以保障不同线程之间同步。
public class ControlSubThread implements Runnable {
private Thread worker;
private final AtomicBoolean running = new AtomicBoolean(false);
private int interval;
public ControlSubThread(int sleepInterval) {
interval = sleepInterval;
}
public void start() {
worker = new Thread(this);
worker.start();
}
public void stop() {
running.set(false);
}
public void run() {
running.set(true);
while (running.get()) {
try {
Thread.sleep(interval);
} catch (InterruptedException e){
Thread.currentThread().interrupt();
System.out.println(
"Thread was interrupted, Failed to complete operation");
}
// do something here
}
}
}
使用interrupt方法中断线程
在上面实现方式中,如果遇到sleep被设置为很长时间,或等待锁永远不释放,则线程一直被阻止或线程永远不终止(一致在while循环体内)。我们可以使用interrupt方法解决这中场景。
public void interrupt() {
running.set(false);
worker.interrupt();
}
我们增加了interrupt()方法,其负责设置running标志位为false并调用worker线程的interrupt方法,如果正在sleep的线程调用该方法,sleep方法会结束并抛出InterruptedException异常,与其他的阻塞调用一样。然后回到循环内,因为running为false,任务会结束
什么是协程
https://www.sohu.com/a/236536167_684445
协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
既然协程这么好,它到底是怎么来使用的呢?
由于Java的原生语法中并没有实现协程(某些开源框架实现了协程,但是很少被使用),所以我们来看一看python当中对协程的实现案例,同样以生产者消费者模式为例:
这段代码十分简单,即使没用过python的小伙伴应该也能基本看懂。
代码中创建了一个叫做consumer的协程,并且在主线程中生产数据,协程中消费数据。
其中 yield 是python当中的语法。当协程执行到yield关键字时,会暂停在那一行,等到主线程调用send方法发送了数据,协程才会接到数据继续执行。
但是,yield让协程暂停,和线程的阻塞是有本质区别的。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。
因此,协程的开销远远小于线程的开销