文章目录
一、Synchronized
1.1、原子性
- 原子性:
数据库的事务:ACID
A:原子性-事务是一个最小的执行的单位,一次事务的多次操作要么都成功,要么都失败。
并发编程的原子性:一个或多个指令在CPU执行过程中不允许中断的。
i++;操作是原子性?
肯定不是:i++操作一共有三个指令
getfield:从主内存拉取数据到CPU寄存器
iadd:在寄存器内部对数据进行+1
putfield:将CPU寄存器中的结果甩到主内存中
如何保证i++是原子性的。
锁:synchronized,lock,Atomic(CAS)
使用lock锁也会有类似的概念,也就是在操作i++的三个指令前,先基于AQS成功修改state后才可以操作
使用synchronized和lock锁时,可能会触发将线程挂起的操作,而这种操作会触发内核态和用户态的切换,从而导致消耗资源。
CAS方式就相对synchronized和lock锁的效率更高(也说不定),因为CAS不会触发线程挂起操作!
CAS:compare and swap
线程基于CAS修改数据的方式:先获取主内存数据,在修改之前,先比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃这次修改
CAS就是比较和交换,而比较和交换是一个原子操作
CAS在Java层面就是Unsafe类中提供的一个native方法,这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略,自己实现!
CAS问题:
CAS只能对一个变量的修改实现原子性。
CAS存在ABA问题。
A线程修改主内存数据从1~2,卡在了获取1之后。
B线程修改主内存数据从1~2,完成。
C线程修改主内存数据从2~1,完成。
A线程执行CAS操作,发现主内存是1,没问题,直接修改
解决方案:加版本号
在CAS执行次数过多,但是依旧无法实现对数据的修改,CPU会一直调度这个线程,造成对CPU的性能损耗
synchronized的实现方式:CAS自旋一定次数后,如果还不成,挂起线程
LongAdder的实现方式:当CAS失败后,将操作的值,存储起来,后续一起添加
CAS:在多核情况下,有lock指令保证只有一个线程在执行当前CAS
1.2 有序性
指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。
如果不希望CPU对指定进行重排序,怎么办?
可以对属性追加volatile修饰,就不会对当前属性的操作进行指令重排序。
什么时候指令重排:满足happens-before原则,即可重排序
单例模式-DCL双重判断。
申请内存,初始化,关联是正常顺序,如果CPU对指令重排,可能会造成
申请内存,关联,初始化,在还没有初始化时,其他线程来获取数据,导致获取到的数据虽然有地址引用,但是内部的数据还没初始化,都是默认值,导致使用时,可能出现与预期不符的结果
1.3 可见性
可见性:前面说过CPU在处理时,需要将主内存数据甩到我的寄存机中再执行指令,指向完指令后,需要将寄存器数据扔回到主内存中。倒是寄存器数据同步到主内存是遵循MESI协议的,说人话就是,
不是每次操作结束就将CPU缓存数据同步到主内存。造成多个线程看到的数据不一样。
volatile每次操作后,立即同步数据到主内存。
synchronized,触发同步数据到主内存。
final,也可以解决可见性问题。
2、synchronized使用
synchronized方法
synchronized代码块
类锁和对象锁:
类锁:基础当前类的Class加锁
对象锁:基于this对象加锁
synchronized是互斥锁,每个线程获取synchronized时,基于synchronized绑定的对象去获取锁!
synchronized锁是基于对象实现的!
synchronized是如何基于对象实现的互斥锁,先了解对象再内存中是如何存储的。
3、synchronized锁升级
synchronized不存在从重量级锁降到偏向或者轻量
synchronized 在偏向锁升级到轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点,才可以撤销,并发偏向锁撤销比较消耗资源。
在程序启动时,偏向锁有一个延迟开启的操作,因为项目启动时,ClassLoader会加载.class文件,这里会设计到synchronized操作
- 无锁状态、匿名偏向状态:没有线程拿锁。
- 偏向锁:没有线程的竞争,只有一个线程再获取锁资源。线程竞争锁资源时,发现当前synchronized没有线程占用资源,并且锁都是偏向锁,使用CAS的方式,设置o的线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获取到锁资源。
- 轻量级锁:偏向锁出现竞争时,会升级到轻量级锁。
- 重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁
- 偏向锁是延迟开启的,并且在开启偏向锁之后,默认不存在无锁状态,只存在匿名偏向Synchronized因为不存在从重量级锁降级到偏向或者轻量。
锁消除:线程在执行一段synchronized 代码块时,发现没有共享数据的操作,自动把synchronized去掉
锁膨胀:在一个多次循环的操作中频繁的获取和释放锁资源,可能会有花
二、ReentrantLock源码
1、ReentrantLock介绍
如果竞争比较激烈,推荐lock锁,效率更高。
如果没有竞争,推荐synchronized
原因:synchronized只有锁升级,当升级到重量级锁后,无法降级到轻量级,偏向锁。
synchronized是非公平锁,lock是公平+非公平
lock锁更完善,lock可以使用trylock指定等待锁的时间。
lock锁还提供了lockintereuptibly允许线程在获取锁的期间被中断。
synchronized基于对象实现,lock锁基于AQS+CAS实现。
2、ReentranctLock的lock方法源码
非公平锁:上来就先尝试将state从0修改为1,如果成功,代表获取锁资源,如果没有成功,调用acquire
公平锁:调用acquire
state是AQS中的一个由volatile修饰的int类型变量,多个线程会通过CAS的方式修改state,在并发情况下,只会有一个线程成功的修改state(从0~1)
如果修改state失败怎么办?
如果线程没有拿到锁资源,会到AQS的双向链表中排队等待(在其间,线程节能会挂起)
AQS的双向链表是基于内部类Node维护的,
//公平锁
final void lock() {
acquire(1);
}
//非公平锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
3、ReentranctLock的acquire方法源码
acquire是一个业务方法,里面并没有实际的业务处理
调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法,没有拿到所资源,需要执行&&后面的方法。
当没有获取锁资源后,会先吊用addwaiter方法:会将没有获取到锁资源的线程封装为Node对象,//并且插入到AQS的队列的末尾,并且作为tail
继续调用Acquirequeue方法,查看当前排队的Node是否在队列的前面,如果在前面,尝试获取锁资源,如果没在前面,就将线程挂起。
// 核心acquire arg = 1
public final void acquire(int arg) {
//1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法。 没有拿到锁资源, // 需要执行&&后面的方法
//2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象, // 并且插入到AQS的队列的末尾,并且作为tail
//3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源 // 如果没在前面,尝试将线程挂起,阻塞起来!
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4、ReentranctLock的tryAcquire方法源码
tryAcquire分为公平和非公平两种、
tryAcquire主要做了两件事:
1、如果state为0,尝试获取锁资源。
2、如果state不为0,看一下是不是锁重入操作。
// 非公平锁实现!
final boolean nonfairTryAcquire(int acquires) {
// 拿到当前线程!
final Thread current = Thread.currentThread();
// 拿到AQS的state
int c = getState();
// 如果state == 0,说明没有线程占用着当前的锁资源
if (c == 0) {
// 没人占用锁资源,我直接抢一波(不管有没有线程在排队)
if (compareAndSetState(0, acquires)) {
// 将当前占用这个互斥锁的线程属性设置为当前线程 setExclusiveOwnerThread(current); // 返回true,拿锁成功
return true; }
}
// 当前state != 0,说明有线程占用着锁资源
// 判断拿着锁的线程是不是当前线程(锁重入)
else if (current == getExclusiveOwnerThread()) {
// 将state再次+1
int nextc = c + acquires;
// 锁重入是否超过最大限制 // 01111111 11111111 11111111 11111111 + 1
// 10000000 00000000 00000000 00000000 // 抛出error
if (nextc < 0) throw new Error("Maximum lock count exceeded");
// 将值设置给state setState(nextc);
// 返回true,拿锁成功
return true; }
return false; }
公平锁实现
// 公平锁实现
protected final boolean tryAcquire(int acquires) {
// 拿到当前线程!
final Thread current = Thread.currentThread();
// 拿到AQS的state int c = getState();
// 阿巴阿巴~~~~
if (c == 0) {
// 判断是否有线程在排队,如果有线程排队,返回true,配上前面的!,那会直接不执行返回最外层的false
if (!hasQueuedPredecessors() &&
// 如果没有线程排队,直接CAS尝试获取锁资源
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true; }
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock countexceeded");
setState(nextc);
return true; }
return false; }
5、ReentranctLock的addWaiter方法源码
在获取锁资源失败后,需要将当前线程封装为Node对象,并且插入到AQS队列的末尾
// 将当前线程封装为Node对象,并且插入到AQS队列的末尾
private Node addWaiter(Node mode) {
// 将当前线程封装为Node对象,mode为null,代表互斥锁
Node node = new Node(Thread.currentThread(), mode);
// pred是tail节点
Node pred = tail;
// 如果pred不为null,有线程正在排队
if (pred != null) {
// 将当前节点的prev,指定tail尾节点
node.prev = pred;
// 以CAS的方式,将当前节点变为tail节点
if (compareAndSetTail(pred, node)) {
// 之前的tail的next指向当前节点
pred.next = node;
return node;
}
}
// 添加的流程为, 自己prev指向、tail指向自己、前节点next指向我
// 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列
enq(node);
return node;
} 、
// enq,无论怎样都添加进入
private Node enq(final Node node) {
for (;;) {
// 拿到tail
Node t = tail;
// 如果tail为null,说明当前没有Node在队列中
if (t == null) {
// 创建一个新的Node作为head,并且将tail和head指向一个Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 和上述代码一致!
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t; } } } }
6、ReentranctLock的Acquirequeue方法源码
acquireQueued方法会查看当前排队的Node是否是head的next,如果是,尝试获取锁资源,如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
在挂起线程前,需要确认当前节点的上一个节点的状态必须是小于等于0,
如果为1,代表是取消的节点,不能挂起
如果为-1,代表挂起当前线程
如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程
// acquireQueued方法
// 查看当前排队的Node是否是head的next,
// 如果是,尝试获取锁资源,
// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
final boolean acquireQueued(final Node node, int arg) {
// 标识。
boolean failed = true;
try {
// 循环走起
for (;;) {
// 拿到上一个节点
final Node p = node.predecessor();
if (p == head && // 说明当前节点是head的next
tryAcquire(arg)) { // 竞争锁资源,成功:true,失败:false
// 进来说明拿到锁资源成功
// 将当前节点置位head,thread和prev属性置位null
setHead(node);
// 帮助快速GC
p.next = null;
// 设置获取锁资源成功
failed = false;
// 不管线程中断。
return interrupted;
}
// 如果不是或者获取锁资源失败,尝试将线程挂起
// 第一个事情,当前节点的上一个节点的状态正常!
// 第二个事情,挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
// 通过LockSupport将当前线程挂起
parkAndCheckInterrupt())
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 确保上一个节点状态是正确的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 拿到上一个节点的状态
int ws = pred.waitStatus;
// 如果上一个节点为 -1
if (ws == Node.SIGNAL)
// 返回true,挂起线程
return true;
// 如果上一个节点是取消状态
if (ws > 0) {
// 循环往前找,找到一个状态小于等于0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将小于等于0的节点状态该为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
7、ReentranctLock的unlock方法源码
释放锁资源:
1、state-1;
2、如果state减为0,唤醒在队列中排队的Node(一定唤醒离Node最近的)
// 真正释放锁资源的方法
public final boolean release(int arg) {
// 核心的释放锁资源方法
if (tryRelease(arg)) {
// 释放锁资源释放干净了。 (state == 0)
Node h = head;
// 如果头节点不为null,并且头节点的状态不为0,唤醒排队的线程
if (h != null && h.waitStatus != 0)
// 唤醒线程
unparkSuccessor(h);
return true;
}
// 释放锁成功,但是state != 0
return false;
}
// 核心的释放锁资源方法
protected final boolean tryRelease(int releases) {
// 获取state - 1
int c = getState() - releases;
// 如果释放锁的线程不是占用锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否成功的将锁资源释放利索 (state == 0)
boolean free = false;
if (c == 0) {
// 锁资源释放干净。
free = true;
// 将占用锁资源的属性设置为null
setExclusiveOwnerThread(null);
}
// 将state赋值
setState(c);
// 返回true,代表释放干净了
return free; }
// 唤醒节点
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
// 如果头节点状态小于0,换为0
if (ws < 0) compareAndSetWaitStatus(node, ws, 0);
// 拿到当前节点的next
Node s = node.next;
// 如果s == null ,或者s的状态为1
if (s == null || s.waitStatus > 0) {
// next节点不需要唤醒,需要唤醒next的next
s = null;
// 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)
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);
}
为什么唤醒线程时,从尾部往前找,而不是从头部往后找???
因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前的Node,最后才是上一个节点的next指针,指向当前Node。
三、ReentrantReadWriteLock读写锁源码
1、为什末要出现读写锁
因为ReentrantLock是互斥锁,如果一个操作时读多写少,同时还需要保证线程安全,那么使用ReentrantLock会导致效率比较低。
因为多个线程在对同一个数据进行读操作时,也不会造成线程安全问题。
所以出现了ReentranctReadWriteLock锁:
读操作时共享的。
写操作时互斥的。
读写操作是互斥的。
写读操作是互斥的。
单个线程获取写锁后,再次获取读锁,可以拿到。(写读可重入)
单个线程获取读锁后,再次获取写锁,拿不到。
使用方式:
public class XxxTest {
// 读写锁!
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 写锁
static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
// 读锁
static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main(String[] args) throws InterruptedException {
readLock.lock();
try {
System.out.println("拿到读锁!");
} finally {
readLock.unlock();
}
writeLock.lock();
try {
System.out.println("拿到写锁!");
} finally {
writeLock.unlock();
}
}
}
2、读写锁的核心思想
ReentrantReadWriteLock还是基于AQS实现的,很多功能的实现和ReentrantLock类似 还是基于AQS的state来确定当前线程是否拿到锁资源。
state表示读锁:将state的高16位作为读锁的标识
state表示写锁:将state的低16位作为写锁的标识
锁重入问题:
写锁重入怎么玩:因为写操作和其他操作是互斥的,代表同一时间,只有一个线程持有写锁,只要锁重入,就对低位+1即可,而且锁重入的限制,从原来的2^31-1,变为了
每个读操作的线程,在获取读锁时,都需要开辟一个ThreadLocal,读写锁为了优化这个事情,做了两手操作:
- 第一个拿到读锁的线程,不需要ThreadLocal记录重入锁,在读写锁内有一个firstRead记录冲入次数
- 还记录了最后一个拿到读锁的线程的冲入次数,交给cacheHoldCounter属性标识,可以避免频繁的在锁重入时,从TL中获取。
读锁重入的时候就不操作state了?不对,每次锁重入还要修改state,只是记录当前线程锁重入的次数,需要基于ThreadLocal记录
00000000 00000000 00000000 00000000 : state
写锁:
00000000 00000000 00000000 00000001
写锁:
00000000 00000000 00000000 00000010
A读锁:拿不到,排队
00000000 00000000 00000000 00000010
写锁全部释放(唤醒)
00000000 00000000 00000000 00000000
A读锁:
00000000 00000001 00000000 00000000
B读锁:
00000000 00000010 00000000 00000000
B再次读锁:
00000000 00000011 00000000 00000000
每个读操作的线程,在获取读锁时,都需要开辟一个ThreadLocal。读写锁为了优化这个事情,做了两手操作:
- 第一个拿到读锁的线程,不用ThreadLocal记录重入次数,在读写锁内有有一个firstRead记录重入次数
- 还记录了最后一个拿到读锁的线程的重入次数,交给cachedHoldCounter属性标识,可以避免频繁的在锁重入时,从TL中获取
3、写锁的操作
3.1 、写锁加锁-acquire
public final void acquire(int arg) {
// 尝试获取锁资源(看一下,能否以CAS的方式将state 从0 ~ 1,改成功,拿锁成功)
// 成功走人
// 不成功执行下面方法
if (!tryAcquire(arg) &&
// addWaiter:将当前没按到锁资源的,封装成Node,排到AQS里
// acquireQueued:当前排队的能否竞争锁资源,不能挂起线程阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
因为都是AQS的实现,主要看tryAcquire
// state,高16:读,低16:写
00000000 00000000 00000000 00000000
00000000 00000001 00000000 00000000 - SHARED_UNIT
00000000 00000000 11111111 11111111 - MAX_COUNT
00000000 00000000 11111111 11111111 - EXCLUSIVE_MASK
&
00000000 00000000 00000000 00000001
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 只拿到表示读锁的高16位。
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 只拿到表示写锁的低16位。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 读写锁的写锁,获取流程
protected final boolean tryAcquire(int acquires) {
// 拿到当前线程
Thread current = Thread.currentThread();
// 拿到state
int c = getState();
// 拿到了写锁的低16位标识w
int w = exclusiveCount(c);
// c != 0:要么有读操作拿着锁,要么有写操作拿着锁
if (c != 0) {
// 如果w == 0,代表没有写锁,拿不到!拜拜!
// 如果w != 0,代表有写锁,看一下拿占用写锁是不是当前线程,如果不是,拿不到!拜拜!
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 到这,说明肯定是写锁,并且是当前线程持有
// 判断对低位 + 1,是否会超过MAX_COUNT,超过抛Error
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 如果没超过锁重入次数, + 1,返回true,拿到锁资源。
setState(c + acquires);
return true;
}
// 到这,说明c == 0
// 读写锁也分为公平锁和非公平锁
// 公平:看下排队不,排队就不抢了
// 走hasQueuedPredecessors方法,有排队的返回true,没排队的返回false
// 非公平:直接抢!
// 方法实现直接返回false
if (writerShouldBlock() ||
// 以CAS的方式,将state从0修改为 1
!compareAndSetState(c, c + acquires))
// 要么不让抢,要么CAS操作失败,返回false
return false;
// 将当前持有互斥锁的线程,设置为自己
setExclusiveOwnerThread(current);
return true;
}
剩下的addWaiter和acquireQueued和ReentrantLock看的一样,都是AQS自身提供的方法
3.2 写锁-释放锁操作
读写锁的释放操作,跟ReentrantLock一致,只是需要单独获取低16位,判断是否为0,为0就释放成功
// 写锁的释放锁
public final boolean release(int arg) {
// 只有tryRealse是读写锁重新实现的方法,其他的和ReentrantLock一致
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 读写锁的真正释放
protected final boolean tryRelease(int releases) {
// 判断释放锁的线程是不是持有锁的线程
if (!isHeldExclusively())
// 不是抛异常
throw new IllegalMonitorStateException();
// 对state - 1
int nextc = getState() - releases;
// 拿着next从获取低16位的值,判断是否为0
boolean free = exclusiveCount(nextc) == 0;
// 返回true
if (free)
// 将持有互斥锁的线程信息置位null
setExclusiveOwnerThread(null);
// 将-1之后的nextc复制给state
setState(nextc);
return free;
}
4、读锁的操作
4.1 读锁的加锁操作
// 读锁加锁操作
public final void acquireShared(int arg) {
// tryAcquireShared,尝试获取锁资源,获取到返回1,没获取到返回-1
if (tryAcquireShared(arg) < 0)
// doAcquireShared 前面没拿到锁,这边需要排队~
doAcquireShared(arg);
}
// tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 拿到state
int c = getState();
// 那写锁标识,如果 !=0,代表有写锁
if (exclusiveCount(c) != 0 &&
// 如果持有写锁的不是当前线程,排队去!
getExclusiveOwnerThread() != current)
// 排队!
return -1;
// 没有写锁!
// 获取读锁信息
int r = sharedCount(c);
// 公平锁: 有人排队,返回true,直接拜拜,没人排队,返回false
// 非公平锁:正常的逻辑是非公平直接抢,因为是读锁,每次抢占只要CAS成功,必然成功
// 这就会出现问题,写操作无法在读锁的情况抢占资源,导致写线程饥饿,一致阻塞…………
// 非公平锁会查看next是否是写锁的,如果是,返回true,如果不是返回false
if (!readerShouldBlock() &&
// 查看读锁是否已经达到了最大限制
r < MAX_COUNT &&
// 以CAS的方式,对state的高16位+1
compareAndSetState(c, c + SHARED_UNIT))
{
// 拿到锁资源成功!!!
if (r == 0) {
// 第一个拿到锁资源的线程,用first存储
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 我是锁重入,我就是第一个拿到读锁的线程,直接对firstReaderHoldCount++记录重入的次数
firstReaderHoldCount++;
} else {
// 不是第一个拿到锁资源的
// 先拿到cachedHoldCounter,最后一个线程的重入次数
HoldCounter rh = cachedHoldCounter;
// rh == null: 我是第二个拿到读锁的!
// 或者发现之前有最后一个来的,但是不我,将我设置为最后一个。
if (rh == null || rh.tid != getThreadId(current))
// 获取自己的重入次数,并赋值给cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
// 之前拿过,现在如果为0,赋值给TL
else if (rh.count == 0)
readHolds.set(rh);
// 重入次数+1,
// 第一个:可能是第一次拿
// 第二个:可能是重入操作
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current); }
// 通过tryAcquireShared没拿到锁资源,也没返回-1,就走这
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
// 拿state
int c = getState();
// 现在有互斥锁,不是自己,拜拜!
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current) return -1;
// 公平:有排队的,进入逻辑。 没排队的,过!
// 非公平:head的next是写不,是,进入逻辑。 如果不是,过!
} else if (readerShouldBlock()) {
// 这里代码特别乱,因为这里的代码为了处理JDK1.5的内存泄漏问题,修改过~
// 这个逻辑里不会让你拿到锁,做被阻塞前的准备
if (firstReader == current) {
// 什么都不做
} else { if (rh == null) {
// 获取最后一个拿到读锁资源的
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
// 拿到我自己的记录重入次数的。
rh = readHolds.get();
// 如果我的次数是0,绝对不是重入操作!
if (rh.count == 0)
// 将我的TL中的值移除掉,不移除会造成内存泄漏
readHolds.remove();
}
}
// 如果我的次数是0,绝对不是重入操作!
if (rh.count == 0)
// 返回-1,等待阻塞吧!
return -1;
}
}
// 超过读锁的最大值了没?
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 到这,就CAS竞争锁资源
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 跟tryAcquireShared一模一样
if (sharedCount(c) == 0) { firstReader = current;
firstReaderHoldCount = 1; }
else if (firstReader == current) {
firstReaderHoldCount++; }
else { if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh;
}
return 1;
}
}
}
4.2 加锁-扔到队列准备阻塞操作
// 没拿到锁,准备挂起
private void doAcquireShared(int arg) {
// 将当前线程封装为Node,当前Node为共享锁,并添加到队列的模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try { boolean interrupted = false;
for (;;) {
// 获取上一个节点
final Node p = node.predecessor();
if (p == head) {
// 如果我的上一个是head,尝试再次获取锁资源
int r = tryAcquireShared(arg);
if (r >= 0) {
// 如果r大于等于0,代表获取锁资源成功
// 唤醒AQS中我后面的要获取读锁的线程(SHARED模式的Node)
setHeadAndPropagate(node, r);
p.next = null; if (interrupted) selfInterrupt();
failed = false;
return;
}
}
// 能否挂起当前线程,需要保证我前面Node的状态为-1,才能执行后面操作
if (shouldParkAfterFailedAcquire(p, node) &&
//LockSupport.park挂起~~
parkAndCheckInterrupt())
interrupted = true; }
} finally {
if (failed) cancelAcquire(node);
}
}
四、线程池源码
1、介绍
java构建线程的方式:
- new Thread
- new Runnable
- new Callable
为了避免频繁创建和销毁线程造成不必要的性能,一般在使用线程时,会采用线程池。
核心线程数的设置方案:
CPU密集型和IO密集型:
CPU密集:将核心线程数设置为CPU内核数+1;
IO密集:将核心线程数设置为CPU内核数*2;
混合密集型:推荐拆分出两个任务,设置两个线程池,分别执行CPU密集型和IO密集型;
CPU密集:代表当前线程一直在执行指令。
IO密集:执行一段指令后,需要通过IO获取结果(调用远程服务,查询数据库)
线程池的使用方式:
public static void main(String[] args) {
ThreadPoolExecutor eh=new ThreadPoolExecutor(
1,
2,
1,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t=new Thread();
return t;
}
},
new ThreadPoolExecutor.AbortPolicy()
);
eh.execute("任务");
eh.submit("又反回结果的任务");
}
2、核心属性&状态
//AtomicInteger就是一个int,写操作用CAS实现,保证原子类
//ctl维护这线程池的2个核心内容:
//1、线程池状态(高3位,维护者线程池状态)
//2、工作线程数量(核心线程+非核心线程,低29位,维护着工作线程个数)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
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;
// 拿到线程的状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
//拿到工作线程的个数
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
ctl维护这线程池的2个核心内容:
1、线程池状态(高3位,维护者线程池状态)
2、工作线程数量(核心线程+非核心线程,低29位,维护着工作线程个数)
线程池状态:
3、execute方法
public void execute(Runnable command) {
//判空操作
if (command == null)
throw new NullPointerException();
//拿到ctl
int c = ctl.get();
//通过ctl获取到当前线程的个数,并判断是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// true:代表是核心线程,false:代表是非核心线程
if (addWorker(command, true))
return;
// 如果添加失败,重新获取ctl
c = ctl.get();
}
// 核心线程数已经到了最大值、添加时,线程池状态变为SHUTDOWN/STOP
// 判断线程池是否是运行状态 && 添加任务到工作队列
if (isRunning(c) && workQueue.offer(command)) {
//拿到ctl
int recheck = ctl.get();
//DCL double check
// 如果状态不是RUNNING,把任务从工作队列移除。
if (! isRunning(recheck) && remove(command))
// 走一波拒绝策略。
reject(command);
// 线程池状态是RUNNING。
// 判断工作线程数是否是0个。
// 可以将核心线程设置为0,所有工作线程都是非核心线程。
// 核心线程也可以通过keepAlived超时被销毁,所以如果恰巧核心线程被销毁,也会出现当前效果
else if (workerCountOf(recheck) == 0)
// 添加空任务的非核心线程去处理工作队列中的任务
addWorker(null, false);
}
// 可能工作队列中的任务存满了,没添加进去,到这就要添加非核心线程去处理任务
else if (!addWorker(command, false))
//
reject(command);
}
4、addWorker添加工作线程
private boolean addWorker(Runnable firstTask, boolean core) {
//双循环的标签
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
判断线程当前的状态
if (rs >= SHUTDOWN &&
//如果当前线程是SHUTDOWN,还要处理队列中的任务。
//如果你添加了工作线程的方式,是任务的非核心线程,并且工作队列中还有任务
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//判断工作线程的个数
for (;;) {
//获取工作线程的个数
int wc = workerCountOf(c);
// 判断1:工作线程是否已经 == 工作线程最大个数
// 判断2-true判断:判断是核心线程么?如果是判断是否超过核心线程个数
// 判断2-false判断:如果是非核心线程,查看是否超过设置的最大线程数
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//CAS操作对线程进行+1
if (compareAndIncrementWorkerCount(c))
// +1成功,跳出外层循环,执行添加工作线程的业务
// 以CAS方式,对ctl+1,多线程并发操作,只有会有一个成功
break retry;
//重新拿取Ctl
c = ctl.get();
// 判断线程池状态是否有变化
if (runStateOf(c) != rs)
continue retry;
}
}
// 添加工作线程的业务
// 工作线程启动了吗?
boolean workerStarted = false;
boolean workerAdded = false;
// Worker就是工作线程
Worker w = null;
try {
// 创建工作线程,将任务传到Worker中
w = new Worker(firstTask);
final Thread t = w.thread;
// 只有你写的线程工厂返回的是null,这里才会为null
if (t != null) {
//获取锁资源
final ReentrantLock mainLock = this.mainLock;
//加锁,我要避免在启动线程的时候,线程池状态发生变化
mainLock.lock();
try {
//重新获取ctl,拿线程池的状态
int rs = runStateOf(ctl.get());
// DCL i think you know~~~
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 判断Worker中的thread是否已经启动了,一般不会启动,除非你在线程工厂把他启动了
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//将工作线程放到,hashset中去
workers.add(w);
// 获取工作线程个数,判断是否需要修改最大工作线程数记录。 int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
// 工作线程添加成功 0
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 如果添加成功
if (workerAdded) {
// 启动工作线程
t.start();
// 设置标识为true
workerStarted = true;
}
}
} finally {
// 如果工作线程启动失败
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
// 如果添加工作线程失败,执行
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 说明worker可能存放到了workers的hashSet中。
if (w != null)
//移除
workers.remove(w);
// 减掉workerCount的数值 -1
decrementWorkerCount();
tryTerminate();
} finally {
mainLock.unlock();
}
}
5、runWorker方法执行类
final void runWorker(Worker w) {
//获取当前的线
Thread wt = Thread.currentThread();
//拿到Worker存放的Runnable
Runnable task = w.firstTask;
//将Worker清空
w.firstTask = null;
w.unlock(); // allow interrupts
//这就是一个标识
boolean completedAbruptly = true;
try {
//如果Worker自身携带任务,直接执行
// 如果Worker携带的是null,通过getTask去工作队列获取任务
while (task != null || (task = getTask()) != null) {
w.lock();
// 判断线程池状态是否大于等于STOP,如果是要中断当前线程
if ((runStateAtLeast(ctl.get(), STOP) ||
//DCL 中断当前线程
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 前置钩子
beforeExecute(wt, task);
Throwable thrown = null;
try {
//执行任务
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
//后置钩子
afterExecute(task, thrown);
}
} finally {
task = null;
// 当前工作执行完一个任务,就++
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
6、getTask工作线程排队拿任务
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 线程池状态判断
// 如果线程池状态为SHUTDOWN && 工作队列为空
// 如果线程池状态为STOP
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
//对数量的判断
int wc = workerCountOf(c);
// 判断核心线程是否允许超时?
// 工作线程个数是否大于核心线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 判断工作线程是否超过了最大线程数 && 工作队列为null
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 工作线程数有问题,必须-1,干掉当前工作线程
// 工作线程是否超过了核心线程,如果超时,就干掉当前线程
// 对工作线程个数--
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 如果是非核心,走poll,拉取工作队列任务,
// 如果是核心线程,走take一直阻塞,拉取工作队列任务
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
// 当工作队列没有任务时,这时就会被Condition通过await阻塞线程
// 当有任务添加到工作线程后,这是添加完任务后,就会用过Condition.signal唤醒阻塞的线程
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
7、processWorkerExit工作线程告辞~
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 如果是不正常操作,需要先对工作线程数-- (如果正常情况,getTask就--了)
if (completedAbruptly)
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 将当前工作线程完整的任务个数赋值给整个线程池中的任务数
completedTaskCount += w.completedTasks;
// 干掉当前工作线程
workers.remove(w);
} finally {
mainLock.unlock();
}
// 线程池是否可以中止,线程池状态是否发生变化。
tryTerminate();
int c = ctl.get();
//如果当前线程池状态小于STOP
if (runStateLessThan(c, STOP)) {
// 判断线程池中的工作队列是否还有任务,并且工作线程是否还在。
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 添加非核心空任务的线程处理工作队列中的任务
addWorker(null, false);
}
}
拒绝策略:线程池提供的拒绝策略,一般不适合你的业务场景时,你就自己定义即可。
AbortPolicy:抛出异常!
CallerRunsPolicy:让提交任务的线程处理这个任务!
DiscardPolicy:啥也不做,任务没了!
DiscardOldestPolicy:扔掉队列最前面的任务,尝试把当前任务添加进去!
任务处理流程:
主线程执行execute添加任务,线程池创建工作线程,执行任务,执行任务,再次拉取工作队列任务,直到工作队列没有任务,阻塞工作线程
工作线程阻塞在工作队列,主线程执行execute添加任务到工作队列,工作线程被唤醒,拿到工作队列中的任务执行,执行完毕,再次拉取工作队列任务,直到工作队列没有任务,阻塞工作线程
5、ConcurrentHashMap源码分析
1、结构介绍
HashMap和ConcurrentHashMap存储结构是一样的
ConcurrentHashMap是线程安全的。
存储结构:
关于put和putIfAbsent的区别
// put和putIfAbsent都是想ConcurrentHashMap中存储值。
// 如果出现key一致的,将新数据覆盖老数据,并且返回老数据
public V put(K key, V value) {
return putVal(key, value, false);
}
// 如果出现key一致的,什么都不做,返回老数据。 最只有key不存在时,才会正常的添加数据
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}
2、散列算法
final V putVal(K key, V value, boolean onlyIfAbsent) {
//ConcurrentHashMap中的key和value不能为空,hashMapkey和value可以为空
if (key == null || value == null) throw new NullPointerException();
// 散列算法就是基于key进行hash运算,并且根据散列算法的结果,确定当前key-value存储到数组的哪个索引位置。
int hash = spread(key.hashCode());
//散列算法
//散列算法是为了让Hashcode的高16位参与到索引位置的计算中,从而尽可能的打散数据存到数组上,从而减少hash冲突
// ConcurrentHashMap中,还会将hash值对HASH_BITS进行&运算,让hash值一定是一个正数。
// 因为ConcurrentHashMap中数组上的数据的hash值,如果为负数,有特殊含义
// static final int MOVED = -1; // 代表当前位置数据在扩容,并且数据已经迁移到了新数组
// static final int TREEBIN = -2; // 代表当前索引位置下,是一个红黑树。 转红黑树,TreeBin有参构造
// static final int RESERVED = -3; // 代表当前索引位置已经被占了,但是值还没放进去呢。 compute方法
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
00011000 00000110 00111000 00001100 h
^ 00000000 00000000 00011000 00000110 h >>> 16
00011000 00000110 00111000 00001100
& 00000000 00000000 00000111 11111111 2048 - 1
ConcurrentHashMap是如何根据hash值,计算存储的位置? (数组长度 - 1) & (h ^ (h >>> 16))
00011000 00000110 00110000 00001100 key1-hash
00011000 00000110 00111000 00001100 key2-hash
& 00000000 00000000 00000111 11111111 2048 - 1
3、初始化数组
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//代表当前数组还没有初始化
if (tab == null || (n = tab.length) == 0)
//对当前数组进行初始化
tab = initTable();
//初始化
private final Node<K,V>[] initTable() {
// 声明tab:临时存数组。 sc:临时存sizeCtl
Node<K,V>[] tab; int sc;
//代表当前数组还没有初始化
while ((tab = table) == null || tab.length == 0) {
//sc赋值,并判断sc是否小于0
if ((sc = sizeCtl) < 0)
//线程做让步
Thread.yield();
// 如果sc大于等于0,没人在执行初始化操作。
// 以CAS的方式,将sizeCtl,改为-1,代表当前线程正在执行初始化逻辑
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//如果当前数组没有初始化
if ((tab = table) == null || tab.length == 0) { 拿到数组的初始化长度
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//创建数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 依次给局部变量和成员变量赋值。
table = tab = nt;
//计算下次扩容的阈值
sc = n - (n >>> 2);
}
} finally {
// 将扩容阈值赋值给sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
4、添加数据-数组
final V putVal(K key, V value, boolean onlyIfAbsent) {
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// n: 数组长度。 i:索引位置。 f:i位置的数据。 fh:是f的hash值
Node<K,V> f;
int n, i, fh;
// tabAt(数组,索引位置) = 拿到数组指定索引位置的数据
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null){
// 当前索引位置数据为null。
// 以CAS的方式,将数据放到tab的i位置上,将hash,key,value封装成了一个Node对象
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) break; }
// 说明当前位置数据已经被迁移到了新数组。
else if ((fh = f.hash) == MOVED)
// 帮你扩容,快点扩容完,我好把数据放到新数组~~~
tab = helpTransfer(tab, f); } return null; }
5、整个put的流程
final V putVal(K key, V value, boolean onlyIfAbsent) {
//如果ConcurrentHashMap的key和value为空,直接抛异常
if (key == null || value == null) throw new NullPointerException();
//通过散列散发计算key的hash值
int hash = spread(key.hashCode());
//计算链表的长度
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// n: 数组长度。 i:索引位置。 f:i位置的数据。 fh:是f的hash值
Node<K,V> f; int n, i, fh;
//数组还没有初始化
if (tab == null || (n = tab.length) == 0)
//数组初始化
tab = initTable();
//如果当前节点的数据为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//执行cas操作将当前位置添加上Node
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
//如果当前的hash值为-1,代表当前数据正在扩容,并且数据已经迁移到了新数组。
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,记录链表中Node的长度
binCount = 1;
// e:暂时指向数组位置数据
for (Node<K,V> e = f;; ++binCount) {
K ek;
//拿到当前数据的hash值和数组位置hash值做比较
//当前数据的key和数组的key做比较,当签数据的key不为空,并且将当前的key和数组的key做equal和==比较
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
//获取数组的key的value值
oldVal = e.val;
//判断是否进行覆盖
if (!onlyIfAbsent)
e.val = value;
break;
}
//将当前数据的Node赋值为上一个节点
Node<K,V> pred = e;
// e指向下一个节点,并且如果e == null,说明下面没节点了
if ((e = e.next) == null) {
// 将当前的值封装为Node对象,并挂在最后一个节点的后面
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//判断当前节点的数据是否为TreeBin类型
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;
}
}
}
}
//如果binCount不等于0
if (binCount != 0) {
//判断当前的bincount是否大于8
if (binCount >= TREEIFY_THRESHOLD)
// 判断是扩容还是转红黑树
treeifyBin(tab, i);
//如果旧的值不为空,就直接返回旧的值
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
6、触发扩容
private final void treeifyBin(Node<K,V>[] tab, int index) {
//b:当前节点的数据 n数组的大小 sc:sizeCtl
Node<K,V> b; int n, sc;
//判断数组是否为null
if (tab != null) {
//判断数组的长度是否小于64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 扩容前的一些准备和业务判断
tryPresize(n << 1);
// 转红黑树操作
// 将单向链表转换为TreeNode对象(双向链表),再通过TreeBin方法转为红黑树。
// TreeBin中保留着双向链表以及红黑树!
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
//对当前的b加锁
synchronized (b) {
//DCL double check操作
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));
}
}
}
}
}
6、ConcurrentHashMap扩容
三种触发方式
达到了扩容的阈值
1、tryPreSize-初始化数组
private final void tryPresize(int size) {
// 这个判断是给putAll留的,要计算当前数组的长度(初始化)
// 如果size大于最大长度 / 2,直接将数组长度设置为最大值。
// tableSizeFor,将长度设置的2的n次幂
// c是初始化数组长度
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
// sc是给sizeCtl赋值 // -1:正在初始化阶段,//小于-1:代表正在扩容,等于0代表还没有初始化,等于1,不确定是没有初始化,还是初始化完成了
int sc;
//代表此时数组的状态没有初始化或者已经初始化完成
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//判断当前数组还没有初始化
if (tab == null || (n = tab.length) == 0) {
//给初始化数组长度赋值
n = (sc > c) ? sc : c;
//利用cas使得当前的sc=-1代表正在初始化
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;
}
}
}
//要么是c没有超过阈值,要么是超过最大值,啥事不做~~~
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//DCL
else if (tab == table) {
// 计算扩容标识戳(基于老数组长度计算扩容标识戳,因为ConcurrentHashMap允许多线程迁移数据。)
int rs = resizeStamp(n);
//这地方是一个bug永远也不会进来了
//表示当前是正在扩容或者正在初始化阶段
if (sc < 0) {
Node<K,V>[] nt;
// 判断协助扩容线程的标识戳是否一致
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 // BUG之一,在判断扩容操作是否已经到了最后的检查阶段
||sc == rs + MAX_RESIZERS // BUG之一,判断扩容线程是否已经达到最大值
|| (nt = nextTable) == null ||
transferIndex <= 0)// transferIndex为线程领取任务的最大节点,如果为0,代表所有老数据迁移任务都没领干净了
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 还没有执行扩容,当前线程可能是第一个进来执行扩容的线程
// 基于CAS的方式,将sizeCtl从原值改为 扩容标识戳左移16位
// 10000000 00011010 00000000 00000010 一定是< -1的负数,可以代表当前ConcurrentHashMap正在扩容
// 为什么是低位+2,代表1个线程扩容。 低位为5,就代表4个线程正在并发扩容
// 扩容分为2部:创建新数组,迁移数据。
// 当最后一个线程迁移完毕数据后,对低位-1.最终结果低位还是1,需要对整个老数组再次检查,数据是否迁移干
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 开始扩容操作,传入老数组~~
transfer(tab, null);
}
}
}
2、transfer
transfer方法:
计算步长
初始化新数组
线程领取迁移数据任务
判断迁移是否完成,并判断当前线程是否是最后一个完成的
查看当前位置数据是否为null
查看当前位置数据是否为fwd
链表迁移数据-lastRun机制
红黑树迁移-迁移完数据长度小于等于6,转回链表
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 创建新数组流程!
// n:老数组长度32, stride:扩容的步长16
int n = tab.length, stride;
// NCPU:4
// 00000000 00000000 00000000 00000000
// 00000000 00000000 00000100 00000000 - 1024 512 256 128 / 4 = 32
// 如果每个线程迁移的长度基于CPU计算,大于16,就采用计算的值,如果小于16,就用16
// 每个线程每次最小迁移16长度数据
// stride = 1 < 16
// 这个操作就是为了充分发挥CPU性能,因为迁移数据是CPU密集型操作,尽量让并发扩容线程数量不要太大,从而造成CPU的性能都消耗在了切换上,造成扩容效率降低
// 如果要做优化的,推荐将扩容线程数设置为和CPU内核数+1一致。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
//判断新数组是否初始化
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
//初始化数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; //将数组赋值给新数组
nextTab = nt;
// 要么OOM,要么数组长度达到最大值。
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
// transferIndex设置为老数组长度
transferIndex = n;
}
//新数组长度
int nextn = nextTab.length;
// 先看领取任务的过程!!!
// 声明fwd节点,在老数组迁移数据完成后,将fwd赋值上去
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 领任务的核心标识
boolean advance = true;
// 扩容结束了咩?
boolean finishing = false;
//扩容的for循环
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//领取任务的while循环
while (advance) {
int nextIndex, nextBound;
// 第一个判断是为了迁移下一个索引数据(暂时不管)
if (--i >= bound || finishing)
advance = false;
//说明没有任务可以领取了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// transferIndex:16
// stride:16,nextIndex:32,nextBound:16
// bound:16,i:31
// 开始领取任务,如果CAS成功,代表当前线程领取了32~16这个范围数据的迁移
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 迁移最后一段的线程干完活了,或者其他线程没有任务可以领取了。
if (i < 0) {
int sc;
// 判断结束了没,第一次肯定进不来
if (finishing) {
// 结束扩容,将nextTabl设置为null
nextTable = null;
// 将迁移完数据的新数组,指向指向的老数组
table = nextTab;
// 将sizeCtl复制为下次扩容的阈值
sizeCtl = (n << 1) - (n >>> 1);
// 结束
return;
}
// 到这,说明当前线程没有任务可以领取了
// 基于CAS的方式,将低位-1,代表当前线程退出扩容操作(如果是最后一个,还有一个额外的活)
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断我是否是最后一个完成迁移数据的线程,如果不是,直接return结束
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果到这,说明我是最后一个结束迁移数据的线程。
// finishing结束表示和advance领取任务的标识全部设置为true
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
// 如果发现迁移为主的数据为null,设置放置一个fwd,代表当前位置迁移完成
advance = casTabAt(tab, i, null, fwd);
// 是在检查时的逻辑MOVED代表当前已经扩容完成了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//对当前的桶加锁
synchronized (f) {
//拿到当前位置的数据
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 说明当前节点状态正常,不是迁移,不是红黑树,不是预留
if (fh >= 0) {
// fh与老数组进行&运算,得到runBit
// 00001111
// 00010000
// 这个计算的结果,会决定当前数据在迁移时,是放到新数组的i位置还有新数组的 i + n位置
int runBit = fh & n;
Node<K,V> lastRun = f;
// lastRun机制
// 提前循环一次链表,将节点赋值到对应的高低位Node./
// 如果链表最后面的值没有变化,那就不动指针,直接复制。
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 再次循环时,就循环到lastRun位置,不再继续往下循环
// 这样可以不用每个节点都new,避免GC和OOM问题。
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//放低位
setTabAt(nextTab, i, ln);
//放高位
setTabAt(nextTab, i + n, hn);
// 将当前迁移完的桶位置,设置上fwd,代表数据迁移完毕
setTabAt(tab, i, fwd);
// advance,代表执行下次循环,i--。
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
6、helpTransfer方法-协助扩容
// 协助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 老数组不为null,当前节点是fwd,新数组不为null
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null)
{
// 创建自己的扩容标识戳
int rs = resizeStamp(tab.length);
// 判断之前赋值的内容是否有变化,并且sizeCtl是否小于0
while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0)
// 有一个满足,就说明不需要协助扩容了 break;
// CAS,将sizeCtl + 1,代表来协助扩容了
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab); break; }
}
return nextTab;
}
return table;
}
7、JUC并发工具
1、CountDownLatch应用
CountDownLatch本身就好像一个计数器,可以让一个线程或多个线程等待其他线程完成后再执行。
应用方式
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
// 声明CountDownLatch,有参构造传入的值,会赋值给state,CountDownLatch基于AQS实现
// 3 - 1 = 2 - 1 = 1 - 1
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(() -> { System.out.println("111"); countDownLatch.countDown(); }).start();
new Thread(() -> { System.out.println("222"); countDownLatch.countDown(); }).start();
new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("333"); countDownLatch.countDown(); }).start();
// 主线会阻塞在这个位置,直到CountDownLatch的state变为0
countDownLatch.await();
System.out.println("main"); }
2、CountDownLatch核心源码分析
构造方法:
// CountDownLatch 的有参构造
public CountDownLatch(int count) {
// 健壮性校验
if (count < 0) throw new IllegalArgumentException("count < 0");
// 构建Sync给AQS的state赋值
this.sync = new Sync(count);
}
countDown方法:
// countDown方法,本质就是调用了AQS的释放共享锁操作
// 这里的功能都是AQS提供的,只有tryReleaseShared需要实现的类自己去编写业务
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 唤醒在AQS队列中排队的线程。
doReleaseShared();
return true;
}
return false;
}
// countDownLatch实现的业务
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false;
// state - 1
int nextc = c-1;
// 用CAS赋值
if (compareAndSetState(c, nextc))
return nextc == 0; } }
// 如果CountDownLatch中的state已经为0了,那么再次执行countDown跟没执行一样。
// 而且只要state变为0,await就不会阻塞线程。
await方法:
// await方法
public void await() throws InterruptedException {
// 调用了AQS提供的获取共享锁并且允许中断的方法
sync.acquireSharedInterruptibly(1); 、
}
// AQS提欧的获取共享锁并且允许中断的方法
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
// countDownLatch操作
if (tryAcquireShared(arg) < 0)
// 如果返回的是-1,代表state肯定大于0
doAcquireSharedInterruptibly(arg);
}
// CountDownLatch实现的tryAcquireShared
protected int tryAcquireShared(int acquires) {
// state为0,返回1,。否则返回-1
return (getState() == 0) ? 1 : -1; }
// 让当前线程进到AQS队列,排队去
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程封装为Node,并且添加到AQS的队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try { for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再次走上面的tryAcquireShared,如果返回的是的1,代表state为0
int r = tryAcquireShared(arg);
if (r >= 0) {
// 会将当前线程和后面所有排队的线程都唤醒。
setHeadAndPropagate(node, r);
p.next = null;
// help GC
failed = false;
return; } }
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } }
finally { if (failed) cancelAcquire(node); } }
3、Semaphore应用
也是常用的JUC并发工具,一般用于流控。比如有一个公共资源,多线程都可以访 问时,可以用信号量做限制。
连接池,内部的链接对象有限,每当有一个线程获取连接对象时,对信号量-1,当这个线程归还资源时对信号量+1。
如果线程拿资源时,发现Semaphore内部的资源个数为0,就会被阻塞。
Semaphore是实例
// Semaphore有公平和非公平两种竞争资源的方式。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
// 设置资源个数,State其实就是信号量的资源个数
Sync(int permits) { setState(permits);
}
// 阿巴阿巴~
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 公平
protected int tryAcquireShared(int acquires) {
for (;;) {
// 公平方式,先好看队列中有木有排队的,有排队的返回-1,执行doAcquireSharedInterruptibly去排队
if (hasQueuedPredecessors())
return -1;
// 那state
int available = getState();
// remaining = 资源数 - 1
int remaining = available - acquires;
// 如果资源不够,直接返回-1
if (remaining < 0 ||
// 如果资源够,执行CAS,修改state
compareAndSetState(available, remaining))
return remaining; } }
// 非公平
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining; } }
release:
// 两个一起 阿巴阿巴
public void release() {
sync.releaseShared(1); }
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 唤醒在AQS中排队的Node,去竞争资源
doReleaseShared();
return true; }
return false;
}
// 信号量实现的归还资源
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 拿state
int current = getState();
// state + 1
int next = current + releases;
// 资源最大值,再+1,变为负数
if (next < current) throw new Error("Maximum permit count exceeded");
// CAS 改一手
if (compareAndSetState(current, next)) return true; } }
分析AQS中PROPAGATE类型节点(唯一的难点):
JDK1.5中,使用信号量时,可能会造成在有资源的情况下,后继节点无法被唤醒。
在JDK1.8中,问题被修复,修复方式就是追加了PROPAGATE节点状态来解决。
共享锁在释放资源后,如果头节点为0,无法确认真的没有后继节点。如果头节点为0,需要将头节点的状态修改为-3,当最新拿到锁资源的线程,查看是否有后继节点并且为共享锁,就唤醒排队的线程
五、CyclicBarrier应用:
一般称为栅栏,和CountDownLatch很像。
CountDownLatch在操作时,只能使用一次,也就是state变为0之后,就无法继续玩了。
CyclicBarrier是可以复用的,他的计数器可以归位,然后再处理。而且可以在计数过程中出现问题后,重置当前CyclicBarrier,再次重新操作!
应用一波
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
// 声明栅栏
CyclicBarrier barrier = new CyclicBarrier(3,() -> { System.out.println("打手枪!"); });
new Thread(() -> { System.out.println("第一位选手到位");
try { barrier.await(); System.out.println("第一位往死里跑!"); } catch (Exception e) { e.printStackTrace(); } }).start();
new Thread(() -> { System.out.println("第二位选手到位"); try { barrier.await(); System.out.println("第二位也往死里跑!"); } catch (Exception e) { e.printStackTrace(); } }).start();
System.out.println("裁判已经到位");
barrier.await(); }
await:
线程执行await方法,会对count-1,再判断count是否为0
如果不为0,需要添加到AQS中的ConditionObject的Waiter队列中排队,并park当前线程
如果为0,证明线程到齐,需要执行nextGeneration,会先将Waiter队列中的Node全部转移到AQS的队列中,并且有后继节点的,ws设置为-1。没有后继节点设置为0。然后重置count和broker标记。等到unlock执行后,每个线程都会被唤醒。
// 选手到位!!!
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
// 加锁?? 因为CyclicBarrier是基于ReentrantLock-Condition的await和singalAll方法实现的。
// 相当于synchronized中使用wait和notify
// 别忘了,只要挂起,会释放锁资源。
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 里面就是boolean,默认false
final Generation g = generation;
// 判断之前栅栏加入线程时,是否有超时、中断等问题,如果有,设置boolean为true,其他线程再进来,直接凉凉
if (g.broken) throw new BrokenBarrierException();
if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); }
// 对计数器count--
int index = --count;
// 如果--完,是0,代表突破栅栏,干活!
if (index == 0) {
// 默认false
boolean ranAction = false;
try {
// 如果你用的是2个参数的有参构造,说明你传入了任务,index == 0,先执行CyclicBarrier有参的任务
final Runnable command = barrierCommand;
if (command != null) command.run();
// 设置为true
ranAction = true; nextGeneration();
return 0; } finally {
if (!ranAction) breakBarrier(); } }
// --完之后,index不是0,代表还需要等待其他线程
for (;;) { try {
// 如果没设置超时时间。 await()
if (!timed) trip.await();
// 设置了超时时间。
await(1,SECOND)
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier(); throw ie; } else { Thread.currentThread().interrupt(); } }
if (g.broken) throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
]throw new TimeoutException(); } } } finally { lock.unlock(); } }
// 挂起线程
public final void await() throws InterruptedException {
// 允许中断
if (Thread.interrupted()) throw new InterruptedException();
// 添加到队列(不是AQS队列,是AQS里的ConditionObject中的队列)
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 挂起当前线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } }
// count到0,唤醒所有队列里的线程线程
private void nextGeneration() {
// 这个方法就是将Waiter队列中的节点遍历都扔到AQS的队列中,真正唤醒的时机,是unlock方法
trip.signalAll();
// 重置计数器
count = parties;
// 重置异常判断
generation = new Generation(); }