未经允许所有内容都可都可都可转载,不对的地方欢迎指正
ThreadLocal
ThreadLocal是什么
- ThreadLocal叫线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。
- 该变量属于当前线程独有的变量,ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部变量的副本。
- ThreadLocal变量,在不同的Thread中有不同的副本
1) 每个Thread内有自己的实例副本,该副本只能由当前Thread使用,这也是ThreadLocal命名由来。 2) 既然每个Thread有自己的实例副本,且其他Thread不可以访问,那就不存在多线程之间的共享问题。
ThreadLocal与Synchronized区别, 不一起使用!!!
- ThreadLocal
实际上是与线程绑定一个变量。 - ThreadLocal与Synchronized都用于解决多线程的并发访问。
1) Synchronized用于线程间的数据共享 2)ThreadLocal用于多线程间的数据隔离 3) Synchronized是利用锁机制,是变量或者代码块某一时刻只能被一个线程访问 4) ThreadLocal为每个线程都提供了变量的副本,使每个线程访问到的并不是同一个对象,这样就隔离了多个线程对数据的共享。
- 向Thread里面存东西就是向他里面的Map中存东西,然后ThreadLocal把这个Map挂到当前线程底下,这样map就只属于这个线程了。
ThreadLocal的简单使用
public class Test {private static ThreadLocal<String> threadLocal= new ThreadLocal<>();public static void main(String[] args) {new Thread(){@Overridepublic void run() {threadLocal.set("thread_1");System.out.println(threadLocal.get());threadLocal.remove();System.out.println(threadLocal.get());}}.start();new Thread(){@Overridepublic void run() {threadLocal.set("thread_2");System.out.println(threadLocal.get());threadLocal.remove();System.out.println(threadLocal.get());}}.start();}}
运行结果:
thread_1
null
thread_2
null
从上面的代码运行结果可以看到,两个线程分别获取了自己线程存放的变量,他们之间的变量获取并不会错乱。
ThreadLocal原理
ThreadLocal的set(T value)方法:
public void set(T value) {
// 1. 获取当前线程
Thread t = Thread.currentThread();
// 2. 获取当前线程中的属性threadLocalMap
ThreadLocalMap map = getMap(t);
// 如果当前线程的map不为空,更新map中的值,this,即当前localThread对象
if (map != null)
map.set(this, value);
else
// 如果当前线程map为空,创建ThreadLocalMap并给赋值给当前的线程
createMap(t, value);
}
// getMap(Thread t)方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 实际上 ThreadLocalMap是LocalThread的一个内部类
static class ThreadLocalMap {...}
// 同时也是Thread的一个属性
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// createMap(Thread t, T t)方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
综上:
- 当一个线程调用LocalThread的set方法时,会首先通过当前线程的属性ThreadLocalMap.
- 如果当前线程的ThreadLocalMap不为空,直接更新值。key值为LocalThread对象。
- 如果当前线程的ThreadLocalMap为空,创建ThreadLocalMap并赋值给当前线程。
ThreadLocalMap的源码
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// 内部存储数据使用的是Entry
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal中的get方法
public T get() {
// 1. 获取当前线程
Thread t = Thread.currentThread();
// 2. 获取当前线层的ThreadLocalMap属性
ThreadLocalMap map = getMap(t);
// 如果当前线程的ThreadLocalMap不为空,从threadLocalMap中获取值返回
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果ThreadLocalMap为空,如果当前线程的ThreadLocalMap属性为空,初始化并复制value为null返回。
return setInitialValue();
}
// setInitialValue方法
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// initialValue方法
protected T initialValue() {
return null;
}
ThreadLocal的remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// ThreadLocalMap中的remove(LocalThread t)方法:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
remove方法,获取了当前线程的ThreadLocalMap方法,并从中移除了当前的ThreadLocal的key。
ThreadLocal、Thread、ThreadLocalMap的关系
- 每个Thread都有一个ThreadLocalMap属性,该属性为ThreadLocal的内部类。
- ThreadLocalMap中存储的数据的是Entry,而Entry中存储对象的是K,V。K即ThreadLocal对象,Value即需要隔离的对象。
- Thread内部的ThreadLocalMap是由ThreadLocal维护的。正删改查均是。
- 对于不同的线程,没获取副本值时,别的线程并不能获取当前线程的副本值,因为是通过Thread获取器ThreadLocalMap私有属性,再获取副本变量的。这样就形成了副本的隔离。互不干扰。
为什么使用弱引用?
// 1.初始化一个LocalThread
private LocalThread<String> lt = new LocalThread("JackMa");
// 2.执行业务逻辑
public void method1(){
...
// 3.业务逻辑执行完成,LocalThread设置为null.
}
// 4.业务逻辑某部门执行完成,即上述methond1执行完,线程并未结束

