并发编程篇复习总结


别问 问就是为了面试豁出了老命

进程,协程,线程基础概念

能否解释进程,线程,协程的关系?

进程是一个程序代码运行所执行的一个程序,但是一个进程可以包含多个线程,在单核cpu下,Java默认多线程可以以一种抢占式的方式执行一种并发状态,协程是近些年走进视野的,以GO语言为代表可以操作协程,一个线程中可以包含更多的协程,可以简单的说线程包含协程。

协程对于多线程有什么优缺点吗?

  1. 首先是更小的协程可以在不使用内核的前提下进行上下文切换
  2. 一个线程就可以完成高并发的任务,对高并发的支持更好
  3. 协程在一个线程下,是不用考虑数据的读写不一致问题(读写变量冲突问题)
  4. 缺点: 缺点也很明显,本质还是一个单线程,不能利用多核资源,同时也不独立,需要线程,进程配合才可以运行

并行和并发的区别是什么?

  1. 并行是指多个程序 同时多个一起运行
  2. 并发是指多个程序在某一个时间段内交替的快速运行,宏观是有点类似并行,但是实际上是交替运行
  3. 恶补英语之==> 并发 (concurrency) 并⾏ parallellism

多线程基础之实现(学了这么多年还真的第一次这么认真的去研究该怎么写)

