Java基础:多线程与线程池

多线程

线程调度

线程状态

线程是cpu任务调度的最小执行单位,每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈。

线程状态:创建、就绪、运行、阻塞、死亡

状态切换

阻塞唤醒过程

阻塞:

  • sleep( ) 使线程在一定的时间内进入阻塞状态,不能得到cpu时间,但不会释放锁资源。指定的时间一过,线程重新进入可执行状态
  • wait( ) 使线程进入阻塞状态,同时释放自己占有的锁资源,和notify( )搭配使用
  • suspend( ) 使线程进入阻塞状态,并且不会自动恢复,必须其对应的resume( )被调用,才能使线程重新进入可执行状态

区别:

yield( ) 使得线程放弃当前分得的CPU时间,但是不使线程阻塞,即线程任然处于可执行状态,随时可能再次分得CPU时间。

其中sleep(),suspend(),rusume(),yield()均为Thread类的方法,wait()为Object类的方法

唤醒:

  • 线程将会从等待队列中移除,重新成为可调度线程。它会与其他线程以常规的方式竞争对象同步请求。一旦它重新获得对象的同步请求,所有之前的请求状态都会恢复,也就是线程调用wait的地方的状态。线程将会在之前调用wait的地方继续运行下去。

wait()方法为什么要出现在同步代码块中:

由于wait()属于Object方法,调用之后会强制释放当前对象锁,所以在wait() 调用时必须拿到当前对象的监视器monitor对象。因此,wait()方法在同步方法/代码块中调用。

wait和sleep区别

  • wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
  • wait 方法会主动释放 monitor 锁,在同步代码中执行 sleep 方法时,并不会释放 monitor 锁。
  • wait 方法意味着永久等待,直到被中断或被唤醒才能恢复,不会主动恢复,sleep 方法中会定义一个时间,时间到期后会主动恢复。
  • wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

创建线程方式

实现 Runnable 接口(优先使用)

步骤:

1.创建一个实现了Runnable接口的类

2.实现类去实现Runnable中的抽象方法:run()

3.创建实现类的对象

4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

5.通过Thread类的对象调用start()

① 启动线程

②调用当前线程的run()–>调用了Runnable类型的target的run()

public class RunnableThread implements Runnable {    
    @Override    
    public void run() {
        System.out.println('用实现Runnable接口实现线程');
    }
}

实现Callable接口(有返回值可抛出异常)

步骤:

1.创建一个实现Callable的实现类

2.实现call方法,将此线程需要执行的操作声明在call()中

3.创建Callable接口实现类的对象

4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象

5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()

6.获取Callable中call方法的返回值

实现Callable接口的方式创建线程的强大之处

  • call()可以有返回值的
  • call()可以抛出异常,被外面的操作捕获,获取异常的信息
  • Callable是支持泛型的
class CallableTask implements Callable<Integer> {    
    @Override    
    public Integer call() throws Exception { 
        return new Random().nextInt();
    }
}

继承Thread类(java不支持多继承)

1.创建一个继承于Thread类的子类
2.重写Thread类的run() --> 将此线程执行的操作声明在run()中
3.创建Thread类的子类的对象
4.通过此对象调用start()执行线程

public class ExtendsThread extends Thread {    
    @Override    
    public void run() {
        System.out.println('用Thread类实现线程');                      
    }
}

使用线程池(底层都是实现run方法)

static class DefaultThreadFactory implements ThreadFactory {    
    DefaultThreadFactory() {        
        SecurityManager s = System.getSecurityManager();        
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();        
        namePrefix = "pool-" + poolNumber.getAndIncrement() +"-thread-";    
    }    
    public Thread newThread(Runnable r) {        
        Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);        
        if (t.isDaemon()) t.setDaemon(false);  //是否守护线程        
        if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); //线程优先级       
        return t;    
    }
}

runnable和callable比较:

  • 开发中优先选择实现Runnable接口的方式
  • 原因:
    (1)实现的方式没有类的单继承性的局限性
    (2)实现的方式更适合来处理多个线程有共享数据的情况
  • 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中

线程安全

乐观锁和CAS(需要继续完善)

java乐观锁机制:

乐观锁体现的是悲观锁的反面。它是一种积极的思想,它总是认为数据是不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被更新过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。在Java中 java.util.concurrent.atomic下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁,大多是基于数据版本 (Version)记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来 实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据 版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

CAS思想:

CAS就是compare and swap(比较交换),是一种很出名的无锁的算法,就是可以不使用锁机制实现线程间的同步。使用CAS线程是不会被阻塞的,所以又称为非阻塞同步。CAS算法涉及到三个操作:

需要读写内存值V;进行比较的值A;准备写入的值B

当且仅当V的值等于A的值等于V的值的时候,才用B的值去更新V的值,否则不会执行任何操作(比较和替换是一个原子操作-A和V比较,V和B替换),一般情况下是一个自旋操作,即不断重试。

缺点:https://www.zhihu.com/question/23281499/answer/854522984

ABA问题-知乎

高并发的情况下,很容易发生并发冲突,如果CAS一直失败,那么就会一直重试,浪费CPU资源

原子性:

功能限制CAS是能保证单个变量的操作是原子性的,在Java中要配合使用volatile关键字来保证线程的安全;当涉及到多个变量的时候CAS无能为力;除此之外CAS实现需要硬件层面的支持,在Java的普通用户中无法直接使用,只能借助atomic包下的原子类实现,灵活性受到了限制

CAS 操作通常包括三个参数:需要修改的变量 V、旧的期望值 A 和新的值 B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS实现原理

实现原理,如下图所示:

如上图显示线程1和线程2同时修改值,如果通过CAS 来实现。

具体流程如下:

  1. 在内存地址V当中,存储着值为7的变量;
  2. 线程1想要把变量的值增加1,对线程1来说,旧的预期值A=7,要修改的新值B=8;
  3. 线程2抢先一步,把内存值V修改:8;
  4. 线程1开始提交更新,首先对比了预期值A=7和实际值V的比较8(Compare),发现A不等于V的实际值,提交失败;

synchronized的使用方法

使用方法:主要的三种使⽤⽅式

修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁

修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员。

修饰代码块: 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。

总结:synchronized锁住的资源只有两类:一个是对象,一个是

synchronized的底层实现,什么是锁的升级、降级?

synchronized 底层如何实现?

synchronized 同步代码块是由一对儿 monitorenter/monitorexit 指令实现的,而同步方法则是利用 flags 实现的。Monitor 对象是同步的基本实现单元。对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息。

在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。

现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。

synchronized是可重入锁
当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。
在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。

注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。
这个就是可重入锁的实现原理:《可重入锁 介绍以及原理》

monitor(管程)是如何实现的?

管程是一种解决并发问题的模型。

所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。在管程模型里面,共享变量和对共享变量的操作是被封装起来。当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。管程里还引入了条件变量的概念,而且每个条件变量都对应一个条件变量等待队列。条件变量和条件变量等待队列的作用是什么呢?其实就是解决线程同步问题。

假设有个线程T1执行则色队列的出队操作,出队操作需要有个前提条件,就是阻塞队列不能是空的,阻塞队列不空这个前提就是对应管程里面的条件变量。当阻塞队列不空这个条件不满足时,T1要调用A.wait()在条件变量A的等待队列中等待。