- 如上图所示,ThreadLocalMap的生命周期是和线程相同的。
- 如果在某个线程执行过程中,LocalThread对象被设置为null。理论上他已经是垃圾对象,应该被回收。
- 但是由于线程对象不是垃圾,间接的ThreadLocalMap也不是垃圾。Entry同样不是垃圾。而entry的key如果是个强引用指向 LocalThread对象。此时LocalThread对象也就无法被回收。会造成内存泄漏。
- 可以看到value同样有这个的问题,如何解决的。 源码中的 get set remove 方法都会遍历table数组清除key为null的value.
- 因此,在单个线程使用完threadLocal时,一般需要调用threadLocal的remove方法,清除value.
ThreadLocal使用场景
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离。
- 进行事物操作,用于存储线程事物信息。
- 数据库连接,Session回话管理。
- Spring事物
1) Spring框架在事物开始时,会给当前线程绑定一个 Jdbc Connection 2) 在整个事物过程中都是使用该线程绑定的connection来执行数据库操作,实现事物隔离性。 3) Spring框架中就是使用ThreadLocal来实现这种隔离。
多线程常见术语
进程
- 应用程序由数据和指令组成,指令要运行,数据要读写,就必须将指令加载进CPU,在指令运行过程中,还需要用到磁盘,网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 当一个应用程序被运行,就必须将指令加载进内存,就开启了一个进程。
- 进程可以视为一个应用程序的实例,有的应用程序可以运行多个实例进程,有的只能启动一个实例进程。
线程
- 一个进程可以分为一个或者多个线程。
- 一个线程就是一个指令流,指令流中的一条条指令以一定的顺序交给CPU执行。
进程和线程的对比
- 进程间的通信比较麻烦,需要通过网络和一定协议。比如http,ssh等。
- 线程间的通信比较简单,因为他们共享进程内的内存,比如java中的多线程可以访问一个共享变量。
并行
- 同一时间做多件事情的能力
- 多核CPU上不同线程的同时运行。
- 一般应用程序既又并行,又有并发。
并发
- 同一时间应对一件事情的能力。
- 线程轮流使用CPU的做法称为并发。
- 在同一个CPU上,多个线程抢占CPU资源,轮流执行。由于任务调度器将CPU的时间切片分的很小,人类感觉就是同时运行的。总结一句话,围观串行,宏观并行。
查看进程线程的方法
- 查看所有进程: ps -ef
- 查看某个进程的所有线程 ps -fT -p PID, ps -H -p PID
- 杀死进程 kill PID
- 查看所有java进程 jps
- 查看某个java进程的所有线程状态 jstack PID
- jconsole连接
线程上下文切换
- CPU的时间片用完。
- 垃圾回收,会STW.
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock方法。
- 当Context Switch发生时,需要由操作系统保存当前线程的状态。并恢复另一个线程的状态。java中对用的概念就是程序计数器。他的所用是记住下一条JVM指令的执行地址。线程私有。频繁地发生上下文切换会影响性能。
守护线程
- 默认情况下,java进程需要等所有线程执行结束,才会结束。
- 默认情况下,如果线程A启动了线程B(非守护线程),在B没有执行完成的情况下,A也不会结束。
- 如果B为守护线程,A自己执行到最后一条指令后,不会等待B执行完成,A会执行结束。 同时B也会被强制结束。
- 设置守护线程的方法 setDaemon(true).
- 垃圾回收线程就是一种守护线程。
- Tomcat总的Acceptor和Poller线程都是守护线程。
- setDaemon(true)必须在start之前设置,否则会抛出illegalThreadStateException异常。不能把正在运行的常规线程设置有守护线程。
- 在Daemon线程中产生的线程也是守护线程。
Java中的6种线程State
java中的线程状态用6个enum表示
- NEW 被new出来,没有调用过start方法。
- RUNNABLE,当调用了start后,会变为RUNNABLE状态。
- BLOCKED,等待monitor lock.
- WAITING, 等待状态,调用了Object的wait方法。
- TIMED_WAITING,调用了sleep方法后,等待对应的时间。
- TERMINATED, 终止状态,已经执行完成。
临界区 就是这个
- 一个程序运行多个线程本身没有问题
- 问题出在多线层访问共享资源,多个线程对共享资源读取也没有问题,问题出在多个线程对共享资源的写操作,发生指令交错,就会出现问题。
- 一个代码块内,如果存在对共享资源的多线程读写操作,这个代码块就称为临界区。
线程安全解决方案
阻塞式解决方案
synchronized:
- 采用互斥的方法让同一个时刻只能有一个线程持有锁对象。
- 其他线程采用互斥锁就能锁住
- 保证有锁的线层安全的执行临界区中的代码,不用担心上下文切换。
ReentranLock: 不同于synchronized:
- 可中断
- 可设置超时时间
- 可设置为公平锁
- 支持多个条件变量
1) 可以通过ReentranLock对象.newCondition()设置多个条件变量。 2) 这样就可以防止虚假唤醒,通过singlenal唤醒await的特定条件的线程集合。 相同点:
- 都支持可重入,可重入是指同一个线程首次获取了这把锁,因为他是这把锁的拥有者,因此有权利再次拥有这把锁。不可重入的意思是第二次获取就会被锁住,获取不到。
非阻塞式解决方案
JUC的原子变量
变量的线程安全
成员变量和静态变量
- 如果没有共享,则线程安全。
- 如果共享了,只有读操作,线程安全。
- 如果共享且有写操作,这段代码是临界区,需要考虑线程安全。
局部变量
- 局部变量本身是线程安全的。
- 但局部变量引用的对象需要区分
1) 如果局部变量引用的对象逃离了方法的作用范围,需要考虑线程安全。 2) 局部变量引用的对象没有逃离方法的作用范围,线程安全。
常见的线程安全类
- String 、Integer、StringBuffer、Bandom、Vector、HashTable、java.util.concureent包下的类,即JUC下的类。
- 此处的线程安全指的是,多个线程调用他们同一个实例的某个方法时,是线程安全的。
- 可理解为:线程安全的类,他的每个方法是原子的。
- 但是他们的多个方法组合时,并不是原子的。
Monitor
- monitor对象为虚拟机实现的对象,程序中无法获取。
- 每个java对象可以关联一个monitor对象。若使用synchronized给对象上锁之后,该对象头中的markword就会指向monitor对象。
- 多个线程共用一把锁,即多对一,一个锁对象的一个monitor,多个线程和monitor的关系也是多对一。
- 一个monitor对象记录的信息:
1) waitSet: 通过调用wait方法,等待被notify的线程对象集合,状态为waiting. 2) EntryList: 指向执行到synchronized后等待锁的多个线程,他们的状态是Blocked。 3) owner: 指向当前正在执行的线程对象。状态为runnable.
JVM对重量级锁的优化
重量级锁
- 每个锁对象都对应了一个monitor对象
- 有owner时对象头中记录的信息为10
- 没有owner时对象头中记录的信息为01
- 线程的上下文切换锁住的对象,需要记录信息。
- 线程的上下文切换,与锁住的对象关联的monitor对象,需要切换owner对象。
轻量级锁
- 使用场景:一个对象虽然有多个线程访问,但时间都是错开的,即没有竞争。
- 实际语法没有变,JVM底层优化,不创建monitor对象。
- 实现原理:
1) 每个线层的栈帧都会记录一个包含锁记录的结构。内部存储锁对象的markdown. 2) 当前线程执行临界区中的代码获取锁时,尝试用栈帧中的锁记录结构(00)通过cas操作替换对象的对象头中的markword信息(01)。 替换成功: 获取锁成功。 替换失败:如果是本线程持有轻量锁,添加一条LockRecord作为重入计算。 如果是其他线程已经持有了该对象的轻量级锁。进入膨胀锁。
膨胀锁
- 当线程通过栈帧中存储的锁数据结构的00和锁对象的头信息01交换的时候,如果锁对象的头信息非01,cas操作就会失败。
- 失败后会为锁对象重新申请重量级锁monitor,并让锁对象指向重量级锁。
- 自身进入monitor对象中的monitor中,Blocked.
- 当之前已经获取锁对象的线程解锁时,之前肯定是轻量级锁,需要通过cas操作恢复值给对象头,必定失败。此时会进入重量级锁的释放流程。即将monitor中的owner置空,唤醒EntryList中的Blocked线程。
自旋优化
- 重量级锁在竞争的时候,可以通过自旋来进行优化,如果当前线程已经自旋成功,就可以获取锁避免阻塞(这里的阻塞指monitor被置空后,在entryList中的线层被唤醒的整个等待时间)。
- 自旋成功:即当monitor对象中的owner被置为null时,获取到monitor对象并将owner中的对象设置为自身。
- jdk6之后的自旋时自适应的,比如对象刚刚的一次自旋成功过,JVM会认为自旋成功的几率较高,会多自旋,反之减少自旋甚至不自旋。
- 自旋占用CPU时间,JDK7之后不能开启是否开启自旋。
偏向锁
- 轻量级锁在没有竞争时,每次重入仍然需要cas.
- 只有第一次使用cas将线程id设置到对象头。
- 之后发现线程id是自己的就表示没有竞争,不用重新cas.
- 只要不发生竞争,整个对象就归该线程所有。
- 偏向锁的状态默认是打开的,对象头markword中的值为101.第一个1表示开启偏向。
- 偏向锁时延迟加载的,可以禁用 -xx:-UseBiasedLocking
- 当有其他线程进入竞争,偏向锁会进入重量锁。
同步模式之保护性暂停
- 即一个线程等待另一个线程的执行结果。
- 要点: 有一个结果需要从一个线程传递到另一个线程。
- 如果有一个结果不断从一个线程产生到另一个线程。那么可以使用消息队列。即生产者消费者。
- JDK中,join的实现,Future的实现,就是采用此模式。
- 因为要等待另一个线程的结果,因此归类到同步模式。
线程活跃性
- 死锁
1) 发生的条件 最少两个线程,两把锁。 2) t1线程持有A对象锁,需要获取B对象锁。 3) t2线程持有B对象锁,需要获取A对象锁。
- 定位死锁
1) 使用jps查询java进程号,通过jstack PID打印栈信息。 2) 通过jconsole工具连接,直接点击检测死锁。
- 活锁
1) 出现在两个线程相互改变对象的结束条件,最后谁也无法结束。
- 饥饿
1) 一个线程的优先级太低,始终无法获取到CPU的调度执行。
共享锁和独占锁
- 共享锁
- 允许多个线程同时获得锁,如semaphore,CountDownLath,ReadLock等。
- 使用AQS的acquireShared和releaseShared实现。
- 独占锁
- 每次只能一个线程持有锁,如ReentrantLock,synchronized,writeLock等。
- 使用AOS的acquire和release实现。
Synchronized和Lock的区别
- synchronized是一个关键字,lock是一个类。
- 加锁方式:synchronized作用在方法上,锁定的是当前类类的,作用在同步代码块中,锁定的是指定对象。
- 释放锁:synchronized释放锁,JVM自动释放。 lock需要在finally中手动释放。
- 可重入: 都支持。
- 锁状态: synchronized无法判断,lock可以判断。
- 中断: synchronzed不可中断。 lock可中断
- 公平: synchronized不能设置公平锁,lock可以。
- 底层实现不同
- synchronized底层是monitor重量锁。
- lock底层实现是状态值+cas+双向链表,即AQS.
- int状态值:用于锁状态变更。
- 双向链表:存储等待线程。
- 获取锁的过程,本质是通过cas来获取状态值修改。
- 如果没有获取到,将线程放入等待链表中。
- lock释放过程,改变状态值,调整等待链表。
JMM
什么是JMM
- java memory model ,java内存模型。
- 定义了主存,工作内存抽象概念。
- 底层对应着CPU寄存器,CPU指令优化、缓存、硬件内存等。

