1、synchronized
线程安全问题的主要诱因
- 存在共享数据(也称临界资源)。
- 存在多条线程共同操作这些共享数据。
解决问题的根本方法: 同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作。
互斥锁的特性:
互斥性∶即在同一时间只允许—一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
可见性∶必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
synchronized锁的不是代码,锁的都是对象。
根据获取的锁的分类:获取对象锁和获取类锁
获取对象锁的两种用法
- 1.同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号( )中的实例对象
- 2.同步非静态方法(synchronized method),锁是当前对象的实例对象
获取类锁的两种用法
- 1、同步代码块(synchronized(类.class)),锁是小括号( )中的对象(Class对象)。
- 2、同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)。
对象锁和类锁的总结:
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;
- 同一个类的不同对象的对象锁互不干扰;
- 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的;
- 类锁和对象锁互不干扰。
2、synchronized底层实现原理
实现synchronized的基础
- Java对象头
- Monitor
对象在内存中的布局
- 对象头
- 实例数据
- 对齐填充
synchronized使用的锁对象是存储在java对象头里的,其主要结构是由Mark Word和Class Metadata Address组成,Class Metadata Address是指向类元数据的指针,虚拟机通过这个指针确认其是哪个对象的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
对象头的结构
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64 bit | Mark Word | 默认存储对象的hashCode,分代年龄,锁类型,锁标志位等信息 |
32/64 bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的数据 |
Mark Word(非固定数据结构)
Monitor:每个Java对象天生自带了一把看不见的锁。
Monitor也称为管程或者监视器锁,我们可以把它理解为一个同步工具,也可以描述为一种同步机制,通常它被描述为一个对象。
重量级锁进行分析,锁的标识位为10,指针指向的是monitor对象的起始地址,每个对象都存在着一个monitor与之关联,monitor存在于对象的对象头中,对象与monitor之间有多存在多种实现方式,如monitor可以与对象一起存在销毁,或当线程试图获取对象锁时,自动生成.当monitor被某个线程持有后,它便处于锁定状态.
加锁过程:
当多个线程访问同一段同步代码的时候,首先会进入到EntryList集合中,当线程获取到对象的Monitor之后,就进入到Object区域,并把Monitor中的Owner变量设置为当前线程,同时Monitor中的count就会加1,如果线程调用wait方法将会释放当前持有的Monitor,owner会被恢复成null,count也会被减1,同时该线程ObjectWaiter实例就会进入到waitSet集合等待被唤醒,若当前线程执行完毕,它也将释放monitor锁,并复位对应变量的值,以便其它线程进入获取monitor锁.
Java中任意对象可以作为锁的原因: Moniter对象存在于每一个Java对象的对象头中,synchronized便是通过这种方式获取锁的。
锁的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。
当获取当前锁以后,再获取该锁,synchronized是支持该操作的。
public void syncsTask(){
synchronized (this){
System.out.println("Hello");
synchronized (this){
System.out.println("World");
}
}
}
为什么会对 synchronized嗤之以鼻(不想用)
- 早期版本中,synchronized属于重量级锁,依赖于 Mutex Lock(底层操作系统)实现。
- 底层操作系统,在线程之间的切换需要从用户态转换到核心态,开销较大。
Java6以后,synchronized性能得到了很大的提升。
- Adaptive Spinning(自适应自旋)
- Lock Eliminate(锁消除)
- Lock Coarsening(锁初化)
- Lightweight Locking(轻量级锁)
- Biased Locking(偏向锁)
自旋锁()
- 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。
- 通过让线程执行忙循环等待锁的释放,不让出CPU。
- 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销。
- 用户可以用过PreBlockSpin进行更改
自适应自旋锁(Adaptive Spinning)
- 自旋的次数不再固定。
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 可能性大,就会增大次数。很难获取到就直接忽略自旋
锁消除( Lock Eliminate)
- JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
可以看到append是加锁的,但是这个锁永远不会发生竞争,sb属于本地变量没有return,所以这个锁可以消除。
public class SyncBlockAndMethod {
public void add(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
}
锁粗化(Lock Coarsening)
另一种极端
如果一连串系类操作都会同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,那即使没有锁竞争,频繁的进行互斥锁操作也会导致不必要的性能浪费,此时我们可以矿大枷锁的范围,避免反复加锁和解锁,
- 通过扩大加锁的范围,避免反复加锁和解锁
public class SyncBlockAndMethod {
public String add(String str1) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
return sb.toString();
}
}
3、synchronized的四种状态
- 无锁、偏向锁、轻量级锁、重量级锁
- 锁膨胀方向:无锁→偏向锁→轻量级锁→重量级锁
偏向锁:减少统一线程获取锁的代价
- 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。
- 不适用于锁竞争比较激烈的多线程场合。
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做仼何同步操作即获取锁的过程。只需要检查 Mark Word的锁标记位为偏向锁,以及当前线程Id等于 Mark Word 的 ThreadID即可,这样就省去了大量有关锁申请的操作。
轻量级锁:
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
适应的场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
轻量级锁的加锁过程:
(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为"01"状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如图所示。
(2)拷贝对象头中的 Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的 Mark Word更新为指向 Lock Record的指针,并将 Lock record里的 owner指针指向 object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark word的锁标志位设置为“00",即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
(5)如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋咱们前面讲过,就是为了不让线程阻塞,而采用循环去获取锁的过程。
解锁的过程:
(1)通过cAS操作尝试把线程中复制的 Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程就完成了。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
锁的内存语义
当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
偏向锁、轻量级锁、重量级锁的汇总
4、Synchronized和ReentrantLock(再入锁)的区别
ReentrantLock(再入锁)
- 位于 java.util.concurrent.locks包
- 和CountDownlatch、FutureTask、Semaphore-样基于AQS实现。
- 能够实现比synchronized更细粒度的控制,如控制fairness。
- 调用lock( )之后,必须调用unlock( )释放锁。
- 性能未必比synchronized高,并且也是可重入的。
ReentrantLock公平性的设置
- ReentrantLock lock = new ReentrantLock(true);
- 参数为true时,倾向于将锁赋予等待时间最久的线程。
- 公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)。
- 非公平锁:抢占的顺序不一定,看运气。
- synchronized是非公平锁
Java默认的调用策略,很少会导饥饿情况的发生。要保证公平性,会导致额外的开销。会导致吞吐量下降,如果非必要,不要设置公平锁。
Reentrantlock将锁对象化
- 判断是否有线程,或者某个特定线程,在排队等待获取锁。
- 带超时的获取锁的尝试。
- 感知有没有成功获取锁。
ReentrantLock使用
true为公平锁,false为非公平锁
ReentrantLock lock = new ReentrantLock(true);
使用代码:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo implements Runnable {
private static ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
lock.lock();
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
将wait、notify、notifyAll的对象化
- java.util.concurrent.locks.Condition
ArrayBlockingQueue源码:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
ArrayBlockingQueue是数组实现的、线程安全的、有界的阻塞队列。
- 其互斥锁是通过ReentrantLock来实现的。
- ArrayBlockingQueue对应的数组是有界限的,
- 阻塞队列是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其它要获取该资源的线程要阻塞等待。
区别总结:
- synchronized是关键字,ReentrantLock是类。
- Reentrantlock可以对获取锁的等待时间进行设置,避免死锁。
- ReentrantLock可以获取各种锁的信息。
- ReentrantLock可以灵活地实现多路通知。
- 机制:sync操作 Mark Word,lck调用 Unsafe类的park( )方法。
Unsafe类的park( )方法位于unsafe类里,unsafe是一个类似于后门的工具,可以在任意内存位置处读写数据.另外unsafe还支持一些CAS的操作.
5、Java内存模型JMM
Java内存模型(即 Java Memory Model,简称JMM),本身是—种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,有些地方成为栈空间,用于存储线程私有的数据,而java内存模型中规定,所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问。线程对变量的操作如读取,赋值等必须在工作内存中进行。
首先将变量从主内存拷贝到自己的工作内存中,然后对变量操作,操作后再将变量写回主内存。不能直接操作主内存中的变量,工作内存中存储着主内存中变量的副本拷贝,工作内存是每个变量的私有区域,因此不同线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
JMM中的主内存和工作内存。
JMM中的主内存:
- 存储Java实例对象。
- 包括成员变量、类信息、常量、静态变量等。
- 属于数据共亨的区域,多线程并发操作时会引发线程安全问题。
JMM中的工作内存:
- 存储当前方法的所有本地变量信息,本地变量对其它线程不可见。
- 字节码行号指示器,Native方法信息。
- 属于线程私有数据区域,不存在线程安全问题。
JMM与Java内存区域划分是不同的概念层次
- JMM描述的是一组规则,围绕原子性,有序性、可见性展开
- 相似点:存在共享区域和私有区域
主内存与工作內存的数据存储类型以及操作方式归纳
- 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中。
- 引用类型的本地变量:引用存储在工作內存中,实例存储在主内存中。
- 成员变量、statIc变量、类信息均会被存储在主内存中。
- 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。
指定重排序需要满足的条件
- 在单线程环境下不能改变程序运行的结果
- 存在数据依赖关系的不允许重排序
无法通过 happens-before原则推导出来的,才能进行指令的重排序
JVM如何解决可见性问题
A操作的结果需要对B操作可见,则A与B存在happens-befor关系
i = 1; //线程A执行
j = i; //线程B执行
happens—before的八大原则
- 程序次序规则: 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则: 一个 unLock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则: 对一个变量的写操作先行发生于后面对这个变量的读操作。
- 传递规则: 如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则: Thread对象的start( )方法先行发生于此线程的每一个动作。
- 线程中断规则: 对线程 Interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 线程终结规则: 线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join( )方法结束、Thread.isAlive( )的返回值手段检测到线程已经终止执行;
- 对象终结规则: 一个对象的初始化完成先行发生于他的 finalize( ) 方法的开始。
happens-before的概念
如果两个操作不满足上述任意一个 happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序;如果操作 A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。
volatile:JVM提供的轻量级同步机制
- 保证被 volatile 修饰的共享变量对所有线程总是可见的。
- 禁止指令重排序优化
volatile的可见性
volatile修改后,立即可见。
public class VolatileSafe {
volatile boolean shutdown;
public void close() {
shutdown = true;
}
public void doWork() {
while (!shutdown) {
System.out.println("safe ...");
}
}
}
volatile变量为何让其它线程立即可见?
- 当写一个 volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
- 当读取一个 volatile 变量时,JMM会把该线程对应的工作内存置为无效
- 采用这种读写的形式,使其对其它线程立即可见,
volatile 如何禁止重排优化?
内存屏障(Memory Barrier)
- 保证特定操作的执行顺序。
- 保证某些变量的内存可见性。
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化
强制刷出各种CPU的缓存数据,因此仼何CPU上的线程都能读取到这些数据的最新版本.
volatile正是通过内存屏障实现其在内存中的语义即可见性和禁止重排优化.
volatile和synchronized的区别:
- volatile本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止.
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性。
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
6、CAS(Compare and Swap)
像synchronized这种独占锁属于悲观锁,悲观锁始终假定,因此会屏蔽一切可能违反数据完整性的操作,除此之外,还有乐观锁,它假定不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性,如果提交失败则会进行重试,而乐观锁最常见的就是CAS。
CAS是一种高效实现线程安全性的方法
- 支持原子更新操作,适用于计数器,序列发生器等场景。
- 属于乐观锁机制,号称 lock-free。
- CAS操作失败时由开发者决定是继续尝试,还是执行别的操作。
CAS思想:
像synchronized这种独占锁属于悲观锁,悲观锁始终假定会造成并发冲突,因此会屏蔽一切可能违反数据完整性的操作。除此之外,还有乐观锁,它假定不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性,如果提交失败则会进行重试,而乐观锁最常见的就是CAS。
CAS是一种高效实现线程安全性的方法。
- 支持原子更新操作,适用于计数器,序列发生器等场景。
- 属于乐观锁机制,号称lock-free(无锁)。
- CAS操作失败时由开发者决定是继续尝试,还是执行别的操作。
CAS思想:
- 包含三个操作数-内存位置(V),预期原值(A)和新值(B)
将内存位置的值与预期原值进行比较,如果相匹配则处理器会自动将内存位置的值更新为新值,否则处理器不做任何操作,这里内存位置的值V即主内存的值。
CAS多数情况下对开发者来说是透明的
- JU.C的 atomIc包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。
- Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患。
- Java9以后,可以使用 Variable Handle API来替代 Unsafe。
缺点:
- 若循环时间长,则开销大。
- 只能保证一个共享变量的原子操作。
- ABA问题。(解决:AtomicStamped Reference)
ABA问题
CAS的案例:
最简单的方法
public class Test01 {
private int a = 10;
public void add() {
a++;
System.out.println("计算后:"+ a);
}
}
采用synchronized关键字
public class Test01 {
private int a = 10;
public synchronized void add() {
a++;
System.out.println("计算后:"+ a);
}
}
采用实现了CAS思想的atomic类进行计算(compareAndSet 则实现了CAS)
import java.util.concurrent.atomic.AtomicInteger;
public class Test01 {
private int a = 10;
public synchronized void add() {
// 采用实现了CAS的方法进行数据添加
AtomicInteger ato = new AtomicInteger(a);
boolean result = ato.compareAndSet(a, 10);
System.out.println("计算后:" + a);
System.out.println("添加结果为: "+result);
}
}
7、Java线程池
在web开发中,服务器需要接受并处理请求,所以会为一个请求分配一个线程进行处理,如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程。如此一来,会大大降低系统的效率,可能会出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多,我们需要一种方法能够重复的利用线程去完成新的任务。
利用Executors 创建不同的线程池满足不同场景的需求
- 1、new FixedThreadPool(int nThreads)指定工作线程数量的线程池
- 2、new CachedThreadPool处理大量短时间工作任务的线程池,
- (1)试图缓存线程井重用,当无缓存线程可用时,就会创建新的工作线程
- (2)如果线程闲置的时间超过阈值,则会被终止并移出缓存;
- (3)系统长时间闲置的时候,不会消耗什么资源
- 3、new SingleThreadExecutor 创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它(单线程,顺序执行任务)。
- 4、newScheduledThreadPool(int corePoolSize)与new SingleThreadScheduledExecutor( )定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程。
- 5、new WorkStealingPool内部会构建 ForkJoinPool,利用 working-tealing算法,并行地处理任务,不保证处理顺序。(是JDK8才引入的创建线程池的方法)
Fork/Join框架
- 把大任务分割成若千个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架。
Work-Stealing算法:某个线程从其他队列里窃取任务来执行。
Fork/Join框架是ExecutorService接口的一种具体的实现,目的是为了更好地利用多处理器带来的好处,它是为那些能递归的拆分成子任务的工作类型量身设计的,其目的在于能够使用所有可用的运算能力来提升你的应用的性能。
运行原理:
Fork/Join将子任务放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务。
那么这里会出现一种情况,有些线程任务队列的任务已经完成,有的队列还有任务没有完成,这就造成已完成任务线程会被闲置,为了提高效率,完成自己任务而处于空闲的线程能够从其它仍处于busy状态的工作线程处窃取等待执行的任务。为了减少窃取任务线程和被窃取任务线程间的竞争,通常会使用双端队列,被窃取任务线程永远会从双端队列的头部执行,而窃取任务的线程永远从双端队列的尾部执行。
为什么要使用线程池:
- 降低资源消耗 (通过重复利用已创建的线程来降低线程创建和销毁造成的消耗)
- 提高线程的可管理性 (线程是稀缺资源,重复创建增大系统的消耗与不稳定性.使用线程池可以进行统一的分配,调优和监控)
源码解读:
J.U.C的三个Executor接口(一次扩展)
- Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦。
public interface Executor {
//创建线程池
void execute(Runnable command);
}
- ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善。
public interface ExecutorService extends Executor {
//可以有返回值
<T> Future<T> submit(Callable<T> task);
}
- ScheduledExecutorService:支持 Future和定期执行任务。
ThreadPoolExector
public class ThreadPoolExecutor extends AbstractExecutorService {
//构造方法
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,
long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize:核心线程数
- maximumPoolSize:线程不够用时能够创建的最大线程数。
- workQueue:任务等待队列。
- keepAliveTime:抢占的顺序不一定,看运气。
- hreadFactory:创建新线程,Executors.defaultThreadFactory( )
- handler:线程池的饱和策略。
阻塞队列满,并且没有空闲线程,继续提交线程:
- AbortPolicy:直接抛出异常,这是默认策略。
- CallerRunsPolicy:用调用者所在的线程来执行任务。
- DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务。
- DiscardPolicy:直接丢弃任务。
- 实现 RejectedExecutionHandler接口的自定义 handler。
新任务提交 execute执行后的判断
- 如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的。
- 如果线程池中的线程数量大于等于 corePoolSize且小于maximumPoolSize,则只有当 workQueue满时才创建新的线程去处理任务。
- 如果设置的 corePoolSize和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue末满,则将请求放入 workQueue中,等待有空闲的线程去从 workQueue中取任务并处理。
- 如果运行的线程数量大于等于 maximumPoolSize,这时如果workQueue已经满了,则通过 handler所指定的策略来处理任务;
流程图:
线程池的运行状态和线程池内的有效线程数
public class ThreadPoolExecutor extends AbstractExecutorService {
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//获取运行状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
//获取活动线程数
private static int workerCountOf(int c) { return c & CAPACITY; }
//获取两者
private static int ctlOf(int rs, int wc) { return rs | wc; }
}
线程池的状态:
- RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。
- SHUTDOWN:不再接受新提交的任务,但可以处理存量任务。
- STOP:不再接受新提交的任务,也不处理存量任务。
- TIDYING:所有的任务都已终止。
- TERMINATED:terminated( )方法执行完后进入该状态
状态转换池
线程池的大小如何选定
- CPU密集型: 线程数=按照核数或者核数+1设定。
- I/O密集型: 线程数=CPU核数*(1+平均等待时间/平均工作时间)