Java线程创建的几种方式

  1. 继承extends

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.runindark.ways;

    public class ThreadByThread extends Thread{

    public void SayHello(){
    System.out.println("Thread by extend Thread");
    System.out.println(Thread.currentThread().getName());
    }

    @Override
    public void run() {
    SayHello();
    }

    public static void main(String args[]){

    ThreadByThread threadByThread = new ThreadByThread() ;
    threadByThread.start();



    }
    }
  2. 使用Runnable

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.runindark.ways;

    public class ThreadByRunnable implements Runnable {
    @Override
    public void run() {
    System.out.println("Thread create by implements Runnable");
    }


    public static void main(String[] args){

    ThreadByRunnable ta = new ThreadByRunnable() ;
    new Thread(ta).start();


    }
    }
  3. 使用CF

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package com.runindark.ways;

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;

    public class ThreadByCF implements Callable {

    @Override
    public Object call() throws Exception {
    return "Thread create by CF" +":" +Thread.currentThread().getName();
    }

    public static void main(String[] args){
    FutureTask<Object> futureTask = new FutureTask<>(new ThreadByCF()) ;
    Thread thread = new Thread(futureTask);
    thread.setName("Cf");
    thread.start();
    try {
    System.out.println(futureTask.get());
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (ExecutionException e) {
    e.printStackTrace();
    }

    }


    }
  4. 使用线程池

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.runindark.ways;

    import javax.print.DocFlavor;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    public class ThreadByPool {

    public static void main(String[] args){

    ExecutorService service = Executors.newFixedThreadPool(5);

    for (int i=0;i<10;i++){

    service.execute(new ThreadByThread());
    }

    System.out.println(Thread.currentThread());
    service.shutdown();
    }
    }

线程基本原理

线程有哪些状态

创建 -> 就绪 -> 执行 -> 消亡
阻塞: 分为同步组织,等待阻塞。 等待阻塞就是wait啦,sleep啦。同步阻塞就是sychronize锁被占用,另一个线程也需要占用这个锁,结果凉了,就阻塞了。

线程的基本一些方法

  1. sleep

    就是进入了等待阻塞队列中,根据设定的时间阻塞,而且不会释放锁,他的阻塞状态就是time_waiting

  2. yield

    就是让线程立马停止一下,但是不会进入阻塞,而是直接进入就绪,且不会释放锁

  3. join

    谁调用join谁先执行,然后再执行被停用的线程

  4. wait

    就是进入等待状态,而且必须有人去唤醒他,但是wait会释放锁,也可以wait(time)来通过时间唤醒

  5. notify

    随机的唤醒任意的一个被wait的线程

  6. notifyall

    把wait的线程,全部唤醒

volatile

说说volatile的与sychronize的区别

  1. volatile 不是原子性的,sychronize是原子性的
  2. volatile和sychronize都保证了可见性
  3. volatile是禁止了指令重排的
  4. 不能写入 不能修饰写⼊操作依赖当前值的变量,⽐如num++、num=num+1

为啥会出现脏读的问题?

这个是JMM(java内存模型) 导致的,java线程中不是所有的变量都是在主存的,而是每个线程都有自己的一丢丢空间,对于修改的变量的操作,先从主存拿到,再修改,再写回去,如果多线程,可能因为速度问题,写入的时间啥的有差别,所以就会导致把数据脏读了。

为啥volatile可以解决这个问题呢?

volatile就像是一个敏感的报警灯一样,一旦有人妄图修改volatile修饰的数据,立马报警通知修改情况,所以说原子性差了点,但是可见性或者说是共享性好的鸭匹

指令重排

  • 程序次序原则:无论怎么重排,都不会影响最终的运行结果
  • 管程(Synchronized)序锁定原则:锁L被线程A释放,之后又被线程B获取,则线程B可见线程A在他之前获取到锁
  • volatile变量原则: 共享变量可以所有线程中都可见
  • 线程启动原则: 启动线程A后,线程A中还有线程B也会被启动,则线程B可以看到线程A的修改结果
  • 线程终止原则: 启动线程A后,线程A中还有线程B也会被启动,B在结束以前,线程A可以看到线程B的修改结果
  • 线程中断原则: Intercept()中断的线程,Thread.Intercept()可见线程是否被中断
  • 传递原则:A happen-before B,B happen-before C,C happen-before A
  • 对象终结原则: 开始的一定是构造函数,结束的一定是finalize()

并发编程三要素

  • 原子性
  • 有序性
  • 可见性

    悲观锁

    每次读写数据都是悲观的,认为可能会出现数据被其它线程读的问题,所以要上锁比如sychronized

    乐观锁

    每次读取数据都觉得是乐观的,觉得不会有其它线程更改要读取的数据

    公平锁

    就是大家人人平等,都可以拿到锁,阻塞队列中按照顺序慢慢来=》reetrantlock(fair)

    非公平锁

    不公平的,只要你条件符合,就可以直接拿到锁=》reetrantlock(unfair)
    reetrantlock其实底层就是一个队列,所以也是先来先服务那种,在公平锁体现的很好

    重入锁

    一个线程里吧,还调用另一个线程,然后这个锁对里面的这个线程也生效

    不可重入锁

    一个线程里吧,还调用另一个线程,然后吧,里面这个线程就不能用这个锁了,就只能乖巧的滚去阻塞队列了

    自旋锁

    就是想不开的锁,只要没条件获取到锁,就一直自旋,也就是一直去判断条件看看自己能不能获得锁子,while(flag)的感觉,除非获得锁才能结束,但是注意,自旋锁消耗cpu,毕竟在那转来转去的。
    不会发⽣线程状态的切换,⼀直处于⽤户态,减少了线程上下⽂切换的消耗,缺点是循环会消耗CPU

    共享锁

    也就是读锁,或者是S锁,就是可以让大家读取,查看,就是不能修改

    排他锁

    也就是霸占一把锁,只要这个线程占着,别人就不能去获取这个锁,但是只要霸占这个锁,能读能写

    死锁

    资源抢占矛盾循环了,无外力介入,是解不开的

关于jvm自己内部的几个锁

偏向锁

就是如果哪个线程一直用着这个锁,就一直让他先用,更少的消耗量

轻量级锁

如果其他锁妄图获得人家那个偏向锁,那就自旋吧,等人家用完才给你

重量级锁

自选锁也不自旋了,直接阻塞进化成重量级锁,重量级锁会让其他申请的线程进⼊阻塞,性能也会降低

死锁

死锁代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.runindark.deadlock;

public class DeadLock {

private static final String locka = "A" ;
private static final String lockb = "B" ;

public static void LockA(){
synchronized (locka){


System.out.println("entre the locka");

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lockb){
System.out.println("a 取 b");
}

}

}

public static void LockB(){

synchronized (lockb){

System.out.println("entre the lockb");
synchronized (locka){
System.out.println("b 取 a");
}

}

}

public static void main(String[] args){

for(int i=0;i<10;i++){

System.out.println(i+1 + "次");
new Thread(()->{
DeadLock.LockA();
}).start();

new Thread(()->{
DeadLock.LockB();
}).start();
}
}
}

解锁代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.runindark.deadlock;