再假设一个线程T2执行阻塞队列的入队操作,入队操作执行成功之后,“阻塞队列不空”这个条件对于线程T1来说已经满足了,此时线程T2要调用A.notify()来通知A等待队列中的一个线程。也可以使用notifyAll()方法。

在管程的发展史上,先后出现过三种不同的管程模型,分别是:Hasen 模型、Hoare 模型和 MESA 模型。其中,现在广泛应用的是 MESA 模型,并且 Java 管程的实现参考的也是 MESA 模型。语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。具体如下图所示。

Java用两种方式实现了管程:

  1. synchronized+wait、notify、notifyAll。synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量,即wait(),调用wait()时会将其加入到等待队列中,被notify时,会随机通知一个线程加入到获取锁的等待队列中。
  2. lock+内部的condition。支持中断和增加了时间的等待。lock需要自己进行枷锁解锁,更加灵活,两个都是可重入锁,但是lock支持公平和非公平锁,synchronized支持非公平锁。

总结起来就是,管程就是一个对象监视器。任何线程想要访问该资源,就要在入口等待队列排队进入监控范围。进入之后,接受检查,不符合条件,则要在条件变量等待队列继续等待,直到被通知,然后继续进入入口等待队列排队。

ReenTrantLock的使用方法和底层实现

由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能

使用方法:

基于API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

底层实现:

在具体实现上,ReentrantLock是基于AQS来实现,AQS全名:AbstractQueuedSynchronizer,AQS是一个用于实现同步器的类。

ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

具体实现,大致分为如下几点:

  1. ReentrantLock在调用lock()方法时,会首先判断当前线程是否已经持有锁;
  2. 如果是,则将锁的重入次数加1,并返回;
  3. 否则,将当前线程加入到等待队列中,等待获取锁。

这里我们来讲一下 ReentrantLock 底层加锁的原理

其中 ReentrantLock 底层加锁主要是依靠于 AQS(AbstractQueuedSynchronizer) 来做的,AQS 是 JUC 包下的基础工具类

从名字就可以看出来 AQS 是一个同步器,用于 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">管理多线程环境下获取锁</font> 的问题,接下来会介绍 ReentrantLock 底层原理 以及 AQS 细节!

6K+字 ReentrantLock 原理全面详解!

ReenTrantLock和synchronized区别

1、底层实现:synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层****面的锁。

2、实现原理synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁ReentrantLock实现则是通过利用CAS(CompareAndSwap)**自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

3、是否可手动释放:synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用;ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象

4、是否可中断synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成;ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

5、是否公平锁synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁,公平锁性能非常低。

ReentrantLock应用

ReentrantLock适用于需要高度控制的并发环境,其应用场景包括但不限于以下5种:

  1. 需要实现公平或非公平的锁机制:ReentrantLock可以设置公平或非公平锁,以满足不同的需求。
  2. 需要限制等待锁的时间:ReentrantLock可以设置等待锁的时间,如果等待时间超过设定值,等待的线程就可以自动放弃锁的获取。
  3. 需要使用条件变量:ReentrantLock可以使用Condition对象来实现多个条件变量,可以更加精细地控制线程的等待和通知。
  4. 需要避免死锁:由于ReentrantLock支持可重入性,同一个线程可以多次获取同一把锁,这可以避免死锁的发生。
  5. 需要精确控制锁的释放:使用synchronized关键字时,锁的释放是由JVM自动控制的,而使用ReentrantLock可以手动控制锁的释放,以达到更精确的控制效果。

公平锁和非公平锁区别

公平锁:

公平锁自然是遵循FIFO(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待。

非公平锁:

多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

公平锁的特征:

  • 严格按请求顺序分配资源。
  • 保证了等待时间最长的线程最先获得锁。
  • 防止了线程饥饿(长时间得不到执行)。

使用公平锁的时候:

  • 当你在乎每个线程都要公平获得资源时。
  • 想要避免线程因为等待时间过长而处于饥饿状态。

非公平锁的特征:

  • 可能不按请求顺序分配资源。
  • 性能较好,因为不需要维护一个等待队列。
  • 但有可能导致一些线程等待时间过长,即出现线程饥饿。

使用非公平锁的时候:

  • 当你更在乎整体性能,而不太在乎部分线程等待时间。
  • 当系统中有大量短小任务需要迅速处理时。

公平锁效率低原因:

公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次挂起和唤醒。

线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

使用层面锁优化

【1】减少锁的时间:
不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;

【2】减少锁的粒度:
它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率,比如:

ConcurrentHashMap:

java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组:Segment< K,V >[] segments

Segment继承自ReenTrantLock,所以每个Segment是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。

【3】锁粗化:
大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;

假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;

【4】使用读写锁:

ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可并发读,写操作使用写锁,只能单线程写;

【5】使用CAS:

如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择;

系统层面锁优化

自适应自旋锁:

自旋锁可以避免等待竞争锁进入阻塞挂起状态被唤醒造成的内核态和用户态之间的切换的损耗,它们只需要等一等(自旋),但是如果锁被其他线程长时间占用,一直不释放CPU,死等会带来更多的性能开销;自旋次数默认值是10。

对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

锁消除:

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。Netty中无锁化设计pipeline中channelhandler会进行锁消除的优化。

锁升级:

偏向锁:

如果线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些monitor操作,因为在大部分情况下是没有竞争的,所以使用偏向锁是可以提高性能的;

轻量级锁:

在竞争不激烈的情况下,通过CAS避免线程上下文切换,可以显著的提高性能。

重量级锁:

重量级锁的加锁、解锁过程造成的损耗是固定的,重量级锁适合于竞争激烈、高并发、同步块执行时间长的情况。

ThreadLocal原理

ThreadLocal简介:

通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的。

专属本地变量该如何解决呢?JDK中提供的 ThreadLocal 类正是为了解决这样的问题。类似操作系统中的TLAB。

原理:

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 。

如何使用:

1)存储用户Session

private static final ThreadLocal threadSession = new ThreadLocal();

2)解决线程安全的问题

private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>()

ThreadLocal内存泄漏的场景

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。假如我们不做任何措施的话,value 永远⽆法被GC 回收,如果线程长时间不被销毁,可能会产⽣内存泄露。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。

HashMap线程安全

死循环造成 CPU 100%

HashMap 有可能发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

所以综上所述,HashMap 是线程不安全的,在多线程使用场景中推荐使用线程安全同时性能比较好的 ConcurrentHashMap。

String不可变原因

  1. 可以使用字符串常量池,多次创建同样的字符串会指向同一个内存地址;
  2. 可以很方便地用作 HashMap 的 key。通常建议把不可变对象作为 HashMap的 key;
  3. hashCode生成后就不会改变,使用时无需重新计算;
  4. 线程安全,因为具备不变性的对象一定是线程安全的;

