一、实现方式
1.1继承Thread类 重写run方法
/**
* 继承Thread类 重写run方法
*/
public class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();
}
}
1.2实现Runnable接口 重写run方法
/**
* 实现Runnable接口 重写run方法
*/
public class Thread2 implements Runnable{
private String name;
public Thread2(String name) {
this.name=name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new Thread(new Thread2("C")).start();
new Thread(new Thread2("D")).start();
}
}
最常用的方式:
匿名内部类:
/**
* 匿名内部类创建线程
*/
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("匿名内部类"+i);
}
}
});
lambad方式:
/**
* lambda方式创建线程
*/
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
System.out.println("lambad"+i);
}
});
1.3实现Callable 重写call方法,配合FutureTask
一般用于有返回结果的非阻塞的执行方法同步非阻塞。
/**
* 1.3实现Callable 重写call方法,配合FutureTask
* 创建线程
*/
public class Test02 {
static class MyCallable implements Callable{
@Override
public Object call() throws Exception {
int count = 0;
for (int i = 0; i < 100; i++) {
count += i;
}
return count;
}
}
public static void main(String[] args) throws ExceptionInInitializerError, InterruptedException, ExecutionException {
//1、创建MyCallable
MyCallable myCallable = new MyCallable();
//2、创建FutureTask,传入Callable
FutureTask futureTask = new FutureTask(myCallable);
//3、创建Thread线程
Thread t = new Thread(futureTask);
//4、启动线程
t.start();
//5、业务操作
//。。。
//6、得到结果
Object o = futureTask.get();
System.out.println("o = " + o);
}
}
1.4基于线程池构建线程
追其底层,其实只有一种,实现Runnable
二、线程的状态
2.1从操作系统层面来说总共有5种。
2.2Java层面给了线程6种状态
NEW : 分配内存地址,创建线程
RUNNABLE:(就绪/运行)调用start()之后(/没有调度CPU调度)
BLOCKED:还未拿到锁,等待、被阻塞(拿到synchronized失败状态)
WAITNG:挂起线程、wait(),需要手动唤醒
TIMED_WATING:睡眠,sleep()、join();会被自动唤醒,无需手动唤醒
TERMINATED:run方法执行完毕,线程生命周期结束
2.3 7种:
这种说法实则是将RUNNABLE状态细分成了运行和就绪两种状态
2.4代码案例
/**
* 线程各状态模拟
*/
public class State {
//NEW : 分配内存地址,创建线程
public static void NEW(){
Thread t1 = new Thread(()->{
});
System.out.println("t1 = " + t1.getState());
}
//RUNNABLE:(就绪/运行)调用start()之后(/没有调度CPU调度)
public static void RUNNABLE() throws InterruptedException {
Thread t2 = new Thread(()->{
while (true){
}
});
t2.start();
Thread.sleep(500);
System.out.println("t2 = " + t2.getState());
}
//BLOCKED:还未拿到锁,等待、被阻塞(拿到synchronized失败状态)
public static void BLOCKED() throws InterruptedException {
Object obj = new Object();
Thread t3 = new Thread(()->{
synchronized (obj){
}
});
//main线程拿到obj的锁资源
synchronized (obj){
t3.start();
Thread.sleep(500);
System.out.println("t3 = " + t3.getState());
}
}
//WAITNG:挂起线程、wait(),需要手动唤醒
public static void WAITNG() throws InterruptedException {
Object obj = new Object();
Thread t4 = new Thread(()->{
synchronized (obj){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t4.start();
Thread.sleep(500);
System.out.println("t4 = " + t4.getState());
}
//TIMED_WATING:睡眠,sleep()、join();会被自动唤醒,无需手动唤醒
public static void TIMED_WATING() throws InterruptedException {
Thread t5 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t5.start();
Thread.sleep(500);
System.out.println("t5 = " + t5.getState());
}
//TERMINATED:run方法执行完毕,线程生命周期结束
public static void TERMINATED() throws InterruptedException {
Thread t6 = new Thread(()->{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t6.start();
Thread.sleep(1000);
System.out.println("t6 = " + t6.getState());
}
public static void main(String[] args) throws InterruptedException {
NEW();
RUNNABLE();
BLOCKED();
WAITNG();
TIMED_WATING();
TERMINATED();
}
}
三、如何停止线程
本质:使正在运行的run方法结束,无论是return还是抛出异常都可以。
3.1 stop方法(不推荐)
强制结束,已过时
3.2 使用共享变量(很少会用)
根据业务决定,有的线程可能会通过死循环保证一直运行。
可用过修改共享变量破坏死循环,使线程退出循环,结束run方法。
3.3 interrupt方式
共享变量方式(类似),线程内部提供中断标记位
interrupt标记默认false
线程执行过程中如果修改了终端标记位,会抛出异常
四、sleep和wait方法的区别
sleep | wait | |
---|---|---|
来源 | Thread中的static修饰的方法 | Object类的方法 |
状态 | TIMED_WATING,可自动唤醒 | WAITNG,需手动唤醒 |
持有锁时执行 | 不会释放锁资源 | 释放锁资源 |
执行前提 | 都可执行 | 必须持有锁时才能执行 |
wait方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,
如果没有持有synchronized锁,则无法操作ObjectMonitor对象。
五、并发编程三大特性
5.1原子性
定义:一个操作不可分割,不可中断,一个线程在执行时,另一个线程不会影响到它。
目的:为了方式多线程操作共享变量,带来线程安全问题
5.1.1 synchronize 同步线程锁
互斥锁,需要先获取到锁资源,才能执行后面的命令。
//synchronize
public static void syn(){
Object obj = new Object();
Thread t = new Thread(()->{
synchronized (obj){
//执行业务
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
new Thread(()->{
synchronized (obj){
System.out.println("我获取到了");
}
}).start();
}
5.1.2 CAS
操作系统CPU层面的并发原语。
替换内存某个位置的值时,首先查看其中的值是否与预期值一致,一致才执行替换操作。原子性操作。
优点:避免内核态和用户态的切换,线程不会挂起,且保证数据的原子性。
缺点:通过while循环,执行CAS的操作,性能较差。
自旋时间过长问题:
- 指定循环次数,超过则挂起/失败
- 一次失败后,暂存该操作,后续需要获取结果时,将暂存的操作全部执行,再返回最后的结果。
//CAS锁
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void CASTest() throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("atomicInteger = " + atomicInteger);
}
ABA问题:
解决方式:通过版本号区分
5.1.3 Lock锁
相当于jdk1.6之前的synchronize 锁性能较好,1.6之后相差不大。
并发较多时,推荐使用ReentrantLock锁。
//Lock锁
private static int count;
private static ReentrantLock lock = new ReentrantLock();
public static void increment(){
//加锁
lock.lock();
try {
count++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
//释放锁
lock.unlock();
}
}
5.1.3 ThreadLock锁
保证原子性的方式:不让多线程去操作临界资源,让每个线程只去操作属于自己的数据。
//ThreadLock锁
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) {
tl1.set("123");
tl2.set("456");
Thread t1 = new Thread(()->{
System.out.println("t1:"+tl1.get());
System.out.println("t1:"+tl2.get());
});
t1.start();
System.out.println("main:"+tl1.get());
System.out.println("main:"+tl2.get());
}
5.2 可见性
概念:基于CPU位置出现,CPU提供了L1、L2、L3的三级缓存,每次去主内存拿完数据之后,会存储在三级缓存,下次会去缓存中取。
问题:如今的CPU都是多核的,每个线程的工作内存(三级缓存)都是独立的,每个线程中做修改后只改自己的工作内存,没有及时同步到主内存,导致数据不一致。
解决方式:
5.2.1 volatile (彻底解决)
作用:告知CPU当前属性的操作,不允许使用缓存,必须去主内存操作。
5.2.2 synchronize
同步代码块/同步方法,获取到锁资源后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,释放锁后,会立即将CPU缓存中的数据同步到主内存。
注意:仅在加锁的时刻,同步数据
5.2.3 Lock
基于volatile 实现,对其修饰的属性操作时,CPU会执行带有Lock前缀的指令,将修改的数据以及其他属性立即同步到主内存中。还会将其他CPU缓存中的和这个数据设置为无效。
注意:仅在执行的时刻,同步数据
5.2.4 final
修饰的属性在运行期间不允许修改,简介保证了可见性。
5.3 有序性
为发挥CPU的性能,在不影响最终结果的前提下,会对指令进行重排。
核心解决方式
5.3.1 volatile
实现原理:内存屏障概念。将内存屏障看成一条指令。
在两个操作之间,添加上一道指令,这个指令可避免上下执行的其他指令进行重新排序。
5.4 四种引用类型
5.4.1 强
当一个对象被强引用时,始终处于可达状态,不可能被垃圾回收清理。
内存泄漏主要原因之一。
User user = new User();
5.4.2 软
系统内存不足时会被回收,通常用在对内存敏感的程序中,作为缓存使用。
SoftReference
5.4.3 弱
生命周期更短,不管JVM内存空间是否足够,垃圾回收一运行,总会回收该对象占用的内促。
5.4.4 虚
不能单独使用,必须和引用队列联合使用。
作用:跟踪对象的垃圾回收状态。
六、锁的分类
6.1 可重入锁、不可重入锁
重入:
当前线程获取到锁后,尝试再次获取时可以直接拿到
synchronize、ReentrantLock、ReentrantReadWriteLock
不可重入:
当前线程获取到锁后,尝试再次获取时不能直接拿到,需要等待自己释放(死锁)。
6.2 乐观锁、悲观锁
乐观:
获取不到锁资源,可以再次让CPU调用,重新尝试获取锁资源。不会挂起,不断尝试。
CAS
悲观:
当没有获取到锁资源时,大概率挂起线程(BLOCKED、WAITING),涉及用户态和内核态的切换,消耗资源。
- 用户态:JVM可以自行执行的命令,不需要借助操作系统。
- 内核态:JVM不可以自行执行的命令,需要借助操作系统才可以执行。
synchronize、ReentrantLock、ReentrantReadWriteLock
6.3 公平锁、非公平锁
公平:排队
线程A获取到锁资源,B来排队,此时C来了必须排到B后面。
ReentrantLock、ReentrantReadWriteLock都可实现
非公平:可以插队
线程A获取到锁资源,B来排队,此时C来了先尝试竞争一波。
- 拿到锁资源:插队成功。
- 没拿到锁资源:依然排在B后面,等B释放后,可再次尝试竞争。
synchronize只能实现非公平
6.4 互斥锁、共享锁
ReentrantReadWriteLock两者都有
互斥:
同一时间点,只会有一个线程持有
synchronize、ReentrantLock
共享:
同一时间,可以有多个线程持有
七、synchronize
7.1 实现原理
基于对象实现
MarkWord
标记了不同的锁在不同状态下所存储的对象信息
7.2 锁升级状态的转变
7.3 synchronize在jdk1.6中的优化
7.3.1 锁消除
若代码块中不存在操作临界资源的情况,触发
public synchronized void test(){
//synchronized相当于没有
}
7.3.2 锁膨胀
循环中频繁获取和释放锁资源,则将锁的范围扩大,避免频繁竞争和获取带来不必要的消耗。
public void test(){
Object object = new Object();
for (int i = 0; i < 99999999; i++) {
synchronized (object){
}
}
//上方代码触发锁膨胀等同于如下代码
synchronized (object){
for (int i = 0; i < 99999999; i++) {
}
}
}
7.3.3 锁升级(获取锁资源的成本降低)
jdk1.6之前:获取不到锁资源时,会立即挂起当前线程,因此性能较差。
7.3.3.1 无锁、匿名偏向:
当前对象没有作为锁存在。
7.3.3.2 偏向锁:
当前不存在资源竞争,当前线程再次尝试获取锁资源时,进过判断是否是再次申请
- 是:直接拿走资源。
- 不是:基于CAS方式,尝试将偏向锁指向当前线程,如果获取不到,触发锁升级->轻量级锁。
7.3.3.3 轻量级锁:
采用自旋锁的方式,频繁的以CAS形式获取锁资源(自适应自旋锁)
- 成功:直接拿走资源。
- 失败:自旋一定次数,还未获取资源,触发锁升级。
7.3.3.4 重量级锁:
传统synchronized方式,拿不到锁资源则直接挂起。(用户态&内核态切换)
7.4 ReentrantLock与synchronized的区别
ReentrantLock | synchronized | |
---|---|---|
使用方式 | 执行lock() | 同步方法、同步代码块 |
释放 | 执行unlock() | 不需要手动释放 |
本质 | 对象,功能性更强 | 关键字 |
公平性 | 公平、非公平 | 非公平 |
自定义 | 可以设置尝试竞争的时间,可设置拿不到资源时自定义返回 | 无 |
原理 | 基于AQS实现(JUC下一个基类,Java开发) | 基于ObjectMonitor(底层为C语言编写) |
性能 | 不存在锁升级概念,效率略高 | 竞争过于激烈时,总会升级为重量级锁,舍弃jdk1,8这个层面,基于C语言优化潜力更高 |
八、AQS
JUC包下的一个基类AbstractQueuedSynchronizer
提供了一个有volatile修饰的采用cas方式修改的state属性
AQS队列:双向链表。
九、ReentrantReadWriteLock
8.1 为什么要出现读写锁
ReentrantLock与synchronized都是互斥锁
业务场景:读多写少,互斥锁效率较低
读和读之间不互斥,读写互斥
8.2 实现原理
基于AQS,还是对state进行操作,拿到资源就去干活儿,没拿到依然去AQS队列中排队。
可重入锁,相比于ReentrantLock锁可重入次数降低。
8.2.1 读锁:
基于state的高16位操作。
读锁重入:共享锁,多个读线程持有锁资源,无法确认每个线程读锁重入次数。每个读线程都有一个ThreadLocal记录重入的次数。
8.2.2 写锁:
基于state的低16位操作。
写锁重入:state+1,确认持有锁的线程,是当前写锁线程即可,state取值范围缩小。
8.2.3 写锁饥饿:
写操作要等到所有读操作执行完成之后再执行。读操作过多时,会出现前面的读操作还未执行完,后续的读操作插队继续执行,造成写的操作等待时间过长。
当写线程开始等待时,新来的读操作需要去AQS队列排队。如果队列的前面需要写锁资源的线程,那么后续线程是无法拿到锁资源的。
持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源。
十、JDK提供了哪些线程池
一般情况下不推荐使用jdk自带的线程池,很多参数无法控制。
10.1 newFixedThreadPool 定长线程池
线程数是固定的,参数作为核心线程数和最大线程数,不会创建非核心线程,超过最大线程数时,放入阻塞队列等待。
//10.1 newFixedThreadPool 定长线程池
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, //主线程数
nThreads, //最大线程数
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); //阻塞队列
}
10.2 newSingleThreadExecutor 单例线程池
线程池中只有一个工作线程在处理任务,顺序消费。
10.3 newCacheThreadPool 缓存线程池
默认没有核心工作线程,创建时会直接构建一个工作线程,每次任务先丢入阻塞队列。
10.4 newScheduleThreadPool 定时任务线程池
只对阻塞队列做了更改,可以设置出阻塞队列的等待时间。
10.5 newWorkStealingPool 基于工作窃取
每个线程都有属于自己的阻塞队列。基于ForkJoinPool。
十一、线程池的核心参数有哪些?
11.1 拒绝策略
11.1.1 AbortPolicy
无法处理任务时,直接抛出异常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
11.1.2 CallerRunsPolicy
无法处理任务时,将任务交给调用者处理。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
11.1.3 DiscardPolicy
无法处理任务时,直接丢掉任务。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
11.1.4 DiscardOldestPolicy
无法处理任务时,将队列中最早的任务丢掉,将当前任务再次尝试交给线程池处理
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();//将队列中最早的任务丢掉
e.execute(r);
}
}
十二、线程池的状态
核心参数ctl,维护了工作线程的个数、核心状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
占32个字节,ctl的高3位表示线程池状态,低29位表示工作线程的个数。
状态名称 | 新任务 | 处理中任务 | 阻塞队列中的任务 |
---|---|---|---|
RUNNING | 接收 | 正常处理 | 正常处理 |
SHUTDOWN | 不接收 | 正常处理 | 正常处理 |
STOP | 不接收 | 中断 | 一个不管 |
12.1 RUNNING
可以处理任务,并且处理阻塞队列中的任务
12.2 SHUTDOWN
不会接收新任务,处理中的任务正常进行,阻塞队列的任务也会做完。
12.3 STOP
不会接收新任务,处理中任务中断,阻塞队列中的任务一个不管。
12.4 TIDYING
SHUTDOWN/STOP转换过来,当前线程池马上关闭。
12.5 TERMINATED
TIDYING转换过来,执行terminated方法转换。
十三、线程池的执行流程
//command是提交过来的线程
public void execute(Runnable command) {
//提交过来的线程不能为为null
if (command == null)
throw new NullPointerException();
//获取核心属性ctl
int c = ctl.get();
//若工作线程个数小于核心线程个数
//满足要求,添加核心工作线程
if (workerCountOf(c) < corePoolSize) {
//addWorker(任务, 任务是核心线程吗)
//addWorker返回true:代表添加工作线程成功
//addWorker返回false:代表添加工作线程失败
//addWorker中会基于线程池状态,以及工作线程个数做判断,查看能否添加工作线程
if (addWorker(command, true))
//工作线程构建出来了,任务也提交给command去处理了
return;
c = ctl.get();
//说明线程池状态或工作线程个数发生了变化,导致添加失败,重新获取一次ctl
}
//添加核心工作线程失败
//判断线程池状态是否为RUNNING,如果是,正常基于阻塞队列的offer方法,将任务添加到阻塞队列
if (isRunning(c) && workQueue.offer(command)) {
//如果任务添加到阻塞队列成功,走if内部
//如果任务在扔到阻塞队列之前,线程池状态改变了,则重新获取ctl
int recheck = ctl.get();
//如果线程池的状态不是RUNNING,将任务从阻塞队列移除
if (! isRunning(recheck) && remove(command))
//并且直接拒绝策略
reject(command);
//在这,说明阻塞队列有我刚放进去的任务
//查看更做线程数是不是0个
//如果工作线程个数为0个,需要添加一个非核心工作线程去处理阻塞队列中的任务
//发生这种情况的有两种:
//1、构建线程池时,核心线程数为0个
//2、即便有核心线程,可以设置核心线程也允许超时
else if (workerCountOf(recheck) == 0)
//为了避免阻塞队列中的任务饥饿,添加一个非核心线程去处理
addWorker(null, false);
}
//任务添加到阻塞队列失败
//构建一个非核心工作线程
else if (!addWorker(command, false))
reject(command);
}
十四、线程池添加工作线程的流程
14.1 addWorker方法
- 校验线程池状态及工作线程个数
- 添加工作线程并且启动工作线程
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
//拿到ctl高3位的值,表示线程池状态
int rs = runStateOf(c);
//=======================================================线程池状态判断=======================================================
//如果线程池状态为SHUTDOWN,并且此时阻塞队列有任务,工作线程个数为0个,则添加一个工作线程去处理阻塞队列中的任务
//判断线程池的状态是否大于等于SHUTDOWN,如果满足则说明线程池状态不是RUNNING
if (rs >= SHUTDOWN &&
//如果这3个条件都满足,就代表要添加核心线程去处理阻塞队列中的任务
//如果3个条件有一个没满足,返回false,配合!,就代表不许要添加
! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
//不需要添加工作线程
return false;
for (;;) {
//======================================================工作线程个数判断======================================================
//基于ctl低29位,代表工作线程个数
int wc = workerCountOf(c);
//工作线程数大于最大值了,不可以添加,返回false
if (wc >= CAPACITY ||
//基于core判断添加的是否为核心工作线程
//是,基于corePoolSize判断
//不是,基于maximumPoolSize判断
wc >= (core ? corePoolSize : maximumPoolSize))
//代表不能添加,工作线程个数不满足要求
return false;
//针对ctl进行+1,采用CAS的方式
if (compareAndIncrementWorkerCount(c))
//CAS成功后,直接退出外层循环,代表可以执行添加工作线程池操作了。
break retry;
//重新获取一次crl值
c = ctl.get();
//判断线程池状态是否改变
if (runStateOf(c) != rs)
//跳出一次外层for循环
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//======================================================添加工作线程及启动过程======================================================
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//构建工作线程,并将任务传递进去
w = new Worker(firstTask);
//获取Worker中的Thread对象
final Thread t = w.thread;
if (t != null) {
//加锁保证wokers成员变量以及对largestPoolSize赋值时线程安全
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
//判断当前线程池状态
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
//校验添加构建的线程是否自己启动了,有则抛出异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//放入Worker对象
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;、
//添加工作线程成功
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//成功则直接启动Worker中的线程
t.start();
workerStarted = true;
}
}
} finally {
//如果启动失败,将其处理掉
if (! workerStarted)
addWorkerFailed(w);
}
//返回工作线程是否启动成功
return workerStarted;
}
十五、线程池为何要构建空任务的非核心线程
15.1 场景
避免因核心工作线程数为0,而阻塞队列中有任务,造成任务饥饿的情况
15.2 原因
- 构建线程时,将核心线程数设置为了0
- 核心线程默认情况下不允许超时,但是如果将allowCoreThreadTimeOut设置为ture,核心线程也会在超时后被干掉
十六、线程池使用完毕后为何必须要shutdown()/shutdownNow()?
16.1 问题:
- 线程池启动的工作线程,本质上也是基于Thread对象,通过start方法启动,占用一个虚拟机栈,会一直占用JVM内存资源,核心工作线程不可能被回收。
- addWorker()中启动的线程为worker中的Thread类,传入了worker对象,因此这里woker对象也永远不会被回收。
- worker对象还属于线程池的内部类,内部类没有被回收,则线程池ThreadPoolExecutor也不会被回收。
16.2 shutdown():
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//修改线程池状态,此时所有阻塞的工作线程前面会增加一个判断,不再接收新的任务
advanceRunState(SHUTDOWN);
//把所有没有正在运行工作任务的线程的中断标记位设置为true
interruptIdleWorkers();
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}
十七、线程池核心参数如何设置?
17.1 主要难点:
任务类型无法控制:CPU密集型(需要不断的调用CPU),IO密集型(需要不断的调用第三方API网络IO、不断查询数据库磁盘IO)、混合型
想调试出符合当前任务情况的核心参数,最好的方式是测试。
尽可能发挥CPU硬件的性能。
17.2 动态监控:
17.2.1 核心参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数(一般情况下不推荐修改)
- workQueue:工作队列
17.2.2 开源工具
hippo4j 可以和SpringBoot整合
Github地址:github.com/opengoofy/h…
十八、ConcurrentHashMap在1.8做了哪些优化?
18.1 存储结构
- JDK1.8中是以CAS+synchronized实现的线程安全
- CAS:在没有hash冲突时(Node要放在数组上时)
- synchronized:出现hash冲突时(Node存放的位置已经有数据了)
- 存储结构:数组+链表+红黑树
- 出现hash冲突后,数据会挂在链表上,此时查询的时间复杂度为(O)n,链表长度过长时,查询效率过低。
- 因此1.8后引入红黑树
- 当里链表长度大于8,数组长度大于等于64时,将链表转为红黑树,(O)logn
18.2 存储操作
锁头结点:锁的粒度增大
数据放在数组上时,用CAS;
数据放在链表上,用synchronized,操作哪个节点,只锁对应的节点,因此数组长度有多少,锁的级别就有多少
18.3 扩容
//链表长度大于等于8时,尝试将链表转为红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
//数组不能为空
if (tab != null) {
//数组的长度n,是否小于64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//如果数组长度小于64,不能将链表转为红黑树,先尝试扩容
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
18.4 计数器
计数器由AtomicLong换成类似于LongAdder的一个功能
18.4.1 AtomicLong:
基于CAS操作实现,并发较高情况下,多个现场同时CAS,只会有一个成功,没有成功的线程会一直执行CAS直到成功为止。
18.4.2 LongAdder:
如果并发执行自增操作时,CAS失败了,会将数据单独存储在一个数组中计数。
十九、ConcurrentHashMap的散列算法?
目的,原本只有低位参与了运算,通过散列算法尽量打撒我的数据,将key的hashCode值的高16位进行^运算,最终又与HASH_BITS进行&运算,尽量平均的分摊到真个map的存储上。
final V putVal(K key, V value, boolean onlyIfAbsent) {
//ConcurrentHashMap不允许key或者value出现null值
if (key == null || value == null) throw new NullPointerException();
//根据key的hashCode计算出一个hash值,后期得出当前key-value要存储在哪个数组索引位置
int hash = spread(key.hashCode());
//一个标识
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
//n:数组长度
//i:当前索引的位置
//f:当前数组i索引位置的Node对象
//fn:当前数组i索引位置上数据的hash值
Node<K,V> f; int n, i, fh;
//判断当前数组是否还没有初始化
if (tab == null || (n = tab.length) == 0)
//将数组进行初始化
tab = initTable();
//基于(n-1) & hash 计算出当前Node需要存放在哪个索引位置
//基于tabAt获取到i位置的数据
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//现在数组的i位置上没有数据,基于CAS的方式将数据存放在i上
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
//如果成功,执行break跳出循环,插入数据成功
break; // no lock when adding to empty bin
}
//判断当前位置数据是否正在扩容
else if ((fh = f.hash) == MOVED)
//让当前插入数据的线程协助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
static final int HASH_BITS = 0x7fffffff;
//计算当前Node的hash值的方法
static final int spread(int h) {
//将key的hashCode值的高16位进行^运算,最终又与HASH_BITS进行&运算
//将最高位的hash也参与到计算索引位置的运算中
//为什么HashMap、ConcurrentHashMap都要求数组长度为2^n
//HASH_BITS让hash值的最高位符号位肯定为0,代表当前hash值默认情况下一定为正数,因为hash值为负数时,有特殊的含义
//static final int MOVED = -1; // 代表当前hash位置的数据正在扩容!
//static final int TREEBIN = -2; // 代表当前hash位置下挂载的是一个红黑树
//static final int RESERVED = -3; // 预留当前索引位置
return (h ^ (h >>> 16)) & HASH_BITS;
//计算数组放在哪个索引位置的方法(f = tabAt(tab, i = (n-1) & hash))
//n 是数组的长度
}
二十、ConcurrentHashMap初始化数组的流程?
懒加载:第一次添加数据时,才会初始化数组。
//数组在初始化和扩容操作时的一个控制变量
//-1:代表当前数组正在初始化
//小于-1:低16位代表当前数组正在扩容的线程个数(如果1个线程扩容,值为-2;如果2个线程扩容,值为-3)
//0:代表数组还没初始化
//大于0:代表当前数组的扩容阈值,或者是当前数组的初始化大小
private transient volatile int sizeCtl;
//初始化数组方法
private final Node<K,V>[] initTable() {
//声明标识
Node<K,V>[] tab; int sc;
//再次判断数组没有初始化,并且完成tab赋值
while ((tab = table) == null || tab.length == 0) {
//将sizeCtl赋值给sc变量,并判断是否小于0
if ((sc = sizeCtl) < 0)
Thread.yield();
// 可以尝试初始化数组,线程会以CAS的方式,将sizeCtl修改为-1,代表当前线程可以初始化数组
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//尝试初始化
try {
//再次判断当前数组是否已经初始化完毕
if ((tab = table) == null || tab.length == 0) {
//开始初始化
//如果sizeCtl > 0 ,就初始化sizeCtl长度的数组
//如果sizeCtl ==0 ,就初始化默认的长度
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
//初始化数组!
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
//将初始化的数组nt,赋值给tab和table
table = tab = nt;
//sc赋值为了数组长度 - 数组长度 右移 2位 16-4=12
//将sc赋值为下次扩容的阈值
sc = n - (n >>> 2);
}
} finally {
//将赋值好的sc,设置给sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
二十一、ConcurrentHashMap扩容的流程?
- 确认什么时候触发扩容
- 计算扩容标识戳
- 将扩容表示戳往左移16位+2(代表第一个进来扩容的)
- 计算每个线程迁移的长度
- 初始化一个全新的数组
- 线程领取任务,从多少索引位置迁移到多少索引位置
- 开始扩容、迁移数据
- 判断是否为最后一个完成迁移的
- 如果是则全局检查一次有没有遗漏的数据
//链表长度大于等于8时,尝试将链表转为红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
//数组不能为空
if (tab != null) {
//数组的长度n,是否小于64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//如果数组长度小于64,不能将链表转为红黑树,先尝试扩容
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
21.2 tryPresize方法-针对putAll初始化操作
//size是将之前的数组长度 左移1位得到的结果
private final void tryPresize(int size) {
//如果扩容的长度达到了最大值,就使用最大值
//否则需要保证数组的长度为2的n次幂
//这块的操作,是为了初始化操作准备的,因为调用putAll方法时,也会触发tryPresize方法
//如果刚刚new的ConcurrentHashMap直接调用饿了putAll方法,会通过tryPresize方法进行初始化
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
//这些代码和initTable一模一样
//声明sc
int sc;
//将sizeCtl的值赋给sc,并且判断是否大于0,这里代表没有初始化操作,也没有扩容操作
while ((sc = sizeCtl) >= 0) {
//将ConcurrentHashMap的table值赋值给tab,并声明数组长度n
Node<K,V>[] tab = table; int n;
//数组是否需要初始化
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
//计算扩容表示戳,根据当前数组的长度计算一个16位的扩容戳
//第一个作用是为了保证后面的sizeCtl赋值时,保证sizeCtl为小于-1的负数
//第二个作用用来记录当前是从什么长度开始扩容的
int rs = resizeStamp(n);
//如果sc小于0,代表有线程正在扩容
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//代表没有线程正在扩容,我是第一个
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
21.3 transfer方法计算每个线程迁移的长度
//开始扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
//n = 数组长度
//stride = 每个线程一次性迁移多少数据到新数组
int n = tab.length, stride;
//基于CPU内核数组来计算,每个线程一次性迁移多少长度的数据最合理
//NCPU=4
//例:数组长度为1024 - 512 - 256 - 128 / 4 = 32
//MIN_TRANSFER_STRIDE = 16,为每个线程迁移数据的最小长度
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 根据CPU计算每个线程一次迁移多长的数据到新数组,如果结果大于16,使用计算结果;
//如果小于16,就使用最小长度。
}
二十二、ConcurrentHashMap读取数据的流程?
- 不会阻塞读取数据的线程。
- 先判断当前key对应的value,是否在数组上。
- 其次判断当前位置是否属于特殊情况:数据被迁移、位置被占用、红黑树结构。
- 最后判断链表上是否有对应的数据。
22.1 get方法-查询数据的入口
public V get(Object key) {
//tab:数组;e:查询指定位置的节点;n:数组长度
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//基于传入的key,计算hash值
int h = spread(key.hashCode());
//数组不为null,数组得有数据,拿到指定位置的数组上的数据
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
//数组上数据的hash值,是否和查询条件key的hash值一样
if ((eh = e.hash) == h) {
//key的==或者equals是否一致,一致则数组上就是要查询的数据
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果数组上的数据的hash值为负数,有特殊情况
else if (eh < 0)
//三种情况:数据迁移、节点位置被占、红黑树结构
return (p = e.find(h, key)) != null ? p.val : null;
//肯定走链表操作
while ((e = e.next) != null) {
//如果hash值一致,并且key的==或者equals一致,返回当前链表位置的数据
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
//如果上述三个流程都没有找到指定key对应的value,那就是key不存在,返回null
return null;
}
数据迁移:
Node<K,V> find(int h, Object k) {
//找到迁移后的新数组
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
红黑树:
final Node<K,V> find(int h, Object k) {
if (k != null) {
for (Node<K,V> e = first; e != null; ) {
int s; K ek;
//判断你是否正在有写的操作或者有写的操作在等待
//查询转换红黑树时保留的双向链表
if (((s = lockState) & (WAITER|WRITER)) != 0) {
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
//如果已经没有写操作在进行了,直接去红黑树上查找
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
二十三、ConcurrentHashMap中计数器的实现
- addCount方法中:记录Map中存储了多少个元素
- 为防止效率过低,
- 除了baseCount之外,还准备了多个CounterCell数组,
- 最后将baseCount的值以及每个CounterCell数组中值进行累加
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//baseCount:记录某个元素组个数
//并发量较大时,向 CounterCell[] as中添加数据
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
//判断是否已经向CounterCell[] as中添加数据
if (as != null) {
//遍历数组,每一个值使用+=方式存储近sum
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}