public class DeadLock {

private static final String locka = "A" ;
private static final String lockb = "B" ;

public static void LockA(){
synchronized (locka){


System.out.println("entre the locka");

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lockb){
System.out.println("a 取 b");
}

}

}

public static void LockB(){

synchronized (lockb){
System.out.println("entre the lockb");

}

synchronized (locka){
System.out.println("b 取 a");
}

}

public static void main(String[] args){

for(int i=0;i<10;i++){

System.out.println(i+1 + "次");
new Thread(()->{
DeadLock.LockA();
}).start();

new Thread(()->{
DeadLock.LockB();
}).start();
}
}
}

改变运行策略,其实是线程A中syc获取了锁a,还要获取suob,这样子顺序执行下来是ok 的,就怕线程a获取了锁a后,线程b抢占获取了锁b,此使线程a还要锁b
就阻塞了,所以到了线程b又要获取锁a,那么就死锁了
解决方法也简单,就是让一个锁提早消失就好了,所以改变一下sychronize的次序,提早释放锁,就万事大吉了

死锁的四个条件

互斥条件:资源不能共享,只能由⼀个线程使⽤
请求与保持条件:线程已经获得⼀些资源,但因请求其他资源发⽣阻塞,对已经获得的资源保持不释放
不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强⾏回收,只能由线程使⽤完⾃⼰释放
循环等待条件:多个线程形成环形链,每个都占⽤对⽅申请的下个资源

重入锁和不可重入锁

不可重入锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.runindark.crlock;