Java并发包提供了哪些并发工具类?

  • 各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。
  • 各种并发队列实现,如各种 BlockingQueue 实现,比较典型的 ArrayBlockingQueue、 SynchronousQueue 或针对特定场景的 PriorityBlockingQueue 等。
  • 强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。
  • 提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。(有拓展:https://time.geekbang.org/column/article/9373

内存模型

Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了 Java 程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

JMM 是一种规范,是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。

原子性:

在 Java 中,为了保证原子性,提供了两个高级的字节码指令 Monitorenter 和 Monitorexit。这两个字节码,在 Java 中对应的关键字就是 Synchronized。因此,在 Java 中可以使用 Synchronized 来保证方法和代码块内的操作是原子性的。

可见性:

Java 中的 Volatile 关键字修饰的变量在被修改后可以立即同步到主内存。被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用 Volatile 来保证多线程操作时变量的可见性。除了 Volatile,Java 中的 Synchronized 和 Final 两个关键字也可以实现可见性。只不过实现方式不同

有序性:

在 Java 中,可以使用 Synchronized 和 Volatile 来保证多线程之间操作的有序性。区别:Volatile 禁止指令重排。Synchronized 保证同一时刻只允许一条线程操作。

volatile底层实现

Volatile

一、<font style="color:rgb(221, 17, 68);">volatile</font>关键字的作用

<font style="color:rgb(221, 17, 68);">volatile</font>关键字在Java多线程编程中主要用于以下两个方面:

1. 保证可见性:当一个共享变量被<font style="color:rgb(221, 17, 68);">volatile</font>修饰时,它可以保证该变量修改的值会立即被更新到主内存中,当有其他线程需要读取该变量时,它会去主内存中获取最新的值,而不是使用本地缓存。这样可以确保所有线程都看到最新的变量值。

2. 禁止指令重排序:在多线程环境下,由于指令重排序和处理器管线化的原因,指令的执行顺序可能会发生变化。但是,使用<font style="color:rgb(221, 17, 68);">volatile</font>关键字可以防止这种情况的发生,确保指令按照程序顺序执行。

二、<font style="color:rgb(221, 17, 68);">volatile</font>保证线程可见性的原理

<font style="color:rgb(221, 17, 68);">volatile</font>关键字通过以下机制来保证线程可见性:

  1. 1. 内存屏障<font style="color:rgb(221, 17, 68);">volatile</font>变量的读操作和写操作都会插入内存屏障。写屏障会确保在写操作之前的所有本地内存写操作都会刷新到主内存中,而读屏障会确保在读操作之后的所有本地内存读操作都会从主内存中重新读取。这样,当一个线程修改了<font style="color:rgb(221, 17, 68);">volatile</font>变量的值后,其他线程在读取该变量时,就会看到最新的值。
  2. 2. 禁止缓存<font style="color:rgb(221, 17, 68);">volatile</font>变量会禁止线程私有内存空间的使用。在Java内存模型中,每个线程都有自己的工作内存,用于存储共享变量的副本。但是,当变量被<font style="color:rgb(221, 17, 68);">volatile</font>修饰后,线程在访问该变量时,会直接访问主内存,而不是使用本地缓存。这样可以确保所有线程都看到最新的变量值。

三、<font style="color:rgb(221, 17, 68);">volatile</font>的使用场景和局限性

<font style="color:rgb(221, 17, 68);">volatile</font>关键字适用于以下场景:

  • • 一个线程写,多个线程读的场景。
  • • 变量不需要保证原子性操作的场景。

然而,<font style="color:rgb(221, 17, 68);">volatile</font>关键字也有其局限性:

  • • 它不能保证原子性。如果一个共享变量的操作是由多个操作组成的,就不能使用<font style="color:rgb(221, 17, 68);">volatile</font>关键字来保证其原子性。这时需要使用<font style="color:rgb(221, 17, 68);">synchronized</font>关键字或原子类(如<font style="color:rgb(221, 17, 68);">java.util.concurrent.atomic</font>包中的类)来保证操作的原子性。
  • • 它不能保证有序性。在多线程环境下,如果多个线程同时访问同一个变量,并且这些访问涉及到复杂的操作顺序,那么即使使用了<font style="color:rgb(221, 17, 68);">volatile</font>关键字,也不能保证这些操作会按照代码的顺序执行。这时需要使用<font style="color:rgb(221, 17, 68);">synchronized</font>关键字或显式锁来控制访问顺序。

volatile和synchronized的区别

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

AQS思想

AQS 全称为 AbstractQueuedSynchronizer,是java并发包的核心组件,AQS的主要作用是为Java中的并发同步组件提供统一的底层支持。比如大家熟知的:ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、ReentrantReadWriteLock 等并发类均是基于AQS来实现的。

AQS核⼼思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。

如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是⽤CLH(虚拟的双向队列)队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。

AQS 的实现:

  1. 使用一个Volatile的int类型的成员变量来表示同步状态
  2. 使用一个FIFO队列来完成资源获取的排队工作
  3. 使用CAS来原子性地修改同步状态值

AQS的主要方法:

acquire(int):独占式获取同步状态

release(int):独占式释放同步状态

acquireShared(int):共享式获取同步状态

releaseShared(int):共享式释放同步状态

AQS 原理的实现基于一个双向链表,这个链表中的每个节点都代表一个等待线程。

lock:

是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。默认为非公平锁,但可以初始化为公平锁;通过方法 lock()与 unlock()来进行加锁与解锁操作;

CountDownLatch:

通过计数法(倒计时器),让一些线程堵塞直到另一个线程完成一系列操作后才被唤醒;该⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结束,再开始执⾏。具体可以使用countDownLatch.await()来等待结果。多用于多线程信息汇总。

CompletableFuture:

通过设置参数,可以完成CountDownLatch同样的多平台响应问题,但是可以针对其中部分返回结果做更加灵活的展示。

CyclicBarrier:

字面意思是可循环(Cyclic)使用的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。可以用于批量发送消息队列信息、异步限流。

Semaphore:

信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个用于并发线程数的控制。SpringHystrix限流的思想

AQS

// AQS 队列头结点
private transient volatile Node head;

// AQS 队列阻塞尾节点
private transient volatile Node tail;

// 当前锁的状态,0:没有被占用,大于 0 代表有线程持有当前锁
// 当 state > 1 时,表锁被重入了,每次重入都加上 1
private volatile int state;

// 代表当前持有独占锁的线程
private transient Thread exclusiveOwnerThread;

AQS 的 state 状态

AQS 的 state 状态代表锁是否被占用

  • 如果 AQS 的 state 状态为 0 表示当前锁没有被占用
  • 如果 AQS 的 state 状态 > 0 表示当前锁被占用

为什么 > 0 是被占用呢?

因为可能会发生锁的重入,每次重入会给 state + 1

线程通过 CAS 抢占锁

那么线程来抢占锁,就是通过 CAS 来更新 state 状态,由 0 更改为 1,才算抢锁成功

当没有抢到锁的线程,会被封装为 Node 节点进入 AQS 的队列等待,该节点是由前边一个节点来进行 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">唤醒</font>

AQS 中 Node 的数据结构

AQS 中的 Node 就是对线程的封装,等待锁的线程封装为 Node 进入队列排队,数据结构如下:

// 当前节点的等待状态
volatile int waitStatus;
// 前继指针
volatile Node prev;
// 后继指针
volatile Node next;
// 当前节点中的线程
volatile Thread thread;
// Condition Queue 中的内容,这里不介绍
Node nextWaiter;

waitStatus 的状态有以下几个,各自的含义不同:

/***********waitStatus 的取值定义***********/
// 表示此线程取消了争抢这个锁请求
static final int CANCELLED =  1;
// 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒
static final int SIGNAL    = -1;
// 表示节点在等待队列中,节点线程等待唤醒
static final int CONDITION = -2;
// 当前线程处在SHARED情况下,该字段才会使用
static final int PROPAGATE = -3;

AQS 的作用

上边说了 AQS 是 JUC 的基础工具类,ReentrantLock 就是基于 AQS 来写的

那么我们也可以基于 AQS 来实现一个同步工具,如下 Lock 来源为美团技术团队案例代码:

public class LeeLock  {

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire (int arg) {
            return compareAndSetState(0, 1);
        }

        @Override
        protected boolean tryRelease (int arg) {
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively () {
            return getState() == 1;
        }
    }
    
    private Sync sync = new Sync();
    
    public void lock () {
        sync.acquire(1);
    }
    
    public void unlock () {
        sync.release(1);
    }
}

同步工具使用:

public class LeeMain {

    static int count = 0;
    static LeeLock leeLock = new LeeLock();

    public static void main (String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run () {
                try {
                    leeLock.lock();
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    leeLock.unlock();
                }

            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

CAS和AQS的区别

CAS 和 AQS 都是Java多线程并发编程中实现线程安全的重要机制,但 CAS 更偏重于实现无锁算法,而 AQS 更多地被用于实现锁和同步机制。

如何设计一个阻塞队列,具体设计逻辑是什么,细节,阻塞,挂起、通知、唤醒

阻塞队列的适用场景

阻塞队列通常用于生产者-消费者模式。在这种模式下,生产者线程负责生成数据并将其放入队列中,消费者线程则从队列中取出数据进行处理。当队列满时,生产者会被阻塞,直到消费者消耗掉一些数据;而当队列为空时,消费者会被阻塞,直到生产者提供数据。

阻塞队列的核心思路

阻塞队列核心思路:

  • 队列容量管理:用数组来存储队列元素,维护一个容量上限。
  • 线程同步:利用 wait() ****notifyAll() 来控制线程的阻塞与唤醒。
  • 双指针操作:使用两个指针 headtail 分别表示出队列的头和尾,进行插入和删除操作。

阻塞队列的实现

  • 定义基本结构

使用数组来存储元素,capacity 是队列的最大容量,size 用来记录队列中元素的数量,headtail 分别指向队列的头和尾。

  • 实现put()

put() 方法用于将元素放入队列中。如果队列已满,那么当前线程将被阻塞,直到队列有空位为止。我们使用 synchronized 来确保线程安全。当队列满了,调用 wait() 阻塞当前线程,直到有空间时被唤醒。将元素插入队列后,使用 notifyAll() 唤醒其他等待的线程。

  • 实现take()

take() 方法用于从队列中取出元素。如果队列为空,那么当前线程将被阻塞,直到有元素可取。与 put()**** 类似,take() 会在队列为空时阻塞当前线程,直到有元素可以取出。

如何使用阻塞队列实现生产者-消费者模型

生产者-消费者模型是多线程编程中的经典问题,它描述了两个线程之间的协作关系:生产者负责生成数据并将其放入缓冲区,而消费者则从缓冲区中取出数据进行处理。阻塞队列可以简化这一过程,因为它自动处理了线程间的同步和等待机制。

实现步骤:
  1. 1. 创建阻塞队列:首先需要创建一个阻塞队列实例,比如Java中的<font style="color:rgb(221, 17, 68);">ArrayBlockingQueue</font><font style="color:rgb(221, 17, 68);">LinkedBlockingQueue</font>。这些队列具有固定或可变的容量,并且提供了线程安全的入队和出队操作。
  2. 2. 生产者线程:生产者线程负责生成数据并将其放入阻塞队列中。当队列已满时,生产者线程会被阻塞,直到队列中有空闲位置。这样可以避免生产者线程因缓冲区满而无限期等待。
  3. 3. 消费者线程:消费者线程从阻塞队列中取出数据进行处理。当队列为空时,消费者线程会被阻塞,直到队列中有新的元素加入。这样可以确保消费者不会尝试从空队列中获取数据。
  4. 4. **解耦合与**异步性:使用阻塞队列可以实现生产者和消费者之间的解耦合和异步性。生产者不需要等待消费者处理完数据,也不需要知道消费者何时会消费数据;同样,消费者也不需要知道生产者何时会生成数据。

死锁

一组互相竞争资源的线程因互相等待,导致永久阻塞的现象。

死锁发生的四个条件

  • 互斥
  • 占有且等待
  • 不可抢占
  • 循环等待

破坏死锁

  • 破坏占有且等待。一次性申请全部的资源
  • 破坏不可抢占。申请不到资源就主动释放已经占有的资源
  • 破坏循环等待。给资源加序号。

线程池

什么是线程池?为什么要使用线程池?

计算机发展到现在,摩尔定律在现有工艺水平下已经遇到难易突破的物理瓶颈,通过多核 CPU 并行计算来提升服务器的性能已经成为主流,随之出现了多线程技术。

线程作为操作系统宝贵的资源,对它的使用需要进行控制管理,线程池是一种线程复用技术,它维护着多个线程等待监督管理者分配可并发执行的任务。处理过程中,将任务添加到队列,然后在线程创建后自动启动这些任务。

JUC 给我们提供了 ThreadPoolExecutor 体系类来帮助我们更方便的管理线程、并行执行任务。

下图是 Java 线程池继承体系:

顶级接口Executor提供了一种方式,解耦任务的提交和执行,只定义了一个 execute(Runnable command) 方法用来提交任务,至于具体任务怎么执行则交给他的实现者去自定义实现。

ExecutorService 接口继承 Executor,且扩展了生命周期管理的方法、返回 Futrue 的方法、批量提交任务的方法。

AbstractExecutorService 抽象类继承 ExecutorService 接口,对 ExecutorService 相关方法提供了默认实现,用 RunnableFuture 的实现类 FutureTask 包装 Runnable 任务,交给 execute() 方法执行,然后可以从该 FutureTask 阻塞获取执行结果,并且对批量任务的提交做了编排。

ThreadPoolExecutor 继承 AbstractExecutorService,采用池化思想管理一定数量的线程来调度执行提交的任务,且定义了一套线程池的生命周期状态,用一个 ctl 变量来同时保存当前池状态(高3位)和当前池线程数(低29位)。看过源码的小伙伴会发现,ThreadPoolExecutor 类里的方法大量有同时需要获取或更新池状态和池当前线程数的场景,放一个原子变量里,可以很好的保证数据的一致性以及代码的简洁性,说到 ctl 了,可以顺便讲下几个状态之间的流转过程。

使用线程池可以带来以下好处:

  1. 降低资源消耗。降低频繁创建、销毁线程带来的额外开销,复用已创建线程
  2. 降低使用复杂度。将任务的提交和执行进行解耦,我们只需要创建一个线程池,然后往里面提交任务就行,具体执行流程由线程池自己管理,降低使用复杂度
  3. 提高线程可管理性。能安全有效的管理线程资源,避免不加限制无限申请造成资源耗尽风险
  4. 提高响应速度。任务到达后,直接复用已创建好的线程执行

ThreadPoolExecutor 都有哪些核心参数?execute()方法执行逻辑?

其实一般面试官问你这个问题并不是简单听你说那几个参数,更多的是想听你描述下线程池执行流程。

包含核心线程数(corePoolSize)、最大线程数(maximumPoolSize),空闲线程超时时间(keepAliveTime)、时间单位(unit)、阻塞队列(workQueue)、拒绝策略(handler)、线程工厂(ThreadFactory)这7个参数。

回答完包含这几个参数之后,会再主动描述下线程池的执行流程,也就是 execute() 方法执行流程。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

execute()方法执行逻辑如下:

  1. 判断线程池的状态,如果不是RUNNING状态,直接执行拒绝策略
  2. 如果当前线程数 < 核心线程池,则新建一个线程来处理提交的任务
  3. 如果当前线程数 > 核心线程数且任务队列没满,则将任务放入阻塞队列等待执行
  4. 如果 核心线程池 < 当前线程池数 < 最大线程数,且任务队列已满,则创建新的线程执行提交的任务
  5. 如果当前线程数 > 最大线程数,且队列已满,则执行拒绝策略拒绝该任务

线程池管理器要判断是否进行线程回收

  • 对于非核心线程,如果它们在指定的存活时间(keepAliveTime)内没有执行任务,那么这些线程将会被回收。
  • 当线程池处于SHUTDOWN状态时,它会停止接收新任务,但会继续处理已添加的任务。此时,空闲线程会被中断,线程池会逐步回收这些线程。
  • 当所有任务都已终止,且线程池中的线程数量已经减少到0时,线程池会进入TIDYING状态,并执行钩子函数terminated()。之后,线程池会进入TERMINATED状态,表示线程池已经彻底关闭。

【进阶部分】

在回答完包含哪些参数及 execute 方法的执行流程后。然后可以说下这个执行流程是 JUC 标准线程池提供的执行流程,主要用在 CPU 密集型场景下。

像 Tomcat、Dubbo 这类框架,他们内部的线程池主要用来处理网络 IO 任务的,所以他们都对 JUC 线程池的执行流程进行了调整来支持 IO 密集型场景使用。

他们提供了阻塞队列 TaskQueue,该队列继承 LinkedBlockingQueue,重写了 offer() 方法来实现执行流程的调整。【未完】

以面试官视角万字解读线程池10大经典面试题!

如果让你设计一个线程池,那么这个线程池应该有哪些核心参数?

ThreadPoolExecutor的基础上增加两个参数:

是否允许核心线程超时(Allow Core Threads Time-Out)

- <font style="color:rgb(5, 7, 59);">一个布尔值,指定核心线程是否也受线程存活时间的影响。如果设置为true,则核心线程在空闲时间超过线程存活时间后也会被终止。</font>

任务优先级队列(Optional: Priority Work Queue)

- <font style="color:rgb(5, 7, 59);">如果任务有不同的优先级,可以使用优先级队列来存储任务。这样,线程池会按照任务的优先级顺序来执行任务。</font>

Worker 继承 AQS 实现了锁机制,那 ThreadPoolExecutor 都用到了哪些锁?为什么要用锁?

以面试官视角万字解读线程池10大经典面试题!

你在项目中是怎样使用线程池的?Executors 了解吗?为什么不建议使用executors来创建线程池?

怎么使用线程池?

遵循阿里巴巴 Java 开发规范,该规范里明确说明不允许使用 Executors 创建线程池,而是通过 ThreadPoolExecutor 显示指定参数去创建。

executor类实现线程池

  • newSingleThreadExecutor():只有一个线程的线程池,任务是顺序执行,适用于一个一个任务执行的场景
  • newCachedThreadPool():线程池里有很多线程需要同时执行,60s内复用,适用执行很多短期异步的小程序或者负载较轻的服务
  • newFixedThreadPool():拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,适用执行长期的任务。
  • newScheduledThreadPool():用来调度即将执行的任务的线程池
  • newWorkStealingPool():底层采用forkjoin的Deque,采用独立的任务队列可以减少竞争同时加快任务处理

为什么不建议executors来创建线程?

在开发中不允许使用Executors去创建线程池,而是通过ThreadPoolExecutor的方式,这样可以避免资源耗尽的风险。原因是:

  • FixedThreadPool和SingleThreadPool:创建的线程池内部使用的是无界(Integer.MAX_VALUE)的 LinkedBlockingQueue 队列,可能会堆积大量请求,导致 OOM。
  • CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE.

线程池核心参数设置多少合适呢?

考虑因素

  1. 任务特性
    • CPU密集型任务:这类任务主要耗费CPU资源,因此核心线程数通常设置为CPU可用核心的数量,以使CPU的利用率最大化。
    • IO密集型任务:这类任务大部分时间都在等待IO操作完成,因此核心线程数可以设置得更高,以充分利用CPU在等待IO操作时的空闲时间。通常,线程数会多于CPU核心数。
    • 混合型任务:这类任务既有计算又有IO操作,需要综合考虑两种任务的比例来决定核心线程数。
  2. 系统资源
    • CPU核心数:系统的CPU核心数是限制线程数的重要因素。过多的线程可能导致CPU资源过度竞争,降低系统性能。
    • 内存容量:内存资源也是需要考虑的因素。过多的线程会占用更多的内存,可能导致内存不足的问题。
  3. 应用性能需求
    • 吞吐量:如果应用需要处理大量的并发任务,那么可能需要更多的线程来提高吞吐量。
    • 响应速度:对于需要快速响应的应用,合理的线程数可以确保任务被及时处理。
  4. 任务执行时间和到达频率
    • 如果任务执行时间较长,那么可能需要较少的线程数,因为每个线程会占用较长时间。
    • 如果任务到达频率较高,那么可能需要更多的线程来确保任务能够及时被处理。

确定方法

  1. 理论预估
    • 对于CPU密集型任务,可以使用公式
      • “核心线程数 = CPU核心数”或
      • “核心线程数 = CPU核心数 + 1”进行预估。
    • 对于IO密集型任务,可以使用公式“核心线程数 = CPU核心数 / (1 - 阻塞系数)”或简单地设置为CPU核心数的两倍或多一点进行预估。
    • 对于混合型任务,需要根据具体情况综合考虑两种任务的比例,并使用相应的公式进行预估。
  2. 压测验证
    • 通过负载测试和压力测试来观察系统的性能表现,如吞吐量、响应时间等。
    • 根据测试结果调整核心线程数,直到找到最佳的性能平衡点。
  3. 监控动态调整
    • 在实际应用中,可以使用性能监控工具来监控系统的CPU使用率、内存使用情况和响应时间等指标。
    • 根据监控结果动态调整核心线程数,以适应负载的变化。

execute() 提交任务和 submit() 提交任务有啥不同?

execute() 无返回值,submit() 有返回值,会返回一个 FutureTask,然后可以调用 get() 方法阻塞获取返回值。

这样回答只能算及格,其实面试官问你这个问题主要想听你讲下 FutureTask 的实现原理,FutureTask 继承体系如下:

我们调用 submit() 方法提交的任务(Runnable or Callable)会被包装成 FutureTask() 对象。FutureTask 类提供了 7 种任务状态和五个成员变量。

  /*
     * Possible state transitions:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    // 构造函数中 state 置为 NEW,初始态
    private static final int NEW          = 0;
    // 瞬时态,表示完成中
    private static final int COMPLETING   = 1;
    // 正常执行结束后的状态
    private static final int NORMAL       = 2;
    // 异常执行结束后的状态
    private static final int EXCEPTIONAL  = 3;
    // 调用 cancel 方法成功执行后的状态
    private static final int CANCELLED    = 4;
    // 瞬时态,中断中
    private static final int INTERRUPTING = 5;
    // 正常执行中断后的状态
    private static final int INTERRUPTED  = 6;

    // 任务状态,以上 7 种
    private volatile int state;
    /** 通过 submit() 提交的任务,执行完后置为 null*/
    private Callable<V> callable;
    /** 任务执行结果或者调用 get() 要抛出的异常*/
    private Object outcome; // non-volatile, protected by state reads/writes
    /** 执行任务的线程,会在 run() 方法中通过 cas 赋值*/
    private volatile Thread runner;
    /** 调用get()后由等待线程组成的无锁并发栈,通过 cas 实现无锁*/
    private volatile WaitNode waiters;

创建 FutureTask 对象时 state 置为 NEW,callable 赋值为我们传入的任务。

run() 方法中会去执行 callable 任务。执行之前先判断任务处于 NEW 状态并且通过 cas 设置 runner 为当前线程成功。然后去调用 call() 执行任务,执行成功后会调用 set() 方法将结果赋值给 outcome,任务执行抛出异常后会将异常信息调用 setException() 赋值给 outcome。至于为什么要先将状态变为 COMPLETING,再变为 NORMAL,主要是为了保证在 NORMAL 态时已经完成了 outcome 赋值。finishCompletion() 会去唤醒(通过 LockSupport.unpark())那些因调用 get() 而阻塞的线程(waiters)。

    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

调用 get() 方法会阻塞获取结果(或异常),如果 state > COMPLETING,说明任务已经执行完成(NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED),则直接通过 report() 方法返回结果或抛出异常。如果state <= COMPLETING,说明任务还在执行中或还没开始执行,则调用 awaitDone() 方法进行阻塞等待。

    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

awaitDone() 方法则通过 state 状态判断来决定直接返回还是将当前线程添加到 waiters 里,然后调用LockSupport.park() 方法挂起当前线程。

还有个重要的 cancel() 方法,因为 FutureTask 源码类注释的第一句就说了 FutureTask 是一个可取消的异步计算。代码也非常简单,如果 state 不是 NEW 或者通过 CAS 赋值为 INTERRUPTING / CANCELLED 失败则直接返回。反之如果 mayInterruptIfRunning = ture,表示可能中断在运行中线程,则中断线程,state 变为 INTERRUPTED,最后去唤醒等待的线程。


    public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

什么是阻塞队列?阻塞队列有哪些?

阻塞队列 BlockingQueue 继承 Queue,是我们熟悉的基本数据结构队列的一种特殊类型。

阻塞队列顾名思义是一种支持阻塞操作的队列。它有以下两个特点:

  • 生产者阻塞:当队列满时,生产者会等待,直到队列不满。
  • 消费者阻塞:当队列为空时,消费者会等待,直到队列不为空。

提供 offer()、put()、take()、poll() 等常用方法。这个机制保证了生产者和消费者的平衡,同时避免了空轮询造成的 CPU 资源浪费。

JDK 提供的阻塞队列的实现有以下前 7 种:

1)ArrayBlockingQueue:由数组实现的有界阻塞队列,该队列按照 FIFO 对元素进行排序。维护两个整形变量,标识队列头尾在数组中的位置,在生产者放入和消费者获取数据共用一个锁对象,意味着两者无法真正的并行运行,性能较低。

2)LinkedBlockingQueue:由链表组成的有界阻塞队列,如果不指定大小,默认使用 Integer.MAX_VALUE 作为队列大小,该队列按照 FIFO 对元素进行排序,对生产者和消费者分别维护了独立的锁来控制数据同步,意味着该队列有着更高的并发性能。

3)SynchronousQueue:不存储元素的阻塞队列,无容量,可以设置公平或非公平模式,插入操作必须等待获取操作移除元素,反之亦然。

4)PriorityBlockingQueue:支持优先级排序的无界阻塞队列,默认情况下根据自然序排序,也可以指定 Comparator。

