1 基础概念
1.1 同步和异步
- 同步一旦开始调用,调用者必须等到方法调用返回后才能继续后续的行为
- 异步调用一旦开始(通常会在另一个线程中执行),就会立刻返回,调用者可以继续后续的操作
1.2 并发和并行
- 并发偏重于多个任务交替执行
- 并行是真正意义上的同时执行
1.3 阻塞和非阻塞
- 阻塞是指一个线程占用了临界区的资源,其他需要这个资源的线程就必须在临界区进行等待,等待会导致线程的挂起
- 非阻塞是指没有一个线程可以防癌其他线程执行
1.4 并发级别
根据控制并发的策略,可以分为以下级别:
- 阻塞
一个线程是阻塞的,在其他线程释放资源前,当前线程是无法继续执行.可以使用Synchronized和ReentrantLock实现(试图在执行后续的代码前得到临界区的锁,得不到就会挂起线程,直到占有了所需资源) - 无饥饿
如非公平锁,系统允许插队,导致低优先级的线程产生饥饿;公平锁则不会产生饥饿 - 无障碍
是一种最弱的非阻塞调度,可以依赖一个"一致性标记"来实现,即操作之前读取并保存这个标记,操作完成后再次读取,检查这个标记是否被更改过,任何对资源有修改操作的线程,在修改数据之前都要更新这个标志,表示数据不再安全 - 无锁
无锁的并行都是无障碍的,无锁的并发保证必然有一个线程可以在有限步内完成操作 - 无等待
无等待是在无锁的基础上更进一步,要求所有的线程都必须在有限步内完成
1.5 锁的分类
1.5.1 依据造成影响分类
- 死锁
两个或者两个以上的线程在执行的过程中,因争夺资源而造成的互相等待的现象,在无外力作用情况下,这些线程会一直相互等待而无法继续运行下去
死锁产生条件 | 说明 |
---|---|
互斥条件 | 指线程对以获取到的资源进行排它性使用 |
请求并持有条件 | 指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新的资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源 |
不可剥夺条件 | 指线程获取到的资源在自己使用完之前不能被其他线程抢占 |
环路等待条件 | 指在发生死锁时,必然存在一个线程与资源的环形链,比如T0等待T1占有的资源,T1等待T2占有的资源 |
死锁的避免
只需要破坏掉至少一个构成死锁的必要条件即可
根据操作系统知识,目前只有请求并持有和环路等待条件是可以被破坏的
要注意资源申请的顺序,以避免死锁
- 活锁
活锁是指线程之间相互谦让,主动释放资源给他人使用,可能会导致资源不断地在两个线程之间跳动,而没有一个线程可以同时拿到所有资源以正常执行
1.5.2 依据持有态度分类
- 乐观锁
乐观锁认为数据在一般情况下不会造成冲突,在访问记录前不需要加排它锁,而在进行数据提交更新时,才会正式对数据冲突与否进行检测,Java中是使用无锁编程实现的,常用CAS算法,适用于读多写少的场景 - 悲观锁
悲观锁\认为数据很容易被其他线程修改,在数据被处理之前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态,适用于写多读少的场景
1.5.3 依据抢占机制分类
- 公平锁
公平锁通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性.公平锁会带来性能的开销(CPU唤醒阻塞线程的开销比非公平锁大),但是等待锁的线程不会被饿死 - 非公平锁
非公平锁在多个线程加锁时直接尝试获取锁,获取不到才会去等待队列的队尾等待.如果此时锁刚好可用,则线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景,非公平锁的优点是可以减少唤起线程的开销,整体吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程,缺点是处于等待队列中的线程可能会被饿死,或者很久后才会得到锁
饥饿是指某一个或多个线程因为种种原因无法获得所需要的资源,导致一直无法执行,饥饿还是有可能在未来一段时间内解决的
1.5.4 依据占有方式分类
- 独占锁
独占锁保证任何时候都只有一个线程能得到锁,是一种悲观锁,每次访问资源都加互斥锁,限制了并发性能,Synchronized,Lock,ReentrantLock即是以独占方式实现,AQS中state字段描述有多少个线程持有该锁,独占锁中通常为0或1 - 共享锁
共享锁可以同时由多个线程持有
1.5.5 依据是否可重入分类
- 可重入锁
- 获取锁时
- 先获取state进行判断,如果state==0,则表示没有其他线程获取锁,将state置为1,当前线程获取锁
- 如果state!=0则表示已有线程获取锁, 需要判断持有锁的线程是否为当前线程
- 不是则当前线程进入阻塞
- 是则将state=state+1,当前线程获取锁
- 释放锁时
- 在线程持有锁的情况下(state>0),先将state=state-1,然后判断state的值
- 若state=0,则表示当前线程所有重复获取锁的操作都已执行完毕,会释放锁
- 若state>0,表示当前线程还有获取锁的操作未执行完成,不会释放锁
- 在线程持有锁的情况下(state>0),先将state=state-1,然后判断state的值
- 获取锁时
// 可重入锁
final boolean nonfairTryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
if (c==0){
if (compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
return true;
}
}else if(current == getExclusiveOwnerThread()){
int nextc = c + acquires;
if (nextc<0){
throw new Error("Maximum lock count execeeded");
}
setState(nextc);//已获取锁的线程,status值+1
return true;
}
return false;
}
protected final boolean tryRelease(int releases){
int c = getState() = releases;
if (Thread.currentThread!=getExclusiveOwnerThread(){
throw new IllegalMonitorStateException();
}
boolean free = false;
if (c ==0){
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;//释放时判断status为0,释放锁
}
- 不可重入锁
- 获取锁时
- 先获取state进行判断,如果state==0,则表示没有其他线程获取锁,将state置为1,当前线程获取锁
- 如果state!=0则表示已有线程获取锁, 当前线程进入阻塞
- 释放锁时
- 在线程持有锁的情况下(state>0),直接将state置为0,然后释放锁
- 获取锁时
// 非可重入锁
protected boolean tryAcquire(int acquires){
if (this.compareAndSetState(0,1){
this.owner = Thread.currentThread();
return true;
}else {
return false;
}
}
protected boolean tryRelease(int releases){
if (Thread.currentThread()!=this.owner){
throw new IllegalMonitorStateException();
}else{
this.owner = null;
this.setState(0); //state直接置为0
return true;
}
}
1.6 并发三大特性
- 原子性Atomicity
原子性是指一个操作是不可中断的 - 可见性Visibility
可见性指当一个线程修改了某一个共享变量的值时,其他线程能否立即知道这个修改 - 有序性Ordering
有序性Ordering,指令的重排可以保证串行语义一致,不保证多线程间的语义也一致
1.7 Happen-Before原则
- 程序顺序原则,一个线程内保证语义的串行性
- volatile规则,volatile变量的写,先发生于读,保证了volatile变量的可见性
- 锁规则,解锁必然发生在加锁前
- 传递性,A先于B,B先于C,则A必然先于C
- 线程的start方法优先于它的每一个动作
- 线程的所有操作优先于线程的终结Thread.join()
- 线程的中断interrupt先于中断线程的代码
- 对象的构造函数执行,结束先于finalize方法
1.8 伪共享
- 缓存系统中是以缓存行为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会在无意中影响彼此的性能,这就是伪共享
- 缓存行示例
- 假设缓存行大小为32字节,long a,b,c,d;
- CPU访问变量a时,如果该变量没有在缓存中,就会去主内存把变量a以及内存地址附近的b,c,d放入换存行
- 即地址连续的多个变量才有可能被放到一个缓存行中.如创建数组时,就会将数组里面的多个元素放到同一个缓存行
- 避免伪共享
- JDK8之前通过字节填充的方式避免该问题,即创建变量时使用填充字段填充该变量所在的缓存行,这样可以避免多个变量存放在同一个缓存行中
- JDK8提供了sun.misc.Contended注解,可以修饰类也可以修饰变量,来解决伪共享问题
- 默认情况下@Contended注解只用于Java核心类,如rt包下的类,如果想使用,需要添加JVM参数-XX:-RestrictContended,默认填充宽度是128,可以设置-XX:ContendedPaddingWidth参数
2 Volatile
- 可以确保对一个变量的更新对其他线程马上可见
- 当一个变量被声明为volatile时,线程在写入变量时会把值刷新到主内存
- 当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是从当前线程的工作内存中的值
- 可以保证可见性,有序性,不能代替锁,不能保证复合操作的原子性
- 当线程写入了volatile变量值就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存)
- 读取volatile变量值时就相当于进入同步代码块(先清空本地内存变量值,再从主内存获取最新值)
3 Synchronized
3.1 并发锁内存语义
- 锁可以让临界区互斥执行,还可以让释放锁的线程向同一个锁的线程发送消息
- 锁的释放需要遵循Happens-before原则
- 锁在Java中的体现为Synchronized和Lock
- 锁释放后,会将共享变量刷新到主内存中
- 锁获取时,JMM会将线程的本地内存置为无效,被Monitor保护的临界区代码必须从主内存中读取共享变量
3.2 并发锁的优化
- 减少锁持有时间,有助于降低锁冲突的可能性,进而提高系统的并发能力
- 减小锁粒度,将一个大锁进行分段,是削弱多线程锁竞争的有效手段,只有在类似于size()方法获取对象全局信息不频繁时,分段方式才能有效提升性能
- 使用读写分离锁替换独占锁,在读多写少的场合,适合使用读写锁
- 锁的分离,例如LinkedBlockingQueue,take(),put()分别操作队列的头部和尾部,理论上并不冲突
- 锁的粗化,虚拟机在遇到一连串连续地对同一个锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数
3.3 Synchronized定义
- JDK1.5前,JVM实现的内置互斥锁,锁的获取和释放都是JVM隐式实现的,Synchronized是基于底层操作系统的Mutex Lock实现的,JDK1.6时对Synchronized进行优化后,性能大幅提升,可以保证同一时刻只能由一个线程进入到临界区,同时保证共享变量的可见性/原子性/有序性
- 普通同步方法,锁对象为当前实例对象,同步原理是依靠方法修饰符上ACC_SYNCHRONIZED
- 静态同步方法,锁对象为当前类的class对象,同步原理是依靠方法修饰符上ACC_SYNCHRONIZED
- 同步代码块,锁对象为括号中的对象,同步原理是同步代码块使用monitorenter和monitorexit指令实现
3.4 Synchronized实现原理
- JVM中的同步是基于进入和退出 Monitor 对象实现的,每个Java对象实例都会有一个Monitor,Monitor可以和Java对象一起被创建和销毁
- 多个线程同时访问一段同步代码时,会先被放在EntryList中
当线程获取到Java对象的Monitor时,线程申请Mutex成功,持有该Mutex,其他线程无法获取到该Mutex - 竞争锁失败的线程会进入WaitSet集合;竞争锁成功的线程调用wait方法后,会释放当前持有的Mutex,并且该线程进入WaitSet集合
- 进入WaitSet集合的线程会等待下一次唤醒,然后进入EntryList重新排队
获取到Mutex的线程可以执行完方法,同时释放Mutex - Monitor依赖于底层操作系统的实现,存在用户态和内核态之间的切换,增加了性能的开销
- Monitor锁模型
3.5 Synchronized优化机制
- JDK1.6时引入分级锁进行优化,无锁==>偏向锁==>轻量级锁==>重量级锁
- 当一个线程获取锁时,首先对象锁成为一个偏向锁(避免同一线程重复获取同一把锁时,用户态和内核态频繁切换)
- 多个线程竞争锁资源时,锁会升级为轻量级锁,轻量级锁适用于在短时间内持有锁,且为锁交替切换的场景,轻量级锁结合了自旋锁来避免用户态和内核态的频繁切换
- 如果锁竞争态激烈(自旋锁失败),同步锁会升级为重量级锁
- 优化Synchronized的关键是减少锁的竞争,尽量使Synchronized同步锁处于偏向锁或轻量级锁,锁竞争激烈时可以考虑禁用偏向锁和禁用自旋锁
- Jvm对Synchronized的优化流程图
3.6 Java对象头
- JDK1.6时,JVM中对象实例在堆内存中组成为: 对象头,实例数据,对齐填充
- 对象头组成: MarkWord, 指向类的指针,数组长度(可选,数组类型时才有)
- 锁升级功能依赖于MarkWord中锁标志位和是否偏向锁标志位
- MarkWord记录了对象和锁有关的信息,64位JVM中MarkWord为64bit
3.7 无锁
- 没有对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只能有一个线程能够修改成功(CAS)
- 修改操作在循环内进行,线程会不断尝试修改共享资源,如果没有冲突就能修改成功并退出,否则就会继续循环尝试
- 多个线程同时修改一个值,必定会有一个线程修改成功,其他修改失败的线程会不断重试知道修改成功
3.8 偏向锁
- 偏向锁是指一段同步代码一直被一个线程所访问,那么线程会自动获取锁,降低获取锁的代价.优化同一线程多次申请同一个锁的竞争
- 引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,偏向锁只需要在置换ThreadId时依赖一次CAS原子指令
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁 - 偏向锁原理
- 当一个线程抢到锁时,在对象MarkWord头中将锁标志位置为01,是否偏向锁置为1,记录线程ID,标记进入偏向锁状态
- 当线程A再次获取该锁时,比较当前线程ID与锁对象头部的线程ID是否一致,一致则无需使用CAS抢占锁;不一致则查看锁对象头部记录的线程是否存活,
- 没有存活则将对象置为无锁状态,然后重新设置为该线程的偏向锁状态;
- 存活则查找偏向锁持有线程的栈帧信息
- 如果偏向锁持有线程还需要持有该锁对象,则暂停该线程STW,撤销该对象偏向锁,升级为轻量级锁
- 如果偏向锁持有线程不需要使用使用该对象,则将该对象设置为无锁状态,重新设置为偏向线程A
- 当出现其他线程竞争锁资源时,偏向锁就会被撤销
- 撤销的偏向锁可能需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法
- 如果还没执行完,则说明有多个线程竞争,升级为轻量级锁;如果已经执行完毕则唤醒其他线程继续CAS抢占
- 高并发场景下,大量线程同时竞争同一个锁资源时,偏向锁会被撤销,发生STW,加大了性能开销
- JVM配置
- 默认配置 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000
- 关闭偏向锁 -XX:-UseBiasedLocking
- 直接设置为轻量级锁 -XX:+UseHeavyMonitors
3.9 轻量级锁
- 当锁时偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能
- 线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争
- 原理:
- 在偏向锁状态下,有另外一个线程来竞争时,首先判断对象头MarkWord中线程ID不是自己的线程ID,会执行CAS操作获取锁
- 获取成功,直接替换MarkWord中线程ID为自己的线程ID,该锁保持偏向锁状态
- 获取失败,说明当前锁存在竞争,将偏向锁升级为轻量级锁
- 线程获取轻量级锁时会先把对象的MarkWord复制一份到线程栈帧(DisplacedMarkWord,保留现场),然后使用CAS,线程1,2同时复制了对象头MarkWord进行CAS操作,线程1CAS成功,线程2CAS会失败,这是会尝试通过自旋锁等待线程1释放锁把对象头中的内容替换为线程栈帧中DisplacedMarkWord的地址
- 在偏向锁状态下,有另外一个线程来竞争时,首先判断对象头MarkWord中线程ID不是自己的线程ID,会执行CAS操作获取锁
3.10 自旋锁
- JVM提供的自旋锁,可以通过自旋的方式不断尝试获取锁,从而避免线程被挂起阻塞
- 原理:
- 自旋锁则是在当前线程获取锁时,如果发现锁已经被其他线程占有,不会马上阻塞自己,会在不放弃CPU使用权的情况下,多次尝试获取(默认10次),很可能后面几次尝试中其他线程已经释放了锁. 如果到达指定次数后仍没有获取到锁才会被阻塞挂起,是以CPU时间换线程阻塞与调度的开销
- 在锁竞争不激烈且锁占用时间非常短的场景下,自旋锁可以提高系统性能,一旦锁竞争激烈或锁占用时间太长,自旋锁将会导致大量的线程处于CAS重试状态,占用CPU资源
- JVM配置
- 默认配置,-XX:+UseSpinning -XX:PreBlockSpin=10
- 关闭自旋锁优化,-XX:-UseSpinning
3.11 重量级锁
- 自旋锁重试后依然失败会升级至重量级锁,此时未抢到锁的线程都会进入Monitor,之后被阻塞在WaitSet中
- 将除了拥有锁的线程以外的线程都阻塞
3.12 Mutex锁释放时机
- 正常退出同步代码
- 抛出异常
- 代码内调用wait方法
3.13 synchronized和volatile解决可见性区别
- synchronized是独占锁,只有一个线程可以访问,其他线程会被阻塞
- synchronized方式会存在线程上下文切换和线程重新调度的开销
- volatile采用非阻塞算法,不会造成线程上下文切换的开销
- volatile不保证复合操作的原子性
3.14 Lock与Synchronized区别
Synchronized | Lock | |
---|---|---|
实现方式 | 基于JVM层 | 基于Java代码 |
锁的获取 | JVM隐式获取 | lock() / tryLock() / tryLock(timeout, unit) / lockInterruptibly() |
锁的释放 | JVM隐式释放 | unlock() |
可否中断 | 不可中断 | 可中断 |
锁的类型 | 非公平锁,可重入锁 | 非公平锁/公平锁,可重入锁/不可重入锁 |
锁的性能 | 经过JDK1.6起优化后部分场景优于Lock,但高并发下回升级为重量级锁 | 更稳定 |
4 Lock/AQS
4.1 AQS定义
- AQS即AbstractQueuedSynchronizer抽象同步队列, 提供了一种基于CAS实现阻塞锁和一系列依赖FIFO的等待队列同步器的框架,采用模板设计模式
- AQS将大部分同步逻辑均已实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码即可
- 核心数据结构: CLH双向链表+state(锁状态)
- 独占式获取的资源是与具体线程绑定的,即线程在获取资源后,会标记是这个线程获取到,其他线程再尝试操作state时会发现当前资源不是自己持有,就会被阻塞
- 共享式获取的资源与具体线程是不相关的,当多个线程去访问请求资源时通过CAS方式竞争获取资源(Semaphore,CountDownLatch等要判断资源个数是否满足条件)
- acquire()和acquireShared()方法中,线程在阻塞过程中都是忽略中断的
- acquireInterruptibly()/acquireSharedInterruptibly()是支持响应中断的
4.2 AQS基本框架
- AQS维护了一个volatile语义的共享资源变量state和一个FIFO线程等待队列(CLH队列, 多线程竞争时会被阻塞进入此队列)
public abstract class AbstractQueuedSynchronizer ... {
private transient volatile Node head;
private transient volatile Node tail;
// 是共享资源,表示同步的状态
private volatile int state;
// 内部类
static final class Node {...}
// CLH队列: 竞争资源同一时间只能被一个线程访问
// 用来结合锁实现线程同步,可以直接访问AQS对象内部的变量(state值/AQS队列)
// ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),来存放调用条件变量的await方法后被阻塞的线程
// 条件队列的头尾分别为private transient Node firstWaiter/lastWaiter
public class ConditionObject {...}
}
4.2.1 AQS中Node内部类
- 重要属性
// 表明节点在共享模式下等待的标记
static final Node SHARED = new Node();
// 表明节点在独占模式下等待的标记
static final Node EXCLUSIVE = null;
// 表征等待线程已取消的
static final int CANCELLED = 1;
// 表征需要唤醒后续线程
static final int SIGNAL = -1;
// 表征线程正在等待触发条件(condition)
static final int CONDITION = -2;
// 表征下一个acquireShared应无条件传播
static final int PROPAGATE = -3;
/**
* waitStatus非负时,表示不可用,正数代表处于等待状态,所以waitStatus只需要检查其正负符号即可,不用太多关注特定值
* SIGNAL: 当前节点释放state或者取消后,将通知后续节点竞争state。
* CANCELLED: 线程因timeout和interrupt而放弃竞争state,当前节点将与state彻底拜拜
* CONDITION: 表征当前节点处于条件队列中,它将不能用作同步队列节点,直到其waitStatus被重置为0
* PROPAGATE: 表征下一个acquireShared应无条件传播
* 0: None of the above
*/
volatile int waitStatus;
// 前继节点
volatile Node prev;
// 后继节点
volatile Node next;
// 持有的线程
volatile Thread thread;
// 链接下一个等待条件触发的节点
Node nextWaiter;
- 重要方法
// 返回节点是否处于Shared状态下
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前继节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null) throw new NullPointerException();
else return p;
}
// Shared模式下的Node构造函数
Node() {
}
// 用于addWaiter
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// 用于Condition
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
4.3 AQS中重要方法
- AQS中需要复写的方法都没有声明为abstract,避免子类强制覆写多个方法,一般自定义同步器要么是独占要么是共享,只需要实现tryRelease/tryAcquire, tryReleaseShared/tryAcquireShared中的一种即可
- 状态设置与获取
// 均为原子操作
getState()
setState(int newState)
// 依赖于Unsafe的compareAndSwapInt()方法
compareAndSetState(int expect,int update)
- 资源获取/释放
// 默认方法的实现都是抛出异常 throw new UnsupportedOperationException();
boolean tryAcquire(int)//独占式尝试获取资源
boolean tryRelease(int)//独占式,尝试释放资源
int tryAcquireShared(int)//共享式尝试获取资源,负数表示失败,0表示成功,正数表示还有剩余资源
boolean tryReleaseShared(int)//共享式尝试释放资源,是否后允许唤醒后续等待节点返回true,否则false
boolean isHeldExclusively()//查看该线程是否正在独占资源(Condition需要实现)
4.3.1 获取资源(独占模式) acquire(int)
- 首先通过tryAcquire(arg)尝试获取共享资源,如果获取成功直接返回,不成功则将线程以独占模式添加到等待队列尾部,tryAcquire(arg)由继承AQS的自定义同步器来具体实现
- 当前线程加入等待队列后,会通过acquireQueued方法基于CAS自旋不断尝试获取资源,直至获取到资源
- 自旋过程中,线程被中断,acquireQueued方法会标记此次中断,并返回true
- 若acquireQueued方法获取到资源后,返回true,则执行线程自我中断操作selfInterrupt()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 常规插入与快速插入均依赖于CAS,都依赖于unsafe类,unsafe中cas的操作都是native方法,可以保证原子性
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试将节点快速插入等待队列,若失败则执行常规插入(enq方法)
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 常规插入式自旋过程for(;;),能够保证节点插入成功
enq(node);
return node;
}
// 常规插入比快速插入多包含一种情况,即当前等待队列为空时,需要初始化队列(将待插入节点设置为头节点,也同时为尾节点)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
final boolean acquireQueued(final Node node, int arg) {
// 标识是否获取资源失败
boolean failed = true;
try {
// 标识当前线程是否被中断过
boolean interrupted = false;
// 自旋操作
for (;;) {
// 获取当前节点的前继节点
final Node p = node.predecessor();
// 如果前继节点为头结点,说明排队马上排到自己了,可以尝试获取资源,若获取资源成功,则执行下述操作
if (p == head && tryAcquire(arg)) {
// 将当前节点设置为头结点
setHead(node);
// 说明前继节点已经释放掉资源了,将其next置空,以方便虚拟机回收掉该前继节点
p.next = null; // help GC
// 标识获取资源成功
failed = false;
// 返回中断标记
return interrupted;
}
// 若前继节点不是头结点,或者获取资源失败,
// 则需要通过shouldParkAfterFailedAcquire函数,判断是否需要阻塞该节点持有的线程
// 若shouldParkAfterFailedAcquire函数返回true,则继续执行parkAndCheckInterrupt()函数,将该线程阻塞并检查是否可以被中断,若返回true,则将interrupted标志置于true
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 最终获取资源失败,则当前节点放弃获取资源
if (failed)
cancelAcquire(node);
}
}
// shouldParkAfterFailedAcquire是通过前继节点的waitStatus值来判断是否阻塞当前节点的线程的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前继节点的waitStatus值ws
int ws = pred.waitStatus;
// 如果ws的值为Node.SIGNAL(-1),则直接返回true
// 说明前继节点完成资源的释放或者中断后,会通知当前节点的,回家等通知就好了,不用自旋频繁地来打听消息
if (ws == Node.SIGNAL)
return true;
// 如果前继节点的ws值大于0,即为1,说明前继节点处于放弃状态(Cancelled)
// 那就继续往前遍历,直到当前节点的前继节点的ws值为0或负数
// 此处代码很关键,节点往前移动就是通过这里来实现的,直到节点的前继节点满足
// if (p == head && tryAcquire(arg))条件,acquireQueued方法才能够跳出自旋过程
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前继节点的ws值设置为Node.SIGNAL,以保证下次自旋时,shouldParkAfterFailedAcquire直接返回true
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
4.3.2 释放资源(独占模式)release(int)
- 通过tryRelease(arg) 来释放资源,是继承AQS的自定义同步器来具体实现的
- 当获取了资源后,找出头节点h,判断头结点不为空且waitStatus非0,则调用unparkSuccessor方法唤醒h节点的后续节点
- unparkSuccessor方法用于唤醒等待队列中下一个阻塞队列
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 获取到等待队列的头结点h
Node h = head;
// 若头结点不为空且其ws值非0,则唤醒h的后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 获取当前节点的ws值
int ws = node.waitStatus;
// 将当前节点的ws值置0
if (ws < 0) compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 若后继节点为null或者其ws值大于0(放弃状态),则从等待队列的尾节点从后往前搜索,
// 搜索到等待队列中最靠前的ws值非正且非null的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) s = t;
}
// 如果后继节点非null,则唤醒该后继节点持有的线程
if (s != null)
LockSupport.unpark(s.thread);
}
4.3.3 获取资源(共享模式)acquireShare(arg)
- 执行tryAcquireShared方法获取资源,若获取成功则直接返回,若失败,则进入等待队列,执行自旋获取资源(doAcquireShared)
- tryAcquireShared返回值为负值代表失败,0代表成功,但无资源,正数代表获取资源成功且有剩余资源,其他线程可以获取
- 当线程获取到资源后,doAcquireShared会将当前线程所在的节点设置为头节点,若资源有剩余则唤醒后续节点,比acquireQueued多了个唤醒后续节点的操作
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// doAcquireShared与AcquireQueued类似,doAcquireShared将线程的自我中断操作放在了方法体内部
private void doAcquireShared(int arg) {
// 将线程以共享模式添加到等待队列的尾部
final Node node = addWaiter(Node.SHARED);
// 初始化失败标志
boolean failed = true;
try {
// 初始化线程中断标志
boolean interrupted = false;
for (;;) {
// 获取当前节点的前继节点
final Node p = node.predecessor();
// 若前继节点为头结点,则执行tryAcquireShared获取资源
if (p == head) {
int r = tryAcquireShared(arg);
// 若获取资源成功,且有剩余资源,将自己设为头结点并唤醒后续的阻塞线程
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 如果中断标志位为真,则线程执行自我了断
if (interrupted)
selfInterrupt();
// 表征获取资源成功
failed = false;
return;
}
}
// houldParkAfterFailedAcquire(p, node)根据前继节点判断是否阻塞当前节点的线程
// parkAndCheckInterrupt()阻塞当前线程并检查线程是否被中断过,若被中断过,将interrupted置为true
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 放弃获取资源
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
// 记录原来的头结点,下面过程会用到
Node h = head;
// 设置新的头结点
setHead(node);
// 如果资源还有剩余,则唤醒后继节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
// 自旋操作
for (;;) {
// 获取等待队列的头结点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点的线程
unparkSuccessor(h);
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
4.3.4 释放资源(共享模式)releaseShared(int)
- tryReleaseShared(int)由继承AQS的自定义同步器来具体实现
public final boolean releaseShared(int arg) {
// 尝试释放资源
if (tryReleaseShared(arg)) {
// 唤醒后继节点的线程
doReleaseShared();
return true;
}
return false;
}
4.4 AQS实践示例
- 整个获取/释放资源的过程是通过传播完成的,如最开始有10个资源,线程A、B、C分别需要5、4、3个资源
A线程获取到5个资源,其发现资源还剩余5个,则唤醒B线程;
B线程获取到4个资源,其发现资源还剩余1个,唤醒C线程;
C线程尝试取3个资源,但发现只有1个资源,继续阻塞;
A线程释放1个资源,其发现资源还剩余2个,故唤醒C线程;
C线程尝试取3个资源,但发现只有2个资源,继续阻塞;
B线程释放2个资源,其发现资源还剩余4个,唤醒C线程;
C线程获取3个资源,其发现资源还剩1个,继续唤醒后续等待的D线程;
… - ReentrantLock
- state初始化为0,表示未锁定状态,state表示当前线程获取锁的可重入次数
- A线程lock()时,会调用tryAcquire()独占锁将state+1
- 之后其他线程再想tryAcquire时就会失败,直到A线程unlock()到state=0位置,其他线程才有机会获取该锁
- A释放锁之前,自己是可以重复获取此锁state累加, 即为可重用状态
- 获取多少次就要释放多少次锁,保证state是能够回到零态的
- ReentrantReadWriteLock
- state的高16位表示获取读的次数,低16位表示获取写的次数
- Semaphore
- state用来表示当前可用信号的个数
- CountDownlatch
- 任务分N个子线程去执行,state就初始化为N,state即为当前计数器的值
- N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减1
- 当N个子线程执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作
4.5 AQS实现Lock原理
4.6 AQS写锁获取
AQS中state切分两部分,高16位表示读,低16位表示写
- 判断同步状态state是否为0
- state=0, 还没有线程获取锁
- state!=0, 已有线程获取锁
- state!=0时,会判断state的低16位(w)是否为0
- w=0,说明其它线程获取了读锁,此时直接进入CLH队列进行阻塞等待(读写锁互斥)
- w!=0, 说明有线程获取了写锁,需判断当前线程是否持有锁
- 未持有, 进入CLH队列进行阻塞
- 持有, 判断当前线程获取写锁次数是否超过最大次数,超过则抛出异常,否则更新state写同步状态
4.7 AQS读锁获取
- AQS中state切分两部分,高16位表示读,低16位表示写
- 判断同步状态state是否为0
- state=0, 还没有线程获取锁,需要判断是否阻塞
- 需要阻塞,则进入CLH队列
- 不需要阻塞,则CAS更新state的读状态
- state!=0,其他线程已获取了锁
- state=0, 还没有线程获取锁,需要判断是否阻塞
- state!=0,需要判断state的低16位状态
- 如果存在写锁,则直接进入CLH队列
- 不存在写所以,则判断当前线程是否应该被阻塞
- 需要阻塞,则进入CLH队列
- 不需要阻塞,则CAS更新state的读状态
4.8 AQS中阻塞队列/条件队列
- 当多线程同时调用lock()方法时,只有一个线程获取到锁,其他线程都会被转换为Node节点放到AQS阻塞队列中,并做CAS自旋尝试获取锁
- 当获取到锁的线程对应的条件变量的await()方法调用时,该线程就会释放锁,并把当前线程转为Node节点放到条件变量对应的条件队列中
- 这时AQS阻塞队列中会有一个节点可以得到锁,这个线程又恰巧调用了对应条件变量的await()方法,又会重复上一步,然后阻塞队列中又会有一个节点中的线程获取锁
- 有一个线程调用条件变量的signal()或signalAll()方法,就会把条件队列中一个或者所有节点都移动到AQS阻塞队列中,然后调用unpark方法进行授权,等待时机获取锁
- 一个锁对应一个阻塞队列,但是对应多个条件变量,每个条件变量对应一个条件队列;这两个队列中存放的都是Node节点,Node节点中封装了线程及其状态
4.9 自定义Lock锁(AQS实现)
public class Mutex implements Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int acquires) {
assert acquires == 1; //限定只能为1个
if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
@Override
protected boolean tryRelease(int acquires) {
assert acquires == 1; //限定只能为1个
if (getState() == 0) {//持有锁判断
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);//资源释放
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
// 自定义同步器
private final Sync sync = new Sync();
// 获取资源,直到成功才返回
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
public void unlock() {
sync.release(1);
}
// 锁是否占有
public boolean isLocked(){
return sync.isHeldExclusively();
}
}
public class TestMutex {
final static Mutex lock = new Mutex();
final static Condition canProducer = lock.newCondition();
final static Condition canConsumer = lock.newCondition();
final static Queue<String> queue = new LinkedBlockingQueue<>();
final static int queueSize = 100;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
// 获取独占锁
lock.lock();
try {
while (queue.size() == queueSize) {
//队列满了则等待
canProducer.await();
}
// 添加元素到队列
queue.add("element");
System.out.println("Thread:" + Thread.currentThread().getId() + "生产一个元素");
// 唤醒消费线程
canConsumer.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread consumer = new Thread(() -> {
// 获取独占锁
lock.lock();
try {
while (0 == queue.size()) {
// 队列为空则等待
canConsumer.await();
}
// 消费一个元素
String element = queue.poll();
System.out.println("Thread:" + Thread.currentThread().getId() + "消费一个元素");
// 唤醒生产线程
canProducer.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
// 启动线程
producer.start();
consumer.start();
}
}
5 CAS
5.1 CAS定义
- 全称为Compare and Swap(比较与交换),是一种无锁算法,在不使用锁的情况下实现多线程之间的变量同步
- 需要读写的内存值V,进行比较的值A,要写入的新值B,当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会进行任何操作,一般都会自旋方式更新
- ABA问题可以使用AtomicStampedReference来解决ABA问题
循环时间长开销大,长时间不成功,导致一直自旋,给CPU带来开销 - 只能保证同一个变量的原子操作,只能保证对一个共享变量操作的原子性,多个变量操作不能保证原子性
5.2 Java实现CAS操作
使用Unsafe调用CPU底层指令实现原子操作
// java.util.concurrent.atomic.AtomicInteger
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
// sun.misc.Unsafe
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public native int getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
5.3 处理器实现原子操作
- CAS是调用处理器底层指令来实现原子操作的
- 处理器中存在缓存(L1/L2)用于缓冲处理器与物理内存之间的通信
- 处理器提供了总线锁定和缓存锁定两种机制来保证复杂内存操作的原子性
- 总线锁定
- 当处理器要操作一个共享变量时,会在总线上会发出一个Lock信号,此时其它处理器就不能操作共享变量了
- 总线锁定在阻塞其他处理器获取该共享变量的操作请求时,也可能会导致大量阻塞,从而增加系统的性能开销
- 缓存锁定(后来出现)
- 当某个处理器对缓存中的共享变量进行了操作,就会通知其他处理器放弃存储或者重新读取该共享变量
- 目前最新的处理器都支持缓存锁定机制
- 总线锁定
6 线程
6.1 线程状态
状态 | 说明 |
---|---|
New | 线程刚创建 |
Runnable | 线程所需的资源准备就绪 |
Blocked | 线程暂停执行,直到获得请求的锁 |
Waiting | 进入无限制的等待 |
TimedWaiting | 进入一个有时间限制的等待 |
Terminated | 线程结束 |
6.2 线程的分类
- 守护线程
是一种特殊的线程,一般为在后台完成系统性服务,如GC,JIT线程
只有守护线程时Java虚拟机会退出,守护线程的设置thread.setDaemon(true),必须在thread.start()之前才有效 - 用户线程
6.3 线程的优先级
- Java中线程优先级为1-10
6.4 线程的操作
6.4.1 新建线程
- 继承Thread类并重写run方法
- 优点
- 创建简单
- 缺点
- 本方法已继承了Thread类,Java不支持多继承
- 任务与代码没有分离
- 没有返回值
- 只能使用主线程里面声明为final的变量
- 优点
public class MyThread extends Thread{
public void run(){...}
}
MyThread thread = new MyThread();
thread.start();
- 实现Runnable接口的run方法
- 优点
- 可以继承其他类,方便自定义传参
- 缺点
- 没有返回值
- 优点
public class MyRunnableTask implements Runnable{
public void run(){...}
}
MyRunnableTask myTask = new MyRunnableTask();
new Thread(myTask).start();
- 使用FutureTask方式
- 优点
- 可以继承其他类,方便自定义传参
- 可以有返回值
- 异步调用
- 优点
public class MyCallerTask implements Callable<String>{
public String call() throws Exception{return "hello";}
}
FutureTask<String> futureTask = new FutureTask<>(new MyCallerTask());
new Thread(futureTask).start();
try{
String res = futureTask.get();
}catch(Exception e){...}
6.4.2 等待和通知
wait()系和notify()系都是Object类中的方法,任何对象都可以调用这两个方法,使用必须先要获取对象的监视器锁(必须在synchronized语句中),否则会抛出IllegalMonitorStateException
- wait方法: 让当前线程进入等待状态,并释放它持有的锁
- 当在一个对象实例上调用obj.wait()方法后,当前线程就会在这个对象上等待,直到其它线程调用obj.notify/notifyAll()方法
- 当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的
- 这里obj对象成了多线程之间的通信手段
- Object.wait()和Thread.sleep()都可以让线程等待若干时间,wait()方法可以被唤醒且会释放持有目标对象的锁,而sleep()方法不会释放任何资源
- notify方法: notify/notifyAll是本地方法
- 一个线程调用共享对象的notify方法后,会唤醒一个在该共享变量上调用wait系统方法后被挂起的线程
- 一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的/唤醒所有该共享变量调用wait方法而被挂起的线程
- 被唤醒的线程不能马上从wait方法返回并继续执行,必须在获取了对象的监视器锁后才可以返回
- 虚假唤醒情况
- 虚假唤醒是一个线程可以从挂起状态变为可运行状态(被唤醒), 即使该线程没有被其他线程调用notify,notifyAll方法进行通知,或者被中断,或者等待超时
- 虚假唤醒实际应用中很少发生,但也应该防范未然
- 解决办法是在一个循环中调用wait()方法进行防范,退出的条件是满足了唤醒该线程的条件
synchronized(obj){
while (条件不满足) {
obj.wait();
}
}
6.4.3 等待线程结束
- 等待线程结束join方法
join()方法的本质就是调用线程对象wait(),JDK源码中join()的核心片段while(isAlive){wait(0);},会让调用线程在当前线程对象上进行等待. 所以不要在Thread对象实例上使用wait()方法和notify()方法,可能会影响系统API,或者被系统API影响
// 会无限等待,会一直阻塞当前线程,直到目标线程执行完毕
public final void join() throws InterruptedExceptiion;
// 给出了一个最大等待时间,超过给定时间目标线程还未执行,就会继续往下执行
public final synchronized void join(long millis) throws InterruptedException;
- 谦让yeild方法:
可以在不重要的线程,优先级较低的,怕占用较多CPU的线程上,适当的时候调用Thread.yield方法
// 是静态方法,一旦执行,会让当前线程让出CPU,但是让出CPU后还会继续进行CPU资源争夺
public static native void yield();
6.4.4 线程睡眠
- 睡眠Thread.sleep(ms)
- 指让当前正在运行的占用CPU时间的线程挂起ms毫秒,把CPU的时间交给其他线程,此时持有的监视器资源(锁)是不会让出的,但是并没有指定把CPU的时间片交给哪个线程,而是让这些线程自己去竞争
- 线程睡眠的时间到了,也不会立即被运行,只是从睡眠状态变为了可运行状态,是不会由睡眠状态直接变为运行状态
6.4.5 线程中断
- 线程中断interrupted
- 线程中断并不会使线程立即退出,而是给目标线程发送一个通知,告知目标线程有人希望你退出. 目标线程收到通知后如何处理,完全由目标线程自行决定
- 线程调用了wait系列方法,join方法,sleep方法而被阻塞挂起,这时此线程调用了interrupt方法,线程则会抛出InterruptedException异常
- Thread.sleep()方法由于中断而抛出异常,此时会清除中断标记,如果不加处理,则下次循环开始,无法捕获这个中断,所以在异常处理catch中需要手动设置这个中断标记为Thread.currentThread().interrupt();
//通知目标线程中断,设置中断标志位
public void Thread.interrupt();
//判断是否被中断,不会清除中断标志,内部实现是调用isInterrupted(false);不清除中断标志
public boolean Thread.isInterrupted();
//判断目标线程是否被中断,并清除当前中断状态, 内部实现是currentThread().isInterrupted(true);true则是清除中断标志
public static boolean Thread.interrupted();
Thread threadOne = new Thread(new Runnable(){...});
threadOne.start();
threadOne.interrupt();
log.info(threadOne.isInterrupted()); // true
log.info(threadOne.interrupted()); // false
log.info(Thread.interrupted()); // false
log.info(threadOne.isInterrupted()); // true
threadOne.interrupted()等价于Thread.interrupted(),获取当前线程的中断标志
6.4.6 线程优雅退出
//线程优雅推出的示例
public void run(){
try{
...
while(!Thread.currentThread().isInterrupted()&& more work to do){
// do more work
}
}cache(InterruptedException e){
//thread was interrputed during sleep or wait or join
}finally{
//cleanup,if required
}
}
6.4.7 线程停止
- 终止线程stop()方法,暴力停止线程,以弃用过时
- stop方法会立刻停止run()方法中剩余的全部工作,包括在catch或finally语句中的,并且抛出ThreadDeath异常(通常情况下异常不需要显示的捕获),所以可能会导致一些请理性的工作得不到完成,如文件,数据库等关闭
- stop方法会立刻释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题(赋值对象A的3个字段,赋值了1个字段后stop,对象A则出现脏数据)
6.4.8 线程挂起和继续执行
- 挂起suspend,继续执行resume, 被挂起的线程必须要等到resume方法操作后,才能继续运行
- 早已都被标记为废弃方法,不推荐使用,因为suspend挂起方法不会释放任何锁资源,且如果resume方法在suspend方法之前执行,则可能导致线程永远被挂起
- 被挂起的线程状态上还是Runnable
6.5 线程组的操作
ThreadGroup tg =new ThreadGroup("Test");
Thread t1 =new Thread(tg,new MyThread(),"T1");
t1.start();
tg.activeCount();//activeCount()可以统计活动线程的总数
tg.list();//list()可以打印出这个线程组中所有线程的信息
7 ThreadLocal
- 是JDK包提供的,提供了线程本地变量,每创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存,当多个线程操作这个变量时,实际操作的是自己本地内存的变量,从而避免了线程安全问题
- 必须收回自定义的ThreadLocal变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题,尽量在代理中使用try-finally进行回收
static final ThreadLocal<Long> LONG_THREAD_LOCAL = new ThreadLocal<>();
run(){
LONG_THREAD_LOCAL.set(RANDOM.nextLong());
LONG_THREAD_LOCAL.get();
LONG_THREAD_LOCAL.remove();
}
7.1 ThreadLocal的分类
- ThreadLocal 同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的
- InheritableThreadLocal 同一个InheritableThreadLocal 变量在父线程中被设置值后,在子线程中是可以获取到值的
7.2 Thread实现原理
- Thread类中有threadLocals和inheritableThreadLocals,都是ThreadLocalMap类型的变量,实质是定制化的HashMap
- 默认情况下,每个线程中的这两个变量都为null,只有在当前线程第一次调用ThreadLocal的set或get方法时才会创建
- 每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面
- ThreadLocal就是一个工具壳,通过set方法把value值放入调用线程的threadLocals里存放,当调用get方法时,再从当前线程threadLocals变量里面将其拿出来
- set方法
// 首先获取调用线程t,然后,找到调用线程自己的本地变量threadLocals,并将其绑定到线程的成员变量map上,如果map不为空则将value设置到threadLocals中,如果map为空则说明是第一次调用set方法,这时会创建线程的threadLocals变量
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) map.set(this, value);
else createMap(t, value);
}
- get方法
// 首先获取调用线程t,然后,找到调用线程自己的本地变量threadLocals,并将其绑定到线程的成员变量map上,如果map不为空则直接返回当前绑定的本地变量,如果为空则执行初始化
// 初始化的逻辑: 如果当前线程threadLocals变量不为空,则设置当前线程的本地变量为null,如果为空则调用createMap方法创建当前线程的threadLocals变量
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//this指当前ThreadLocal实例
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- remove方法
// 首先获取调用线程t,然后,找到调用线程自己的本地变量threadLocals,并将其绑定到线程的成员变量m上,如果m不为空则删除当前线程中指定的ThreadLocal实例的本地变量
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
8 Random
- 随机数 只有物理方法才可以得到真正的随机数,我们计算机产生的随机数都是伪随机数
- Random next方法首先获取当前原子变量种子的值,然后根据当前种子的值计算新的种子,然后再使用CAS的机制更新种子的值,保证多线程竞争情况下只有一个能更新成功.最后使用固定算法根据新的种子计算随机数, 局限性在有大量线程并发情况下,通过CAS机制更新随机数种子会导致大量线程自旋,耗费CPU性能,导致系统吞吐量下降
8.1 ThreadLocalRandom
- 是JDK7在JUC包下新增的随机数生成器,弥补了Random类在多线程下的缺陷
- Random的缺点是多个线程会使用同一个原子性种子变量,从而导致原子变量更新的竞争
- ThreadLocalRandom则是在每个线程都维护一个种子变量,每个线程生成随机数时都会根据自己老的种子计算新的种子,并使用新种子更新老种子,再根据新种子计算随机数,这样就不会存在竞争问题,大大提高并发能力
- 使用方式: ThreadLocalRandom random = ThreadLocalRandom.current(); random.nextInt();
9 Unsafe类
JDK中rt.jar包中Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,使用JNI的方式访问本地C++库
10 跳表
- 是一种可以用用来快速查找的数据结构,类似于平衡树,时间复杂度为O(log n),是一种以空间换时间的算法
- 平衡树的插入和删除操作都可能导致平衡树进行一次全局的调整,高并发下需要全局锁
- 跳表的插入和删除操作只需要对整个数据结构的局部进行操作即可,高并发下需要部分锁
10.1 跳表结构
- 跳表的本质是同时维护了多个链表,并且链表是分层的
- 底层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层链表的子集,一个链表插入哪些层完全是随机的
- 跳表内所有的链表元素都是有序的,查找时,先从顶级链表开始查找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续查找
- Node
跳表内部的节点是由Node组成,每个Node表示一个节点,里面包含key和value,next元素,Node中所有的操作都采用CAS方法
static final class Node<K,V>{
final K key;
volatile Object value;
volatile Node<K,V> next;
}
boolean casValue(Object cmp,Object val){ //设置value值
return UNSAFE.compareAndSwapObject(this,valueOffset,cmp,val);
}
boolean casNext(Node<K,V> cmp,Node<K,V> val){ //设置next值
return UNSAFE.compareAndSwapObject(this,nextOffset,cmp,val);
}
- Index
整个跳表就是根据Index组织成的,内部包装了Node,同时增加了向下的引用和向右的引用
static class Index<K,V>{
final Node<K,V> node;
final Index<K,V> down;
volatile Index<K,V> right;
}
- HeadIndex
跳表每一层的表头还需记录当前处于哪一层,用HeadIndex表示
static final class HeadIndex<K,V> extends Index<K,V>{
final int level;
HeadIndex(Node<K,V> node,Index<K,V> down,Index<K,V> right, int level){
super(node,down,right);
this.level = level;
}
}
10.2 跳表与红黑树&链表区别
- ConcurrentHashMap为何不采用跳表,要使用红黑树+链表?
- 跳表是使用空间换时间