public class BcrLock {

public boolean flag = false ;

public synchronized void lock() throws InterruptedException {

if (!flag){
System.out.println("进入加锁");
flag = true ;
}else {

while (flag){
System.out.println(Thread.currentThread().getName() + "进入等待状态");
wait();
}
}

}

public synchronized void unlock(){

System.out.println("进入解锁");
notify();
flag = false ;

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.runindark.crlock;

import org.omg.Messaging.SYNC_WITH_TRANSPORT;

public class TestMain {

private BcrLock bcrLock = new BcrLock() ;

public void methodA(){

try {
bcrLock.lock();
System.out.println("方法A加锁" + bcrLock.flag);

} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bcrLock.unlock();
}
}

public void methodB(){

try {
bcrLock.lock();
System.out.println("方法B加锁" + bcrLock.flag);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bcrLock.unlock();
}
}

public static void main(String[] args){

new TestMain().methodA();
}
}

重入锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.runindark.crlock;

import jdk.nashorn.internal.ir.Block;

public class CrLock {

public boolean islock = false ;
public String currentThread = null ;

public synchronized void lock() throws InterruptedException {

if (currentThread==null) {
currentThread = Thread.currentThread().getName();
}else {

if (currentThread.equals(Thread.currentThread().getName())){
System.out.println(Thread.currentThread().getName() + "成功加锁");
}else {
while (!currentThread.equals(Thread.currentThread().getName())){
System.out.println(Thread.currentThread().getName() + "加锁失败");
wait();
}
}
}
}

public synchronized void unlock(){
notify();
currentThread = null ;
System.out.println(Thread.currentThread().getName() + "成功解锁");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.runindark.crlock;

import sun.awt.windows.ThemeReader;

public class TestMainB {

private CrLock crLock = new CrLock();

public void methodA(){

try {
crLock.lock();
System.out.println("方法A加锁" + crLock.currentThread);
methodB();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("方法a解锁" );
crLock.unlock();
}
}

public void methodB(){

try {
crLock.lock();
System.out.println("方法B加锁"+ crLock.currentThread );
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("方法b解锁" );
crLock.unlock();
}
}

public static void main(String[] args){

new Thread(()->{
new TestMainB().methodA();
}).start();

new Thread(()->{
new TestMainB().methodA();
});
}
}

synchronized说说看

非公平锁,原子性,可重入可以修饰代码块和方法
每个对象有⼀个锁和⼀个等待队列,锁只能被⼀个线程持有,其他需要锁的线程需要阻塞等待。锁被释放
后,对象会从队列中取出⼀个并唤醒,唤醒哪个线程是不确定的,不保证公平性
jdk6优化-> 偏向锁->轻量级锁->重量级锁

CAS

什么是CAS?

CAS是一种乐观锁,CompareAndSwap,也就是比较再交换
执行过程大概如下: 首先是 内存地址V,预期原值A,新值B , 如果线程A过来,V = A ,则满足条件把目标值更换成B,如果线程B过来,V != A,那么无法
将目标值更换成B,而且线程B将进行自旋,直到 A=V ,结束自旋,获取锁
缺点也将显而易见: 自旋锁的存在直接导致了cpu的消耗问题
CAS是原子性的

ABA问题

简单来说就是线程在操作过程中,有其它线程将该变量更改后,又有另一个线程把他改回来,到最开始线程操作的时候,发现该值没有变化,则该线程操作成
功。加一个版本号可以解决问题,每次修改时都要查看版本号

AQS

AQS本质上是为了解决线程安全所提出的一种解决方案的抽象
AQS的组成

  • 程序计数器
  • 阻塞队列
  • 线程标记

阻塞队列

是一个双向的链表,概念上的队列,但不是真正的实现也是队列

条件队列

是根据condition创建出来的队列,上锁后可以负责对线程的监视,比synchronized的监视器更加灵活,是一个单向的链表,当唤醒界节点的时候会直接添加到阻塞队列中

Node节点

1. waitstatus

  1. CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

  2. SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

  3. CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  4. PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态

    2. prev

    前驱节点

    3. next

    后继节点

    4. thread

    thread 同步线程队列主要存储的线程信息。

    5. nextwaiter

    AQS中阻塞队列采用的是用双向链表保存,用prve和next相互链接。而AQS中条件队列是使用单向列表保存的,用
    nextWaiter来连接。阻塞队列和条件队列并不是使用的相同的数据结构

精髓原帖

在Node节点的源码中有两个常量属性

// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 其他模式
// 其他非空值:条件等待节点(调用Condition的await方法的时候)

部分核心方法

acquire(int arg) 源码讲解,好⽐加锁lock操作

  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回,AQS⾥⾯未实现但没有定义成
    abstract,因为独占模式下只⽤实现tryAcquire-tryRelease,⽽共享模式下只⽤实现
    tryAcquireShared-tryReleaseShared,类似设计模式⾥⾯的适配器模式
  2. addWaiter() 根据不同模式将线程加⼊等待队列的尾部,有Node.EXCLUSIVE互斥模式、
    Node.SHARED共享模式;如果队列不为空,则以通过compareAndSetTail⽅法以CAS将当前线程
    节点加⼊到等待队列的末尾。否则通过enq(node)⽅法初始化⼀个等待队列
  3. acquireQueued()使线程在等待队列中获取资源,⼀直获取到资源后才返回,如果在等待过程
    中被中断,则返回true,否则返回false

    release(int arg)源码讲解 好⽐解锁unlock

    独占模式下线程释放指定量的资源,⾥⾯是根据tryRelease()的返回值来判断该线程是
    否已经完成释放掉资源了;在⾃义定同步器在实现时,如果已经彻底释放资源(state=0),要返回
    true,否则返回false
    unparkSuccessor⽅法⽤于唤醒等待队列中下⼀个线程

ReentrantLock实现原理

实现大致的思路是和AQS是一致的,ReentrantLock的实现是分为公平锁和非公平锁的,其中上层Lock(Accquire),Unlock(Release)上层一致,唯独在释放的时候有一点区别,公平锁的实现是直接去队列中去找,看看队列中是否有等待,如果有等待的话则排队,无等待的话就直接给锁,对应的方法也就是TryAccquire() ,而非公平锁则直接判断是不是符合获取锁的条件CompareAndState,如果符合直接给锁,如果不符合,则是按照公平锁的方法处理

ReentrantLock和synchronized区别是什么?

  1. ReentrantLock和synchronized都是独占锁
  2. synchronized:

    · 是悲观锁会引起其他线程阻塞,java内置关键字,
    · ⽆法判断是否获取锁的状态,锁可重⼊、不可中断、只能是⾮公平
    · 加锁解锁的过程是隐式的,⽤户不⽤⼿动操作,优点是操作简单但显得不够灵活
    · ⼀般并发场景使⽤⾜够、可以放在被递归执⾏的⽅法上,且不⽤担⼼线程最后能否正确
    释放锁
    · synchronized操作的应该是对象头中mark word,参考原先原理图⽚

  3. ReentrantLock:

    · 是个Lock接⼝的实现类,是悲观锁,
    · 可以判断是否获取到锁,可重⼊、可判断、可公平可不公平
    · 需要⼿动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
    · 在复杂的并发场景中使⽤在重⼊时要却确保重复获取锁的次数必须和重复释放锁的次数⼀样,否则可能导致 其他线程⽆法获得该锁。
    · 创建的时候通过传进参数true创建公平锁,如果传⼊的是false或没传参数则创建的是⾮公平锁
    · 底层不同是AQS的state和FIFO队列来控制加锁

读写锁ReentrantReadWriteLock

核心其实是将锁分成了读锁写锁两种,其中写锁可以转换成读锁(锁降级),但是读锁不能是写锁

  • 读锁是利用了AQS中的共享锁,而写锁则是独占锁
  • 用高16位用来表示读锁占有的线程数量,用低16位表示写锁被同一个线程申请的次数
  • 利用Sync实现
    • FairSync
      • hasQueuedPredecessors() 查看前面是否有就绪的线程
    • NonfairSync
      • 判断读锁是否要阻塞,是通过阻塞队列前面是不是写锁,如果是写锁则阻塞。

阻塞队列BlockingQueue

j.u.c包下的提供了线程安全的队列访问的接⼝,并发包下很多⾼级同步类的实现都是基于阻塞队列实现的
1、当阻塞队列进⾏插⼊数据时,如果队列已满,线程将会阻塞等待直到队列⾮满
2、从阻塞队列读数据时,如果队列为空,线程将会阻塞等待直到队列⾥⾯是⾮空的时候

ArrayBlockingQueue:

基于数组实现的⼀个阻塞队列,需要指定容量⼤⼩,FIFO先进先出顺序

LinkedBlockingQueue:

基于链表实现的⼀个阻塞队列,如果不指定容量⼤⼩,默认Integer.MAX_VALUE, FIFO先进先出顺序

PriorityBlockingQueue:

⼀个⽀持优先级的⽆界阻塞队列,默认情况下元素采⽤⾃然顺序升序排序,也可以⾃定义排序实现 java.lang.Comparable接⼝

DelayQueue:

延迟队列,在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素,⾥⾯的对象必须实现 java.util.concurrent.Delayed 接⼝并实现
CompareTo和getDelay⽅法

ConcurrentLinkedQueue

并发队列ConcurrentLinkedQueue是基于链表实现的⽆界线程安全队列,采⽤FIFO进⾏排序
保证线程安全的三要素:原⼦、有序、可⻅性
1、底层结构是Node,链表头部和尾部节点是head和tail,使⽤节点变量和内部类属性使⽤
volatile声明保证了有序和可⻅性
2、插⼊、移除、更新操作使⽤CAS⽆锁操作,保证了原⼦性

线程池

提⾼系统资源的使⽤率,同时避免过多资源竞争,避免堵塞,且可以定时定期执⾏、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能

线程池分类

newFixedThreadPool

⼀个定⻓线程池,可控制线程最⼤并发数

newCachedThreadPool

⼀个可缓存线程池

newSingleThreadExecutor

⼀个单线程化的线程池,⽤唯⼀的⼯作线程来执⾏任务

newScheduledThreadPool

⼀个定⻓线程池,⽀持定时/周期性任务执⾏

线程池踩坑

推荐ThreadPoolExecutor的⽅式原因

  1. newFixedThreadPool和newSingleThreadExecutor:

    队列使⽤LinkedBlockingQueue,队列⻓度为 Integer.MAX_VALUE,可能造成堆积,导致OOM

  2. newScheduledThreadPool和newCachedThreadPool:

    线程池⾥⾯允许最⼤的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM

    核心参数

    corePoolSize:

    核⼼线程数,线程池也会维护线程的最少数量,默认情况下核⼼线程会⼀直存活,即使没有任务也不会受存keepAliveTime控制
    坑:在刚创建线程池时线程不会⽴即启动,到有任务提交时才开始创建线程并逐步线程数⽬达到corePoolSize

    maximumPoolSize:

    线程池维护线程的最⼤数量,超过将被阻塞
    坑:当核⼼线程满,且阻塞队列也满时,才会判断当前线程数是否⼩于最⼤线程数,才决定是否创建新线程

    keepAliveTime:

    ⾮核⼼线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于corePoolSize

    unit:

    指定keepAliveTime的单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS

    workQueue:

    线程池中的任务队列,常⽤的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue

    threadFactory:

    创建新线程时使⽤的⼯⼚

    handler:

    RejectedExecutionHandler是⼀个接⼝且只有⼀个⽅法,线程池中的数量⼤于maximumPoolSize,对拒绝任务的处理策略,默认有4种策略AbortPolicy、
    CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

线程池的选择

  1. 针对高并发,执行时间短的任务,线程池的核心线程数应为CPU核数+1,减少上下文切换
  2. 针对并发不高,但是执行时间比较长的业务
    • IO密集型,因为IO操作不占用CPU,所以不必要让CPU闲下来,应该设置较多线程数
    • 计算密集型,因为计算需要大量占用CPU,则应该较少的设置核心线程数

ThreadLoacl

ThreadLocal数据结构.jpg

  • 每一个线程自己维护了ThreadLocalMap
    • ThreadLocalMap的key是ThreadLocal对象本身 (是弱引用,检测到就会gc回收掉)
    • Value则是ThreadLocal中的值
  • ThreadLoaclmap的桶的算法
    • int i = key.threadLocalHashCode & (len-1);
      • key则是threadlocal
      • threadLocalHashCode是threadlocal自己维护的
      • threadLocalHashCode 是 每次添加一个对象后内部再加上一个斐波那契数
      • 之后与容量做与运算,得到桶的位置
  • ThreadLocalMap.set()其实有四种情况需要考虑
    1. 通过hash计算得到的槽的数据是为空的(key为空,entry为空,也就是没有被占用)
      • 直接插入槽位
    2. 通过hash计算发现得到的槽位的数据不为空,key不为空,entry也不为空,同时hash计算的key和找到的key是一样的
      • 直接在槽位上更新数据
    3. 通过hash计算发现得到的槽位的数据都不为空,且key是不一样的,同时在找到key,entry为空的槽位之前,没有遇到key过期,entry不为空的情况
      • 由于threadlocalmap的hash冲突是向后找空插入的
      • 因此直接向后查找,找到空的槽位直接插入就好
      • 如果是向后查找的过程中有了key相同的,也可以直接更新数据
    4. 通过hash计算发现得到的槽位的数据都不为空,且在找到key,entry为空的槽位之前,遇到了key过期,entry不为空的情况
      • 由于ThreadlocalMap的key是维护的弱引用,因此会出现key被回收的情况
      • 会启用探测
        探测A.jpg
        往后遍历过程中,散列数组下标为7位置对应的Entry数据key为null,表明此数据key值已经被垃圾回收掉了
        1. 执行replaceStaleEntry()
          • 初始化探测式清理过期数据扫描的开始位置:slotToExpunge = staleSlot = 7
        2. 以stableSlot为起点,向前进行探测
          • 如果向前探测的过程中,遇到了key为null的,entry不为null的,也就是key过期的值,则更新slotToExpunge的值
          • 直到遇到空槽位,也就是key,entry都为null的位置,则停止向前的探测
        3. 之后执行stableSlot向后的探测 (又分成两钟情况)
          • 找到了key值相同的槽位
            • 则直接更新数据,且更新stableslot的下标
            • 然后进行过期清理,从slotToExpunge到staleSlot进行清理
              探测B.jpg
          • 没有找到key值相同的槽位
            • 则向后继续查找,直到直到空槽位(key,entry都为空)则停止
              探测C.jpg
            • 之后再执行过期清理,从slotToExpunge到staleSlot进行清理
  • ThreadLocalmap清理过程
    • cleanSomeSlots()启发式清理
      • 启发式清理就是把i后移,直到全部探测清理完毕
    • expungeStaleEntries()探测式清理
      • 从index向后探测,如果是过期key则删除,同时size–,直到遍历到空槽
      • 之后对散列表重hash,把之前因为hash冲突而放到后面的key-value放到更接近槽的位置
    • 实际上是cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

原子类

AtomicInteger 为例

  • volatile保证了数据的可见性,但是没有保证原子性
  • 因此使用CAS保证了volatile的原子性,也就是compareAndSwapInt()方法实现了CAS