5)DelayQueue:支持延时获取元素的无界阻塞队列,创建元素时可以指定多久之后才能从队列中获取元素,常用于缓存系统或定时任务调度系统。

6)LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,与LinkedBlockingQueue相比多了transfer和tryTranfer方法,该方法在有消费者等待接收元素时会立即将元素传递给消费者。

7)LinkedBlockingDeque:一个由链表结构组成的双端阻塞队列,可以从队列的两端插入和删除元素。

【拓展进阶】

8)VariableLinkedBlockingQueue:说完以上 JDK 提供这几个阻塞队列后,还可以说下 LinkedBlockingQueue 是我们使用最广泛的阻塞队列,但是 LinkedBlockingQueue 一旦定义好后是不能修改容量 capacity 的。自己在使用线程池的过程中有动态去调整容量的需求,所以参考 RabbitMq 里的 VariableLinkedBlockingQueue,实现了一个可以调整容量的增强版 LinkedBlockingQueue,实现容量的动态调整。

9)MemorySafeLinkedBlockingQueue:而且 LinkedBlockingQueue 默认是使用 Integer.MAX_VALUE 作为容量的,也就是个无界队列,可能会有发生 OOM 的风险,所以自己实现了一个内存安全的 MemorySafeLinkedBlockingQueue,可以配置最大剩余内存,当内存达到该值的时候,再往队列放任务就会失败,很好的保证了不会发生令人头疼的 OOM 问题。

