JUC并发编程底层原理
1. 并发的三大特性
- 可见性
- 有序性
- 原子性
2. 可见性
2.1 并发并行
并发是一个处理器处理多个任务,会分配时间片给多个任务,看起来是多个任务一起执行
并行是多个处理器处理多个任务
2.2 JMM模型
2.2.1 工作流程
1. jmm用的内存共享模型是,每个线程都有一个本地内存,会缓存一些全局变量。
2. 如果线程对某个全局变量做了修改,其实就是对主内存和这个线程的本地内存做了修改,但不会更新到其他线程的本地内存;由于其他线程会从自己的本地内存读取(本地就相当于每个线程自己的缓存),所以其他线程可能会发生脏读,也就是可见性的问题
3. 可用使用内存屏障保证可见性
2.2.2 线程结束把值刷回主存,还是立马刷回主存
不是立马刷回主存,使用内存屏障回立马刷回主存
2.2.3 本地内存什么时候过期
跟redis一样,都有缓存的过期策略;本地内存过期就从主存拿数据
2.2.4 可见性的两种方式
2.2.4.1 内存屏障
- 原理:linux系统 x86版本,jvm底层实现它其实不会去调硬件的内存屏障,而且通过c++语言会去执行一个lock的前缀指令,然后会立马刷新内存(“lock; addl $0,0(%%rsp)” : : : “cc”, “memory”)
1 inline void OrderAccess::storeload() { fence(); }
2 inline void OrderAccess::fence() {
3 if (os::is_MP()) {
4 // always use locked addl since mfence is sometimes expensive
5 #ifdef AMD64
6 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
7 #else
8 __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
9 #endif
10 }
- 如何使用:像lock锁、synchronized锁和volatile它们的底层都会去调jvm层面的内存屏障
2.2.4.2 线程上下文切换
- 原理:上下文切换会有几十毫秒的等待时间,当切换回来时,线程重新载入数据时,本地内存可能存在已过期的情况
2.2.4.3 CPU缓存架构&缓存一致性协议
-
总线窥探(MESI)
当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。
Write-invalidate,值被修改,过期其他缓存(这种cpu用的比较多)
Write-update,值被修改,更新其他缓存
mesi,从主内存加载值,标记为独享状态,其他线程也从主内存加载这个值,标记为共享状态;c线程修改这个值,c线程的状态为修改状态,其他线程的本地内存状态为无效状态,下次加载时,会从主内存读取。
-
总线仲裁机制
类似于串行化的原子性操作,用的不多了解即可
3. 有序性
3.1指令重排序
不管是jvm还是汇编层面,都会在不影响程序结果的情况下,对代码执行顺序进行优化,此过程叫指令的重排序。(内存屏障可以禁止重排序)
3.2 原理
因为有指令重排序的情况存在,在单线程执行下不影响流程,多线程则会因为执行顺序的变化影响流程
例:
Order order = new Order();
代码顺序:
1. 在jvm堆中开辟一个内存给new Order对象
2. 在内存中创建order对象
3. order变量指向 内存地址
重排序后的执行顺序:
1. 在jvm堆中开辟一个内存给new Order对象
2. order变量指向 内存地址
3. 在内存中创建order对象
结论:因为第三步和第二步的顺序调换,如果此时有其他线程判断order == null,因为重排序的原因,代码执行完第二步,order == null 会为true,但其实这个内存中还没有创建这个对象,只是有了这个地址;如果禁止重排序,那么必须等第三步执行完,order == null 才会为true。
4. 集合
4.1 ArrayList
元素有放入顺序,元素可重复 。底层采用数组来实现的,需要维护下标,增删慢(尾部增删也快)查询快。
4.2 LinkedList
元素有放入顺序,元素可重复 。底层采用双向链表来实现的,有指针指向,增删快查询慢。
4.3 HashSet(Set)
元素无放入顺序,元素不可重复;底层采用HashMap来实现,用hashmap的keys做列表。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
// add方法直接用的hashmap的put。key为add的值 value是一个常量不用关注。
return map.put(e, PRESENT)==null;
}
}
4.4 HashMap
存储结构: jdk1.7用数组、链表。1.8用数组、链表、红黑树。
put原理: 首先会对put的key值进行hash运算然后取模,得到下标,如果这个下标的数据没有值就直接赋值,如果出现hash冲突,就以链表的方式存储(1.8尾插。1.7头插),如果数组容量超过阈值就进行扩容,扩容会重新计算hash;1.8如果链表长度超过8,就会变成红黑树来存储。
为什么多线程put会导致cpu百分百: 1.7的头插法,会导致最后进来的值在链表的头部,即ab两个值,a先进b后进。链表的顺序就是b-》a,如果扩容重新计算hash,hashmap用的是单向链表,只能从链表头部遍历拿值,重新计算后的链表顺序可能为a-》b,顺序发生了颠倒;多线程的场景下,线程1put的时候在扩容之前链表的顺序b-》a,同时间线程2put扩容完成后顺序变成了a-》b,导致循环指向。1.8的尾插法,扩容不会发生链表顺序的调换,所以不会出现这种场景。
为什么数组长度一定是2的次幂:为了&运算,增加hash运算的散列性和效率,方便数据迁移,旧数据不需要重新计算,要么是原下标,要么是原下标加上扩充的长度就等于扩容后的下标。
思维拓展:库分表不是经常会根据某个id进行hash运算取模得出库或者表的下标,如果下标不够用了需要扩容,就需要数据迁移,有点类似hashmap底层一样,那是不是可以模仿hashmap底层和扩容方式,用2的n次方作下标的数量,用&计算代替取模,这样下次数据迁移就不用计算下标了,要么是原来的下标,要么是原来的下标+n(原来的下标数量),这样下次分库分表扩容的时候 数据迁移会方便很多 不用重新计算下标
4.5 ConcurrentHashMap
特点:并发安全的HashMap ,比Hashtable效率更高;Hashtable是直接把整个put方法加上synchronized;
1.7原理:对比Hashtable,ConcurrentHashMap的优点是锁的粒度比较小,采用是的分段锁,数据结构对比hashmap也有所改变,把每个数组对应的链表,换成了Segment(就是一个hashmap);在put的时候,先会算出下标,确定put到哪个Segment对象,如果这个Segment对象为null,就初始化;然后对这个Segment对象的put方法加锁,扩容也只会对Segment对象的数组扩容,ConcurrentHashMap的数组不会扩容。
思维拓展:
1.不管是1.7还是1.8的锁,只要是非阻塞,在未获取到锁的时候;总会让线程去做点别的事情,比如帮助扩容,比如1.7的提前计算下标,提前遍历链表看有没有重复的k(如果k重复就直接更换value就行),比如提前创建一些对象,那么我们在写业务代码的时候,如果有个接口用到了非阻塞锁+循环获取锁的情况下。在没有获取到锁的情况下可以提前去做一些别的事情,new对象或者查数据库或者一些其他的计算和判断,在不影响逻辑的情况在,提前去做,等获取到锁了,就可以省略这些步骤,提高接口整体性能。
2.分段锁的思想可以好好利用,可以减少锁竞争。不管是1.7的Segment还是1.8的addcount都是采用把一个任务分成很多个小任务,同时不影响正常的业务逻辑。
1.8原理:1.8采用的是cas+synchronized锁,首先一进来就是一个死循环,判断有没有初始化,没有的就去初始化,然后计算k的数组下标,如果这个下标的数组为空,就用cas的赋值,如果不为空,就用synchronized锁住,然后判断这个节点是红黑树还是链表,然后在put进去。然后还会判断这个map是不是在扩容,如果是就会帮助扩容;还会判断是否需要树化等;
最后就是addcount了,对map的长度+1,ConcurrentHashMap是有两种计数器,一个是basecount,一个是cellcount,cellcount是很多个,放在一个数组里面,ConcurrentHashMap的size方法就是把这两种计数器相加,就得到长度,addcount首先是所有线程都用cas的方式对basecount+1,如果没抢到锁,就计算下标,去竞争这个下标的cellcount,对cellcount+1,也是cas的方式;有点像1.7的分段锁;
如果到达一定长度就会进行扩容,ConcurrentHashMap的扩容的多线程的,而且是一个下标一个下标的进行数据迁移,如果对5这个下标进行扩容,首先会在这个下标创建一个ForwardingNode对象,代表这个下标正在扩容,其他线程帮助扩容的时候就会跳过这个下标,然后在数据迁移的时候会用synchronized锁锁住这个下标,其他线程无法往这个下标put值。
5. 线程池
入参:核心线程数、最大线程数、超时时间、队列、拒绝策略
原理:每个任务都会交给核心线程去处理,如果核心线程满了,就放队列里面,队列满了就继续创建线程;如果达到最大线程数,剩下的任务就交给拒绝策略;如果任务核心线程能够处理完成,那剩下的线程达到一定的过期时间还没任务处理,就自动销毁。jdk提供的拒绝策略有4种,放弃剩下任务、放弃任务并抛异常、交给主线程处理、放弃旧任务处理新任务、自定义拒绝策略
CPU密集型任务: 务最佳的线程数为 CPU 核心数的 1~2 倍
IO密集型任务: 线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间),具体线程数根据实际场景不断压测调整。
5.1 ArrayBlockingQueue
基于数组的有界阻塞队列,生产者和消费者用了同一把ReentrantLock锁,用了不同的条件队列来阻塞生产者消费者。采用了环形数组的方式,当消费者取值时,不会删除掉该元素,而是直接设置为null。
5.2 LinkedBlockingQueue
基于链表结构的阻塞队列,不设置容量大小时;为无界队列,生产者和消费者用了不同ReentrantLock锁,用了不同的条件队列来阻塞生产者消费者。因为存取用了不同的锁,效率会高于ArrayBlockingQueue队列。
5.3 SynchronousQueue
采用了cas+自旋的操作,提供了公平和非公平链表(先进先出和先进后出),队列容量是0,用于传递性场景
5.4 PriorityBlockingQueue
无界队列,生产者和消费者用了同一把ReentrantLock锁,优先级高的先出队,用于有高低优先级的场景。
5.5 DelayQueue
入队不阻塞,出队为空时阻塞,用于延迟的场景。
6. cas
原理:会传一个原值和修改的值,首先会判断这个原值和内存中对应的值是否相等,如果不相等那就返回内存中的值,如果相等就把内存中的值修改成要更新的值;这个读和写的操作是原子性的,并且多个线程去cas操作,只有一个线程会成功,同时其他失败的线程不会是阻塞状态;一般失败的线程会自旋操作,直到cas成功。
jdk原理:汇编层面就是调用了原子指令来进行cas操作,虽然满足了原子性,但是有序性和可见性不能保证;所以在原子指令前面还加了一个lock前缀指令,也就是内存屏障来保证可见性和有序性,同时jdk是直接回返的Boolean值。
缺点
- 自旋操作比较消耗cpu性能
- aba问题,可以使用版本号解决
- 只能保证一个共享变量原子操作
7.synchronized
7.1 对象内存布局
对象头: 有Mark Word和klass类型指针组成,Mark Word包含锁状态和偏向线程id和hashcode码;klass类型指针虚拟机通过这个指 针来确定这个对象是哪个类的实例。
实例数据:存放类的属性数据信息,包括父类的属性信息。
对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。
7.2 偏向锁
在没有其他线程竞争,只有一个线程获取锁的时候,就是偏向锁状态
7.3 轻量锁
采用cas自旋的方式去获取锁,在用户态执行。
7.4 重量锁
阻塞其他线程的方式,在内核态执行。
7.5 锁升级的过程
new一个对象的时候,如果此时偏向锁没有开启,那么是无锁状态,如果开启则是偏向锁状态,只不过是匿名偏向,还没有线程id。如果只有一个线程获取锁,那么此时是线程id是有记录的,如果发生如果线程竞争,同时线程数量很多或者等待时间太长,会之间从偏向锁升级成重量锁;反之,会升级成轻量锁,同过cas的方式去自旋获取锁,当自旋到达一定次数;升级成重量级锁,阻塞其他线程,放入等待队列。
8.AQS
组成
- 有一个voliate修饰state的,标志是否持有锁(0 无锁,大于0有锁)
- 同步队列,双向链表,获取锁失败后放入这个队列
- Condition条件队列,单向链表,线程等待时进入条件队列并且释放锁;如果被唤醒,则进入等待队列去竞争锁
特性
-
阻塞等待队列
-
共享/独占
-
公平/非公平
-
可重入
-
允许中断
原理:AQS是一个抽象同步框架, Lock、 CountDownLatch、Semaphore 等都用到了 AQS,采用模板设计模式,把独占锁/共享锁的加锁解锁交给子类实现;
独占锁-加锁/解锁后逻辑 :首先判断加锁是否成功,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态封装成一个Node节点,通过cas+死循环的方式加入到同步队列中,采用的尾插,同时阻塞该线程,当同步状态释放时,会把头节点唤醒,使其再次尝试获取锁。
共享锁-加锁/解锁后逻辑 :加锁逻辑和独占锁一样,释放锁时,会把头节点唤醒,使其再次尝试获取锁。如果获取锁成功,就会唤醒下一个线程,直到后续线程获取锁失败,继续阻塞。
await: 在Condition接口中,首先把这个线程添加到条件队列中(不存在就创建队列,没有用cas的方式,因为await方法必须在独占锁中使用,是线程安全的),然后释放掉这个线程的独占锁,并阻塞该线程。
signalAll:在Condition接口中,把条件队列的节点,全部迁移到等待队列中。
8.1 ReentrantLock
加锁
1. NonfairSync为ReentrantLock的内部类
// NonfairSync 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// cas的方式修改State的值(也就是用cas的方式获取锁)
if (compareAndSetState(0, 1))
// 获取锁成功的线程设置为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 这个方法是aqs的获取锁的方法,里面调用了tryAcquire方法
acquire(1);
}
// 重写aqs的独占锁获取锁的方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
2. nonfairTryAcquire方法具体实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取aqs的State值(等于0代表无锁)
int c = getState();
if (c == 0) {
// 无锁 cas一次,如果改成功则获取锁成功
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
/**
* 1.current(当前线程) 是否等于 getExclusiveOwnerThread(获取锁的线程)
* 2. 如果相等那么是重入锁状态,State+1写入
* 3. 由此可以看出ReentrantLock中,state等于0代表无锁,state大于0代表有锁
* state的次数代表重入的次数
*/
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
3. 非公平锁实现
// 其他逻辑和公平锁一样,获取锁的时候多了一个hasQueuedPredecessors()方法,判断同时是头节点(或者等待队列没有线程的时候)的情况下 才会取尝试获取锁
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
解锁
// 1. 直接把State设置为0 2. 把当前获取锁的线程标记 设置为null
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;
}
原理总结 :ReentrantLock重写了aqs独占锁的加锁解锁方法,采用cas的方式获取锁,同时支持可重入锁;state等于0代表无锁,state大于0代表有锁, state的次数代表重入的次数;公平锁就是只取等待队列的头部线程去获取锁(或者队列没有线程),非公平锁,会进行两次cas去获取锁;第二次获取失败,add到等待队列;解锁就是把state设置为无锁,把当前获取锁的标记设置为null。
使用场景: 用于单线程执行同时支持可重入。
8.2 Semaphore
加锁
// 非公平锁 初始化的时候需要传一个信号量
NonfairSync(int permits) {
super(permits);
}
// 重写aqs的共享锁 方法
protected int tryAcquireShared(int acquires) {
// 小于0获取锁失败
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
// 一进来就是死循环
for (;;) {
// 获取 State
int available = getState();
// State-acquires
int remaining = available - acquires;
// remaining小于0 代表没有资源 或者 cas成功 退出循环
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
// 公平锁的实现
protected int tryAcquireShared(int acquires) {
for (;;) {
// 其他逻辑一样 多了一个判断是否头节点
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
解锁
protected final boolean tryReleaseShared(int releases) {
// 死循环的方式+releases 然后cas的方式设置到内存
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
原理总结 :Semaphore重写了aqs共享锁的加锁解锁方法,初始化的时候传一个资源量,获取锁的时候通过死循环+cas的对资源量减少,直到cas成功或者资源小于0,小于0获取锁失败;大于0获取成功。解锁的时候对资源量进行回加操作。
使用场景: 用于限流等场景。
8.2 CountDownLatch
await
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
------------------------------------------------------------------------------------------------------------
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果State != 0 就放入阻塞队列
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
------------------------------------------------------------------------------------------------------------
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
countDown
public void countDown() {
sync.releaseShared(1);
}
------------------------------------------------------------------------------------------------------------
public final boolean releaseShared(int arg) {
// cas+自旋的方式 State-- 如果State cas后等于0 就唤醒等待的线程线程
if (tryReleaseShared(arg)) {
// 唤醒线程
doReleaseShared();
return true;
}
return false;
}
------------------------------------------------------------------------------------------------------------
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
原理总结 :CountDownLatch用的共享锁,调用await方法,如果State不等于0,阻塞该线程,其他线程用过countDown方法,对State自减,自减后等于0,就唤醒等待的线程线程
使用场景: 用于一个线程等待其他线程完成操作。
8.3 CyclicBarrier
await
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 里面使用了ReentrantLock
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 对count-1
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
// 如果count==0 那么唤醒所有等待的线程 放入等待队列
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
// 阻塞线程 并放入条件队列
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
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();
}
}
private void nextGeneration() {
// 唤醒所有等待线程 条件队列 转到等待队列
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
原理总结 :用了ReentrantLock和条件队列,先用ReentrantLock加锁,并对计数器减减操作,如果计数器等于0 就唤醒所有线程(吧把条件队列的节点转到同步队列),并把计数器重置,如果不等于0 就把这个线程阻塞放入条件队列,并且释放锁。
使用场景: 用于多个线程阻塞,当阻塞数量到达指定值时就放行。
8.4 ReentrantReadWriteLock
// 写锁 加锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 获取读写锁的状态
int c = getState();
// 获取写锁的状态
int w = exclusiveCount(c);
if (c != 0) {
// 写锁未加锁 || 当前线程不等于 获取写锁的线程 (因为写写 和写读互斥,所以 只要State被上锁, 如果不是重入写锁的情况下都默认失败。)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 重入锁 设置重入次数
setState(c + acquires);
return true;
}
// 公平锁需要先入等待队列 || cas操作尝试获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
// 写锁 解锁
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
// 获取写锁的线程 设置null
setExclusiveOwnerThread(null);
// 设置写锁的状态
setState(nextc);
return free;
}
// 读锁 加锁
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 写锁已被加锁 && 获取写锁的线程不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁状态
int r = sharedCount(c);
// 公平锁先入等待队列 && 读锁小于 读锁最大次数 && cas上读锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// r==0代表首次上读锁
if (r == 0) {
// 设置 第一个读锁的 线程和重入次数
firstReader = current;
firstReaderHoldCount = 1;
// 首次读锁记录 == 当前线程
} else if (firstReader == current) {
// 首次读锁重入次数+1
firstReaderHoldCount++;
} else {
// 不是首次的读锁 用ThreadLocal记录读锁的重入次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// cas上读锁失败的情况下 循环去获取读锁
return fullTryAcquireShared(current);
}
// 读锁 解锁
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 如果是首次的读锁
if (firstReader == current) {
// 首次读锁重入次数==1
if (firstReaderHoldCount == 1)
// 线程记录设置null
firstReader = null;
else
// 重入次数-1
firstReaderHoldCount--;
} else {
// 非首个读锁 也是一样的逻辑 在ThreadLocal里处理重入次数 和线程记录
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// 死循环用cas操作 释放读锁
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
原理总结 :写写互斥读写互斥 读读不互斥,利用State的高低位来记录写锁和读锁;读和写锁都支持可重入,读锁的重入次数用ThreadLocal存储,写锁采用的是独占锁,读锁是共享锁;写锁加锁时除开重入写锁,只要有读锁或者写锁都加锁失败。读锁加锁时除开重入写锁,只要有写锁就加锁失败。
使用场景: 读多写少的场景。
9. ThreadLocal
9.1 原理
public
class Thread implements Runnable {
// 线程对象里面的ThreadLocalMap 所以说这个map不是共用放在ThreadLocal里,而是放在线程里面的。
ThreadLocal.ThreadLocalMap threadLocals = null;
--------------------------------------------------------------------------------------
public class ThreadLocal<T> {
// get方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程里的ThreadLocalMap (t.threadLocals;)
ThreadLocalMap map = getMap(t);
if (map != null) {
// ThreadLocalMap的key是ThreadLocal对象 所以这里get方法直接get(this)就能拿到value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 为空就设置初始化的value
return setInitialValue();
}
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程里的ThreadLocalMap (t.threadLocals;)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 设置value
map.set(this, value);
} else {
createMap(t, value);
}
}
}
用一个map装所存储的值,这个map就是ThreadLocalMap,这个map不是放在ThreadLocal里面,而是放在Thread中,作为一个属性和内部类,它的key就是ThreadLocal对象,是弱引用,value就是存的值,当调用ThreadLocal的get方法时,会先从Thread中拿到map,然后get(this)就可以拿到value值。
这样设计的好处就是一个Thread对应一个map,就不用所有的线程都用同一个map,从而产生线程不安全问题,然后加锁导致性能低。
9.2 为什么会内存泄漏
当ThreadLocal对象被回收,然后线程一直处于工作状态就会出现,因为线程不销毁,map就一直存在,然后ThreadLocal对象被回收就无法使用map又无法回收。可以使用remove方法来避免。
9.3 为什么要用弱引用
ThreadLocalMap的key使用了弱引用,而它的key是ThreadLocal,也就是说,就算ThreadLocal对象存在,但是它的value和线程不存在了,这个map也会被回收。
如果不使用弱引用,就算value和线程都被回收了,只要ThreadLocal对象存在,那么这个线程的ThreadLocalMap还是不会回收,造成资源浪费,因为ThreadLocal本来就是线程共用的,基本不会销毁,或者说我a线程的map不用了,为什么要等其他线程线程和ThreadLocal对象销毁,应该立马清除掉a线程的map。