如上图
- 在JVM运行的时候,会有一个主内存,各个线程会有各自的工作内存。
- 各个线程不能直接操作主内存和别的线程的工作内存,只能通过把主内存中的数据拷贝到自己的工作内存中,从而对数据进行操作。
JMM三大特性
原子性
- 保证指令不会受到线程上下文切换的影响
- 解决的是多线程对同一临界区域代码块共享变量的修改问题。
- 原子性与可见性的区别
1) 可见性保证的是,多线程中一个线程对volitile变量的修改,需要对其他线程可见。只有一个线程在写变量,其他线程都是在读取。 2) 原子性的保证需要通过加锁实现,场景为多个线程都会修改变量。
可见性
- 保证指令不受CPU缓存的影响
- 当一个线程频繁从主存中读取某个变量的值,JIT编译器会将变量的值存储到自己的工作内存中,减少对主存的访问,提高效率。——提高了效率,也是万恶之源
- 此时如果一个线程改变了这个变量的值,即主存中的值发生了改变
- 当前线程还是在自己的工作内存中获取。即感知不到变量的值发生了变化。拿到的永远是旧值。
- 解决办法:
1) 使用volatile来修饰,可以避免线程从自己的工作缓存中查找变量值,必须到主存中获取变量的值。线程操作volatile都是直接操作主存。 2) 打印该变量的值,System.out.Print 加了synchronized保证了原子性。
- 可见性保证的是,在多线程间,一个线程对共享变量的修改对其他线程可见。但是不能保证原子性。
有序性
- 保证指令不受CPU指令重排(并行优化)的影响。
- JVM在不影响正确性的前提下,可以调整语句的执行顺序。
- 多线程场景下,指令重排会影响正确性。
- 为什么要进行指令重排?
- 指令可以划分为更小的单元:
- 取指令
- 指令译码
- 执行指令
- 内存访问
- 数据回写
- 一个单元即一次执行,为了提高效率,每次多个单元同时执行。如下图,两个执行如果串行,如果每个单元需要1s, 2个指令10个单元需要10s.同时执行的话需要6s就完成了。
- 指令可以划分为更小的单元:
- 如何禁止指令重排?
- 临界区代码加锁
- 使用volitile
volatile
- 底层原理
- volitile底层原理是内存屏障
- 对volitile对象的写指令后会加入写屏障
- 对volitile对象的读指令前会加入读屏障
- 如何保证可见性
- 写屏障:保证在共享屏障前,对共享变量的改动,都同步到主存中。
- 读屏障:保证在该屏障之后,对共享变量的改动,都读取的是主存中的最新数据。
DCL中volatile的使用
代码如下:
public final class Test { private static volatile Test instance = null; private Test(){ } public static Test getInstance(){ // 第一个check,当instance被实例化后,后面都不为null,如果去掉这个if,多线程场景下都要进入synchronized。 if (instance == null){ synchronized (Test.class){ // 第一个check不在同步块中,如果不判断,有可能进入第一层check时, instance为null成立。 // 进入synchronized后,instance有可能已经被其他线程实例化,所以需要再次判空 if (instance == null){ // volatile防止指令重排返回的是半实例化对象 instance = new Test(); } } } return instance; } }如果不加volatile可能发生问题的代码
instance = new Test();:
- 虽然上述代码在synchronized中,synchronized可以保证原子性、可见性、有序性。
- 但是synchronized保证有序性的意思并非说synchronized可以防止指令重排。他的意思是如果一个变量的所有操作都在synchronized中,可以保证即使发生指令重排,也不会破坏代码的执行结果。
- 上述代码显然在同步块外面有个操作
if (instance == null){instance = new Test();底层字节码中做了两件事情
- invokespecial 表示利用一个对象引用,调用构造方法。
- putinstance 表示利用一个对象引用,赋值给instance。
- 如果putinstance和invokespecial发生指令重排,其他线程判断 instance为空时会返回false,最终导致返回半初始化对象。
- 加了volitile后,怎么解决的重排序。volitile保证有序性也是基于读写屏障
- 对于volitile修饰的变量,会在读操作前加入读屏障。
- 读屏障之后的操作,会去主存中获取值。
- 读屏障之后的执行,不能指令重排到读屏障之前。
- 对于volitile修饰的变量,会在写操作之后加入写屏障。
- 写屏障之前的操作,都会同步到主存。
- 写屏障之前的操作,不能指令重排到写屏障之后。
- 就是基于volitile不能发生指令重排,即 invokespecial 必须在 putinstance之前,不能重排,解决了可能返回半成品对象的问题。
as-if-serial
- 不管怎么指令重排,单线程场景下执行结果不能发生改变。
- 为了遵守as-if-serial原则,编译器和处理器不会对存在依赖关系的操作进行指令重排。因为这种操作为影响程序执行结果。
- 对于没有依赖关系的操作,就可能会被编译器和处理器做重排序。
happens-before
- happens-before规定了对共享变量的写操作对其他线程可见,是可见性和有序性规则的一套总结。
- 抛开heppens-before规则,JMM并不能保证一个线程对共享资源的写,对于其他线程的读可见。
- 遵守happends-before的相关实现
- 锁规则: 线程解锁之前在临界区对变量的写,在其他线程加锁后再临界区的读可见。
- volatile规则: 线程对volatile变量的写,对之后其他线程的读可见。
- 线程启动规则: 线程start之前对变量的写,对接下来其他线程对该变量的读可见。
- 线程终止规则: 线程结束前对变量的写,对其他线程得知它结束后对变量的读可见。
- 传递性:A先于B,B先于C,则A必定先于C。
Thread
wait和notiy
- 当前线程(monitor对象中的owner线程)中,通过主动调用锁对象的wait方法,即可进入锁对象中关联的monitor对象的waitset中,状态变为waiting状态。
- 线程必须获得锁,才能通过对象锁调用wait,notify,notifyall方法。
- 线程通过锁对象调用wait方法,会让当前线程进入锁对象关联的monitor对象的waitset中。
- 线程通过锁对象调用notify方法。会在waitset中唤醒一个线程。
- 线程通过锁对象调用notifyall方法,会唤醒waitset中的所有线程。
- BLOCKED和WAITING状态的线程都会处于阻塞状态,不占用CPU时间片。
- BLOCKED的线程,即在entryList中的线程会在owner释放锁时被自动唤醒。
- WAITING的线程,即在waitSet中的线程会在owner调用nofity或者nofityall时被唤醒。但并不意味着立刻获得锁,仍需要进入entryList中重新竞争。
- wait和notify都是Object的方法。
wait和sleep区别
- sleep是Thread的方法,而wait是Object的方法。
- sleep不强制和synchronized一起使用,而wait依赖锁对象关联的monitor锁,必须和synchronized一起使用。
- sleep不会释放锁对象,而wait会释放锁。
- sleep后线程的状态是TIMED_WAITING状态。wait后线程状态是WAITING状态。
- 其他线程可以通过正字sleep的线程对象调用它的interrupt方法打断sleep.这时sleep方法会抛出InterruptedException.
park、unpark
- 使用方法: LockSupport.part(线程对象),LockSupport.unpark(线程对象)
- 与wait/notify的区别
- wait,notify必须和锁对象Obj的Monitor对象一起使用。而park不用。
- park,unpark是以线程为单位阻塞和唤醒的,而notify,notifyall是随机唤醒,不精确。
- 可以先unpark,但是不能先notify.
yield
- 执行后线程进入直接进入就绪状态,马上释放了CPU执行权
- 但是依然保留了cpu的执行资格。
- 所以可能CPU下次执行调度还会让这个线程获取到执行权继续执行。
run和start
- start方法,启动一个线程,不能多次启动。
- run方法,没有启动线层,是方法调用。
Thread和Runable的区别
- Thread实现了Runable。
- 由于java的单线程,多实现,Runable有更好的扩展性。
- 使用方式
- new Thread(){…}.start();
- new Thread(new Runable{…}).start();
- Runable更适合相同的代码去处理统一资源的情况。多线程共享数据。
JOIN
- 如果没有指定时间。等待join的线程执行完成后,当前线程再执行。
- 如果执行了时间,指定时间为最大等待时间
- 如果在最大等待时间没有完成,不再等待。
- 如果提前完成,不再等待。
setPriority
- 修改线程优先级
- java中规定的优先级是1-10的整数。如果设置的大于10小于1会抛出IllegalArgumentException。
- 较大的优先级能提高线程被CPU调用的几率。
- 默认优先级为5.
interrupt()、isInterrupt()、interrupted().
- interrupt(),在一个线程中调用另一个线程的interrupt方法,即会向另一个线程发出信号,线程被打断。至于被打断线程如何处理打断,由被打断线程自己代码实现。
- isInterrupt(),用来判断当前的中断状态,true或者false.
- interrupted(),判断当前线程是否处于阻塞状态,并清除标记。
线程池
为什么用线程池?
核心: 线程复用,管理最大并发数。
- 降低资源消耗: 通过复用已经创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建,就能立即执行。 3.提供线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源。还会降低系统稳定性。
类图关系
创建线程池七大参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
corePoolSize
i核心线程数,线程池中常驻的线程数。
maxmumPoolSize
线程池中能容纳同时执行的最大线程数,必须大于等于1否则抛出异常
long keepAliveTime
空闲线程存活时间,当线程池中的线程数超过corePoolSize时,空闲时间超过keepAliveTime时,空闲线程会被销毁,调用的是Interrupte方法。
unit.
keepAliveTime的时间单位。
workQueue
阻塞队列,当核心线程都在执行任务,新进来的任务会放入阻塞队列。
threadFactory
创建线程的工厂,一般使用默认即可。
rejectHander
拒绝策略,当核心线程数都在执行任务,阻塞队列已经满了,对新进入的任务如何拒绝。
线程池执行原理
- 当正在执行的任务数没有超过coolPoolSize,即核心线程数没有占满,新增的任务由核心线程执行。
- 当核心线程数已满,阻塞队列未满,新增加的线程加入后放入阻塞队列。
- 当阻塞队列已满,线程数未达到最大线程数,增加的任务会新创建线程执行。
- 当阻塞队列已满,且达到了最大线程数,增加的任务人执行线程池的拒绝策略:AbortPolicy、DiscardPolicy、DiscardOldPolicy、CallerRunsPolicy等。
拒绝策略类图关系

拒绝策略
- AbortPolicy(默认): 直接抛出RejectExecutionExeption.阻止正常运行。
- CallerRunsPolicy(返回调用者):将任务返回调用者执行。
- DiscardPolicy(丢弃): 直接丢弃任务,不跑异常,也不执行。
- DiscardOldestPolicy(丢弃最久): 直接抛弃阻塞队里中等待最久的任务,然后将新任务加入阻塞队列。
线程池的创建方式
Executors.newFixedThreadPool(n);
- 构造方法:
return new ThreadPoolExecutor(nThreads, nThreads,<br />0L, TimeUnit.MILLISECONDS,<br />new LinkedBlockingQueue<Runnable>()); - 核心线程数等于最大线程数。执行任务的线程数时固定的。不会新创建线程。
- 线程存活时间为0L,不涉及(核心=最大)。
- 阻塞队列为linkedBlockingQueue,即最大为Integer默认值。
- 构造方法:
Executors.newSingleThreadExecutor();
- 构造方法:
return new FinalizableDelegatedExecutorService<br />(new ThreadPoolExecutor(1, 1,<br />0L, TimeUnit.MILLISECONDS,<br />new LinkedBlockingQueue<Runnable>())); - 核心线程数等于最大线程数,都是1,即永远只有一个线层在执行任务。
- 线程存活时间为0.不涉及(核心=最大)。
- 阻塞队列为linkedBlockingQueue,即最大为Integer默认值。
- 构造方法:
Executors.newCachedThreadPool();
- 构造方法:
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,<br />60L, TimeUnit.SECONDS,<br />new SynchronousQueue<Runnable>()); - 核心线程数为0,最大线程数为int最大值。
- 线程核心数为0,所有线程都是外包人员,项目结束后,60S需要撤离。(2021.10.3 我就是和非核心线程啊啊啊啊)
- 使用的队列为SynchronousQueue,相当于阻塞队列不存储元素。结合 2. 每个任务进入后都会创建新的线层。
- 构造方法:
Executors.newScheduledThreadPool(n);
- 构造方法
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {<br />return new ScheduledThreadPoolExecutor(corePoolSize);<br />} public ScheduledThreadPoolExecutor(int corePoolSize) {<br />super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,<br />new DelayedWorkQueue());<br />} - 核心线程数为指定值,最大线程数为Integer最大值。
- 外包人员在项目结束后立即撤离。
- 阻塞队列使用的是DelayedWorkQueue,该队列默认大小是16. 队列满了后会创建新线程。
- 构造方法
为什么不建议使用JDK自带的创建方式 就这四种,但是都不能用
- FixedThreadPool和SingleThreadPool,核心线程数都等于最大线程数,阻塞队列都使用了LinkedBlockingQueue,最大容量为Integer最大值。大量任务场景下,请求堆积,有可能发生OOM.
- Cached和Scheduled, 最大线程数都设置了Integer的最大值。大量任务场景下,会创建大量的线程。造成资源浪费,系统卡顿、OOM.
IO密集型与CPU密集型
- IO密集型
- CPU使用不高,大多数时间在处理耗时的IO操作。这类操作不占用CPU.
- 例如文件读写、DB读写、网络请求等。
- CPU密集型
- CPU使用率较高,逻辑处理多,IO很少或者响应都非常迅速。
- 例如计算型代码,json转化等。
如何创建线程池,设置核心线程数大小?
- 创建线程池通过 new ThreadPoolExecutors(核心线程数、最大线程数、等待时间、时间单位、线程工厂、阻塞队列、拒绝策略)来实现。
- 核心线程数
- IO密集型:大部分时间在处理IO交互,而线程在IO的时间段内不会占用CPU来处理,这时可以将CPU交给其他线程使用。因此在IO密集型任务中,可以多配置一些核心线程数。具体计算方法是暴力 2N,还有个公式记不住。
- CPU密集型:主要消耗的是CPU资源,如果比CPU大太多,会引起频繁地上下文切换,降低效率。设置为 CPU的核心数+1. +1是为了防止线程偶发中断,或者任务暂停导致CPU空闲。
- 最大线程数
- 一般情况下看使用场景,如果设置全局线程池,可以适当的比核心线程数大点。
- 非全局的与核心线程数保持一致。
- 等待时间、时间单位
- 全局场景等待几秒回收。
- 非全局场景,只在特定时间内并发激烈,设置为0,使用完成立即回收。
- 线程工厂一般使用默认即可。
- 阻塞队列
- 由于任务一般生产消费是比较频繁的,使用特定大小的linkedBlockingQueue。
- 拒绝策略
- 不重要的、允许任务丢失的场景下,使用discardPolicy或者discardOldPolicy来实现。
- 任务比较重要,不允许丢失,使用callerrunsPolicy返归调用者或者直接使用AbortPolicy抛出异常。
shutdown和shutdownNow的区别
- shutdown
- 正在执行的任务: 执行完成
- 阻塞队列中的任务: 执行完成
- 新任务被提交: 执行拒绝策略
- shutdownNow
- 正在执行的任务: 打断
- 阻塞队列中的任务:不执行,返回。
- 新任务被提交:执行拒绝策略。
线程池使用完成要不要shutdown
- 如果线程池创建时核心线程数设置的不是0,且核心线程数在空闲是不会被回收。默认AllowCoreThreadTimeOut=false. 这些核心线程数一直会阻塞在获取任务上。即核心线程不会结束、GC无法回收,会导致系统资源浪费,有可能导致内存溢出。
- 如果没有核心线程数,或者核心线程可以被回收。且keepalive时间合理,不是很长,没有OOM风险。
- 综上,如果分全局的线程池,一般是需要shutdown的。
线程池中线程复用的原理
- 线程池将线程和任务进行了解耦,线程是线程、任务是任务。
- 在线程池中,同一个线程可以不断从阻塞队列中获取新任务来执行。
- 核心原理为线程池对Thread进行了封装,并不是每次执行都需要thread.start()创建新线程。
- 而是让每个线程去执行一个循环任务。而这个循环任务中不停的检测是否有任务需要被执行。
- 如果有,直接调用任务的run方法。做到线程复用。
线程池的五种状态
- RUNNING: 在running状态下,线程池可以接受新任务和执行已经添加的任务。
- 线程池的初始化状态就是running.
- 比如调用了Executors.newFixedThreadPool(),一旦创建,状态就是running.
- SHUTDOWN: 线程池出在shutDown状态时,不再处理新任务,但能处理已经添加的任务。
- 调用shutdown方法时,会将线程状态由RUNNING变为shuting.
- STOP: 线程池处在SHUTDOWN状态时,不接受新任务,不处理已经添加的任务,并且会中断正在执行的任务。
- 调用shutdownNow方法时,会将线程状态由RUNNING或者SHUTDOWN变为STOP.
- tidying: 中文整理的意思,当前所有任务已经终止,记录的任务数量为0,线层池会变为tidying状态。会执行函数terminated.
- 当线程在shutdown状态下,执行完阻塞队列的任务后,会由SHUTDOWN变为TIDYING.
- 当处在stop状态下,执行池中执行的任务为空时,会由stop变为tidying.
- terminated: 当terminated执行完成后,变为terminated。
线程辅助类
CountDownLatch(闭锁)
解决问题
多线程场景场景下,一个线程要等待多个线程执行完成后再执行。
原理
- new CountDownLatch(计数器大小),初始化了技术器的大小
- 多个线程执行的时候,countDown(),相当于给计数器减一。
- await 会等到 计数器大小减到0之后,执行后面的代码
- 注意如果线程本身会抛出异常,countDown必须在异常之前,建议写在final中。
- 当所有线程都调用过 await后,CountDownLatch 维护的 volitile int status 已经减到1,所以只能用一次,第二次使用不会报异常,但是也不会生效。
使用方法
- new 出计数器,初始化大小为先执行的线程的数量
- 每个先执行的线程执行完后,调用 countDown方法,计数器减一
- 在后执行的线程中调用await方法。先执行的方法执行完成后,后执行的方法会自动执行
示例代码
@Test public void testCountDownLatch() { // 实例化一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(3); // 初始化数量为3的计数器 CountDownLatch countDownLatch = new CountDownLatch(3); // 启动三个线程 System.out.println(countDownLatch.getCount()); for (int i = 0; i < 3; i++) { int temp = i; executorService.submit(new Runnable() { @Override public void run() { try { System.out.println(Thread.currentThread().getName() + temp); } finally { countDownLatch.countDown(); } } }); } System.out.println(countDownLatch.getCount()); try { System.out.println("await begin."); countDownLatch.await(); System.out.println(countDownLatch.getCount()); System.out.println("await end."); } catch (InterruptedException e) { System.out.println(e.getMessage()); } System.out.println(countDownLatch.getCount()); }流程图
CyclicBarrier(同步屏障)
解决问题
让一组线程到达一个屏障时被阻塞,直到最后一个线程到达时,所有被拦截的线程继续执行
使用方法
- 默认构造方法为 new CyclicBarrier(int parties), 参数表示屏障拦截的线程数量。
- 每个线程调用await()方法告诉CyclicBarrier自己已经到了屏障,然后当前线程被阻塞。
- 当所有线程都调用了 await()方法后,屏障得知所有线程都已到达屏障,所有线程开始继续执行。
示例代码
@Test public void CyclicBarrierTest() throws Exception { CyclicBarrier cyclicBarrier = new CyclicBarrier(3); for (int i = 0; i < 3; i++) { new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " at barrier."); try { cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } System.out.println("await end."); } }).start(); } System.out.println("main end."); }Semaphore(信号量)
解决问题
- 控制并发线程数。控制系统中某个资源被同时访问的线程个数。
- 类似于锁,又比锁强大,锁一般锁住的是一个资源,Semaphore可以锁住多个资源。
- 某些场景下,线程数原高于数据库连接池数量,可以用来限制多线程同时阻塞在数据库连接池。
使用方法
- new Semaphore(int x),参数为允许同时执行的线程数量
- 每个线程执行前调用 acquire().
- 每个线程执行完调用release().
示例代码
@Test public void semapHoreTest() throws Exception { Semaphore semaphore = new Semaphore(2); for (int i = 0; i < 111; i++) { int ii = i; new Thread(() -> { try { semaphore.acquire(); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "----" + ii); semaphore.release(); }).start(); } Thread.sleep(10000); }Exchanger(交换器)
解决问题
解决问题:两个线程之间交换数据
使用方法
- new Exchanger()
- 两个线程中分别 .exchange(要交换的数据)
- 分别得到对方数据。
示例代码
@Test public void exeChangerTest() throws Exception { Exchanger<String> exchanger = new Exchanger<>(); new Thread() { @Override public void run() { try { String test1 = exchanger.exchange("test1");//test1 test2 System.out.println("test1 " + test1); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); new Thread() { @Override public void run() { try { String exchange = exchanger.exchange("test2"); System.out.println("exchange" + exchange);//exchangetest1 } catch (InterruptedException e) { System.out.println(e.getMessage()); } } }.start(); Thread.sleep(10000); }CAS
什么是CAS
- compare-and-set,也有compare-and-swap叫法。是比较并交换的意思。
- 它是一条cpu并发原语,用于判断内存中某个值是否为预期值,如果是则改为新的值,这个过程是原子的
CAS原理
- 主要包括两个操作,compare和swap.
- 如何保证两个操作的原子性: CAS是一种系统原语,原语属于操作系统用语,由若干指令组成,用于完成一个特定的功能,并且原语执行必须是连续的,在执行过程中不允许被中断。
- 综上,CAS是一条CPU原子指令,原子性由操作系统来保证。
- java中的实现时UnSafe类。
ABA问题
代码
public final class Test { private AtomicInteger ai = new AtomicInteger(100); @org.junit.Test public void test() { Thread thread1 = new Thread(() -> { boolean re = ai.compareAndSet(100, 101); System.out.println("thread1 从 100 修改到101" + re); }); Thread thread2 = new Thread(() -> { boolean re = ai.compareAndSet(101, 100); System.out.println("thread2 从 101 修改到100" + re); }); Thread thread3 = new Thread(() -> { boolean re = ai.compareAndSet(100, 101); System.out.println("thread3 从 100 修改到999" + re); }); CompletableFuture.runAsync(thread1).thenRun(thread2).thenRun(thread3); } }- 如上,当atomicInteger的值为100,线程1将值改为101.
- 当值为101,线程2将值改为100.
- 当值为100,线程3将值改为101.
此处的问题
- 3要比较的100是否是原始的100,明显不是,这种场景就要区分。
- 如果只关注数值,不关注是否修改过。就没有问题。
- 如果既要关注数值,也要关注是否被修改过,就有问题。
解决方式
public final class Test2 { private AtomicStampedReference ai = new AtomicStampedReference<Integer>(100,0); @org.junit.Test public void test() { Thread thread1 = new Thread(() -> { // 从100改为101,比较的100的版本号是0,新版本号是1 boolean re = ai.compareAndSet(100, 101,0,1); System.out.println("thread1 从 100 修改到101" + re); }); Thread thread2 = new Thread(() -> { // 从100改为101,比较的101的版本号是1,新版本号是2 boolean re = ai.compareAndSet(101, 100,1,2); System.out.println("thread2 从 101 修改到100" + re); }); Thread thread3 = new Thread(() -> { // 从100改为101,比较的100的版本号是0,新版本号是1 boolean re = ai.compareAndSet(100, 101,0,1); System.out.println("thread3 从 100 修改到999" + re); }); CompletableFuture.runAsync(thread1).thenRun(thread2).thenRun(thread3); } }- 如上代码,解决ABA问题的思路,既要比较值,也要比较版本。
- java中使用AtomicStampedReference来解决。
自旋开销问题
- CAS出现冲突后就开始自旋操作,如果资源竞争非常激烈,自旋长时间不能成功就会给CPU带来非常大的开销。
- 可以考虑限制自旋次数,避免过度消耗CPU. 也可以考虑延迟执行。
cas和volatile
- cas是一条原子指令,可以保证比较和设置值具有原子性。
- volatile保证cas每次操作共享资源时,读写都直接操作主存,保证了可见性和有序性。
- 乐观锁的实现原理就是cas+valatile+自旋
乐观锁悲观锁
- 结合cas和volatile和自旋,可以实现无锁并发,使用线程数较少,即竞争不激烈的场景。
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改变量,就算改了也没关系,我自旋。
- synchronized是基于悲观锁的思想: 最悲观的估计,得防止其他线程来修改共享变量,我上了你们都别改,等我改完了你们才有机会。
- CAS体现的是无锁并发,无阻塞并发。
- 因为没有使用锁,线程不会从RUNNABLE和BLOCKING的切换,也就没有上下文切换的影响。这个是效率提升的原因。
- 如果竞争激烈,每次cas都失败,需要不断的重试不断的争抢CPU的执行权,效率会降低。
AQS-这个源码很重要,欠着先
什么是AQS
- 全称是AbstractQueuedSynchronizer.
- 是阻塞式锁和相关同步器工具的框架。
AQS原理
- 用state属性来表示资源的状态,分为独占模式和共享模式。
- 子类需要定义如何维护这个state状态,控制如何获取锁和释放锁。
- getState 获取state状态。
- setState 设置state状态。
- compareAndSetState cas机制设置state状态。
- 独占模式只有一个线程能访问资源,而共享模式可以允许多个线程访问资源。
- 提了了基于FIFO的等待队列,类似Monitor的EntryList。
- 条件变量来实现等待、唤醒机制。支持多个条件变量,类似Monitor的WaitSet。
- 子类主要实现如下方法,默认抛出UnsupportedOperationException.
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