10)TaskQueue:上面讲 Tomcat 线程池时说过该阻塞队列,作为 LinkedBlockingQueue 的子类,覆写了 offer()、poll()、take() 等方法来调整线程池的执行流程。

重点说下 8、9 这两个自定义阻塞队列,来突出你对阻塞队列丰富的使用经验,这两队列源码可以看以下地址:

MemorySafeLinkedBlockingQueue & VariableLinkedBlockingQueue 队列实现

线程池拒绝策略有哪些?适用场景是怎么样的?

当阻塞队列已满并且达到最大线程数时,再提交任务会走拒绝策略流程,JDK 提供了拒绝策略顶层接口 RejectedExecutionHandler,所有拒绝策略都需要继承该接口,JDK 内置了四种拒绝策略。

简单回答:

  • 中止策略:无特殊场景。
  • 丢弃策略:无关紧要的任务(博客阅读量)。
  • 弃老策略:发布消息。
  • 调用者运行策略:不允许失败场景(对性能要求不高、并发量较小)。

详细回答

1.AbortPolicy中止策略:丢弃任务并抛出RejectedExecutionException异常。

如果是一些比较重要的业务,可以使用该拒绝策略,在系统不能进一步支持更大并发量的情况下通过抛出异常及时发现问题并进行处理。ThreadPoolExecutor中默认的策略就是AbortPolicy。

