《天净沙·我·并发编程篇》 双非菜鸡奇葩,面试项目框架,java java,卑微学子去哪?


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

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

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

进程是一个程序代码运行所执行的一个程序,但是一个进程可以包含多个线程,在单核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

    大发慈悲,冷宫的全放出来了

线程不安全的同步容器比如?

HashMap、ArrayList、LinkedList

volatile

说说volatile的与sychronize的区别

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

为啥会出现脏读的问题?

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

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

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

指令重排/happens-before

啥是指令重排?

为了提高程序的执行效率,再不改变运行结果的前提下,jvm对字节码文件的指令重新排序,以提高效率

知道 happens-before吗,能否简单解释下?

由于多线程的特殊性,对jvm的指令重排有严格的限制,所以有了happens-before的原则(先行发生原则) ==>(这个解释靠谱:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。)

划重点:这些规则一并奉上

  1. 程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!

  2. 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)

  3. volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

  4. 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。

  5. 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。

  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。

  7. 传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。

  8. 对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

并发编程进阶

并发编程三要素:

1. 原子性

要么全成功,要么全失败
例如num++ 这种操作就是非原子性的 可以用lock或者Sychronized来修饰这个操作

1
2
3
public synchronized void add2(){
num++;
}
1
2
3
4
5
6
7
8
9
Lock lock = new ReentrantLock();
public void add1(){
lock.lock();
try {
num++;
}finally {
lock.unlock();
}
}

2. 可见性

3. 有序性

进程的执行顺序

先来先服务调度算法:

按照作业/进程到达的先后顺序进⾏调度 ,即:优先考虑在系统中等待时间最⻓的作业
排在⻓进程后的短进程的等待时间⻓,不利于短作业/进程

短作业优先调度算法:

短进程/作业(要求服务时间最短)在实际情况中占有很⼤⽐例,为了使得它们优先执⾏
对⻓作业不友好

⾼响应⽐优先调度算法:

在每次调度时,先计算各个作业的优先权:优先权=响应⽐=(等待时间+要求服务时间)/
要求服务时间,
因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应⽐=响应
时间/要求服务时间,选 择优先权⾼的进⾏服务需要计算优先权信息,增加了系统的开销

时间⽚轮转调度算法:

轮流的为各个进程服务,让每个进程在⼀定时间间隔内都可以得到响应
由于⾼频率的进程切换,会增加了开销,且不区分任务的紧急程度

优先级调度算法:

根据任务的紧急程度进⾏调度,⾼优先级的先处理,低优先级的慢处理
如果⾼优先级任务很多且持续产⽣,那低优先级的就可能很慢才被处理

线程的调度算法

协同式线程调度(分时调度模式):

线程执⾏时间由线程本身来控制,线程把⾃⼰的⼯作执⾏完之后,
要主动通知系统切换到另外⼀个线程上。最⼤好处是实现简单,且切换操作对线程⾃⼰是可知的,没
啥线程同步问题。坏处是线程执⾏时间不可控制,如果⼀个线程有问题,可能⼀直阻塞在那⾥

(佛系顺序执行,自己完事叫下一个继续)

抢占式线程调度:

每个线程将由系统来分配执⾏时间,线程的切换不由线程本身来决定(Java中,
Thread.yield()可以让出执⾏时间,但⽆法获取执⾏时间)。线程执⾏时间系统可控,也不会有⼀
个线程导致整个进程阻塞

悲观锁

每次读写数据都是悲观的,认为可能会出现数据被其它线程读的问题,所以要上锁比如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的消耗问题

ABA 问题由于CAS不保证原子性问题

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

AQS

什么是AQS

就是 java.util.concurrent (juc) 的一个工具包 全称是 AbstractQueuedSynchronizer (抽象同步队列)
其中核心就是 一个int变量(用于表示同步状态),一个state(计数器),一个线程标记(谁加锁),一个阻塞队列(用于存储未拿到锁阻塞中的线程)

部分核心方法

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 读写锁

读写锁其实就是一个分离的ReentrantLock,由于ReentrantLock无论读或者写都是加锁的,但是读其实是安全的,盲目加锁只能过多的消耗资源,所以应该
只有写是独占锁,写则是排他锁,这样子才可以优化资源,流程一般是获取写锁,获取读锁,释放写锁,释放读锁。
虽然也是重入锁,但是写可以重入读或者写,但是读就只能重入读

阻塞队列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

ThreadLocal

博文地址