2.DiscardPolicy丢弃策略:丢弃任务,不抛出异常,一般无感知。建议一些无关紧要的任务可以使用此策略。

3.DiscardOldestPolicy弃老策略:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

基于这个特性,想到的场景就是,发布消息和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较。

4.CallerRunsPolicy调用者运行策略:在线程池没关闭的情况下,由调用者线程去处理任务,反之直接丢弃。此拒绝策略追求任务都能被执行,不丢失,比较适合并发量不大并且不允许丢失任务的场景场景,性能较低。

也可以根据自己需要自定义拒绝策略,比如 Dubbo 定义了拒绝策略 AbortPolicyWithReport,在抛出异常前会先进行线程堆栈信息的打印。

扩展


5.dubbo中的线程拒绝策略。

当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因。

(1)输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在。

(2)输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。

(3)继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性。

6.Netty中的线程池拒绝策略。

Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常。

7.activeMq中的线程池拒绝策略。

activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常。

8.pinpoint中的线程池拒绝策略。

pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍。

你在使用线程池的过程中遇到过哪些坑或者需要注意的地方?

这个问题其实也是在考察你对一些细节的掌握程度,就全甩锅给年轻刚毕业没经验的自己就行。可以适当多说些,也证明自己对线程池有着丰富的使用经验。

1)OOM 问题。刚开始使用线程都是通过 Executors 创建的,前面说了,这种方式创建的线程池会有发生 OOM 的风险,可以举例说明。

2)任务执行异常丢失问题。可以通过下述4种方式解决

  1. 在任务代码中增加 try、catch 异常处理
  2. 如果使用的 Future 方式,则可通过 Future 对象的 get 方法接收抛出的异常
  3. 为工作线程设置 setUncaughtExceptionHandler,在 uncaughtException 方法中处理异常
  4. 可以重写 afterExecute(Runnable r, Throwable t) 方法,拿到异常 t

3)共享线程池问题。整个服务共享一个全局线程池,导致任务相互影响,耗时长的任务占满资源,短耗时任务得不到执行。同时父子线程间会导致死锁的发生,进而导致 OOM。

4)跟 ThreadLocal 配合使用,导致脏数据问题。我们知道 Tomcat 利用线程池来处理收到的请求,会复用线程,如果我们代码中用到了 ThreadLocal,在请求处理完后没有去 remove,那每个请求就有可能获取到之前请求遗留的脏值。

5)ThreadLocal 在线程池场景下会失效,可以考虑用阿里开源的 Ttl 来解决。

6)需要自定义线程工厂指定线程名称,不然发生问题都不知道咋定位。

如果关闭线程池的时候任务没执行完,如何优雅地关闭线程池?

一、使用<font style="color:rgb(5, 7, 59);">shutdown()</font>方法

<font style="color:rgb(5, 7, 59);">shutdown()</font>方法是<font style="color:rgb(5, 7, 59);">ExecutorService</font>接口提供的一个用于关闭线程池的方法。它会启动线程池的关闭序列,执行以下操作:

  • 设置线程池的状态为<font style="color:rgb(5, 7, 59);">SHUTDOWN</font>,此时线程池不再接受新任务。
  • 中断空闲的线程,但不会中断正在执行的任务。
  • 等待已提交的任务执行完成。

调用<font style="color:rgb(5, 7, 59);">shutdown()</font>方法后,线程池会等待所有已提交的任务执行完毕,然后再关闭。如果需要等待线程池关闭完成,可以调用<font style="color:rgb(5, 7, 59);">awaitTermination()</font>方法。

二、使用<font style="color:rgb(5, 7, 59);">shutdownNow()</font>方法

<font style="color:rgb(5, 7, 59);">shutdownNow()</font>方法也是<font style="color:rgb(5, 7, 59);">ExecutorService</font>接口提供的一个用于关闭线程池的方法。与<font style="color:rgb(5, 7, 59);">shutdown()</font>方法不同,它会尝试立即关闭线程池,执行以下操作:

  • 设置线程池的状态为<font style="color:rgb(5, 7, 59);">STOP</font>
  • 尝试停止所有正在执行的任务,通过中断线程的方式实现。
  • 返回等待执行的任务列表,这些任务在关闭时还未来得及执行。
  • 清空任务队列。

需要注意的是,<font style="color:rgb(5, 7, 59);">shutdownNow()</font>方法并不保证能停止所有正在执行的任务,因为中断线程只是设置了一个中断状态,具体能否立即生效还取决于线程内部的实现。因此,在调用<font style="color:rgb(5, 7, 59);">shutdownNow()</font>方法后,可能需要结合<font style="color:rgb(5, 7, 59);">awaitTermination()</font>方法等待一段时间,以确保尽可能多的任务能够停止。

三、优雅关闭线程池的步骤

  1. 调用**<font style="color:rgb(5, 7, 59);">shutdown()</font>**方法:首先调用<font style="color:rgb(5, 7, 59);">shutdown()</font>方法,拒绝新提交的任务,并等待已提交的任务执行完成。
  2. 调用**<font style="color:rgb(5, 7, 59);">awaitTermination()</font>**方法:使用<font style="color:rgb(5, 7, 59);">awaitTermination()</font>方法等待线程池中的所有任务都完成执行。该方法会阻塞当前线程,直到所有任务都完成执行、超时发生或当前线程被中断。
  3. 处理未完成的任务:如果在超时时间内所有任务没有完成,可以调用<font style="color:rgb(5, 7, 59);">shutdownNow()</font>方法强制停止未执行完的任务。但需要注意,这可能会导致任务数据丢失或其他问题。
  4. 捕获和处理异常:在关闭线程池的过程中,需要注意捕获和处理可能出现的异常,以确保程序的健壮性。

以下是一个优雅关闭线程池的示例代码:


ExecutorService executorService = Executors.newFixedThreadPool(10);  

// 提交任务给线程池  
for (int i = 0; i < 100; i++) {  
    executorService.submit(() -> {  
        // 模拟任务执行  
        try {  
            TimeUnit.SECONDS.sleep(1);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        System.out.println("任务执行完成");  
    });  
}  

// 关闭线程池  
try {  
    // 拒绝新任务,等待已提交任务执行完成  
    executorService.shutdown();  
    // 等待一段时间,确保所有任务都执行完成  
    if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {  
        // 如果超时仍未完成,则强制关闭线程池  
        List<Runnable> notExecutedTasks = executorService.shutdownNow();  
        System.out.println("有 " + notExecutedTasks.size() + " 个任务未执行完成");  
    }  
} catch (InterruptedException e) {  
    // 当前线程被中断,需要重新关闭线程池  
    executorService.shutdownNow();  
    Thread.currentThread().interrupt();  
}

在这个示例中,我们首先使用<font style="color:rgb(5, 7, 59);">shutdown()</font>方法拒绝新任务,并等待已提交的任务执行完成。然后,使用<font style="color:rgb(5, 7, 59);">awaitTermination()</font>方法等待一段时间,以确保所有任务都执行完成。如果超时仍未完成,则使用<font style="color:rgb(5, 7, 59);">shutdownNow()</font>方法强制关闭线程池,并处理未执行的任务。在关闭线程池的过程中,我们还捕获了<font style="color:rgb(5, 7, 59);">InterruptedException</font>异常,以确保程序的健壮性。

请简单的实现一个线程池?

实现一个线程池需要处理线程管理、任务队列、任务调度、线程生命周期控制等多个方面。

以下是一个基本的Java线程池实现示例:


import java.util.concurrent.BlockingQueue;  
import java.util.concurrent.LinkedBlockingQueue;  
import java.util.List;  
import java.util.ArrayList;  
import java.util.concurrent.atomic.AtomicInteger;  

public class SimpleThreadPool {  
    private final BlockingQueue<Runnable> taskQueue;  
    private final List<Worker> workers;  
    private final AtomicInteger workerCount;  
    private final int corePoolSize;  
    private final int maximumPoolSize;  
    private volatile boolean isShutdown;  

    // Worker class that extends Thread  
    private class Worker extends Thread {  
        private Runnable currentTask;  

        public Worker() {  
            workerCount.incrementAndGet();  
        }  

        public void runTask(Runnable task) {  
            currentTask = task;  
            currentTask.run();  
            currentTask = null;  
        }  

        @Override  
        public void run() {  
            while (!isShutdown || !taskQueue.isEmpty()) {  
                try {  
                    Runnable task = taskQueue.take();  
                    runTask(task);  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt(); // Restore interrupt status  
                }  
            }  
            workerCount.decrementAndGet();  
        }  
    }  

    public SimpleThreadPool(int corePoolSize, int maximumPoolSize) {  
        this.corePoolSize = corePoolSize;  
        this.maximumPoolSize = maximumPoolSize;  
        this.taskQueue = new LinkedBlockingQueue<>();  
        this.workers = new ArrayList<>();  
        this.workerCount = new AtomicInteger(0);  
        this.isShutdown = false;  

        // Pre-start core number of workers  
        for (int i = 0; i < corePoolSize; i++) {  
            Worker worker = new Worker();  
            workers.add(worker);  
            worker.start();  
        }  
    }  

    public void execute(Runnable task) {  
        if (isShutdown) {  
            throw new IllegalStateException("ThreadPool is shut down");  
        }  

        if (workerCount.get() < maximumPoolSize) {  
            // If less than maximum pool size, start a new worker  
            Worker worker = new Worker();  
            workers.add(worker);  
            worker.start();  
            worker.runTask(task); // Run the task immediately if the worker is just started  
        } else {  
            // Otherwise, put the task in the queue  
            try {  
                taskQueue.put(task);  
            } catch (InterruptedException e) {  
                Thread.currentThread().interrupt(); // Restore interrupt status  
                throw new RuntimeException(e);  
            }  
        }  
    }  

    public void shutdown() {  
        isShutdown = true;  
        // Optionally, interrupt all workers to terminate them immediately  
        // for (Worker worker : workers) {  
        //     worker.interrupt();  
        // }  

        // Alternatively, wait for all tasks to complete by not interrupting workers  
        // and letting them terminate naturally as the queue empties  
    }  

    // Additional methods like shutdownNow(), awaitTermination(), etc., can be added for a more complete implementation  

    public static void main(String[] args) throws InterruptedException {  
        SimpleThreadPool pool = new SimpleThreadPool(2, 4);  

        for (int i = 0; i < 10; i++) {  
            int taskId = i;  
            pool.execute(() -> {  
                System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());  
                try {  
                    Thread.sleep(1000); // Simulate some work  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
            });  
        }  

        // Shutdown the pool after some time to let tasks complete  
        Thread.sleep(5000);  
        pool.shutdown();  

        // Optionally, wait for all workers to terminate  
        // while (pool.workerCount.get() > 0) {  
        //     Thread.sleep(100);  
        // }  
    }  
}

解释

  1. **<font style="color:rgb(5, 7, 59);">SimpleThreadPool</font>**:
    • <font style="color:rgb(5, 7, 59);">taskQueue</font>: 一个阻塞队列,用于存储待执行的任务。
    • <font style="color:rgb(5, 7, 59);">workers</font>: 一个列表,存储所有工作线程。
    • <font style="color:rgb(5, 7, 59);">workerCount</font>: 一个原子整数,用于跟踪当前活动的工作线程数。
    • <font style="color:rgb(5, 7, 59);">corePoolSize</font><font style="color:rgb(5, 7, 59);">maximumPoolSize</font>: 分别表示线程池的核心大小和最大大小。
    • <font style="color:rgb(5, 7, 59);">isShutdown</font>: 一个标志,表示线程池是否已关闭。
  2. **<font style="color:rgb(5, 7, 59);">Worker</font>** 内部类:
    • 继承自<font style="color:rgb(5, 7, 59);">Thread</font>,表示一个工作线程。
    • <font style="color:rgb(5, 7, 59);">currentTask</font>: 当前正在执行的任务。
    • <font style="color:rgb(5, 7, 59);">runTask(Runnable task)</font>: 运行任务的方法。
    • <font style="color:rgb(5, 7, 59);">run()</font>: 工作线程的主循环,不断从任务队列中取任务并执行,直到线程池关闭或任务队列为空。
  3. 构造方法:
    • 初始化线程池的参数,并预启动核心数量的工作线程。
  4. **<font style="color:rgb(5, 7, 59);">execute(Runnable task)</font>** 方法:
    • 如果线程池已关闭,则抛出异常。
    • 如果当前活动的工作线程数小于最大池大小,则启动一个新的工作线程并立即运行任务。
    • 否则,将任务放入任务队列中等待执行。
  5. **<font style="color:rgb(5, 7, 59);">shutdown()</font>** 方法:
    • 设置关闭标志,可以选择立即中断所有工作线程,或者等待任务队列中的任务完成。
  6. **<font style="color:rgb(5, 7, 59);">main</font>** 方法:
    • 创建一个线程池并提交一些任务。
    • 等待一段时间后关闭线程池。

线程池的实现原理?

一个线程池需要处理线程管理、任务队列、任务调度、线程生命周期控制等多个方面。

线程池的核心参数、线程池的状态、线程池的执行流程、线程池的拒绝策略

线程池的状态

RUNNING:线程池一旦被创建,就处于RUNNING状态,任务数为0,能够接收新任务,对已排队的任务进行处理。

SHUTDOWN:不接收新任务,但能处理已排队的任务。当调用线程池的shutdown()方法时,线程池会由RUNNING转变为SHUTDOWN状态。

STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。当调用线程池的shutdownNow()方法时,线程池会由RUNNING或SHUTDOWN转变为STOP状态。

TIDYING:当线程池在SHUTDOWN状态下,任务队列为空且执行中任务为空,或者线程池在STOP状态下,线程池中执行中任务为空时,线程池会变为TIDYING状态,会执行terminated()方法。这个方法在线程池中是空实现,可以重写该方法进行相应的处理。

TERMINATED:线程池彻底终止。线程池在TIDYING状态执行完terminated()方法后,就会由TIDYING转变为TERMINATED状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值