2.19 并发(2)

本文深入解析Java中的ThreadLocal机制,包括其内部结构、源码分析、扩容与清理机制,以及解决hash冲突的方法。同时探讨了ThreadLocal可能导致的内存泄漏问题及其解决方案。此外,还介绍了Lock接口的实现,如ReentrantLock的公平锁与非公平锁的原理。
摘要由CSDN通过智能技术生成

1.ThreadLocal

线程是隔离的。

1.引用类型

强引用:

不会被GC,OOM也不会回收对象,可能造成内存泄漏。

软引用:

会被回收,当我GC时发现内存不足,就会回收。内存足够,就不会回收

弱引用:

只要发生了GC,都会被回收。

虚引用:

主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。

2.ThreadLocal结构

Thread是单个线程,ThreadLocal是对象,Thread LocalMap / Entry 是类,Entry的数据结构是数组,默认大小为16。

Key是弱引用,Value是强引用。

3.源码解析

3.1 get方法

 进入setInitaialvalues()方法,可设置初始值。如果ThreadLocalMap不为空,进入getEntry方法  

 进入createMap()方法                                 进入getEntry()方法

 this是调用此方法的对象,firstValue是传入的

用户设置的初始值,进入ThreadLocalMap     又进入setInitialValue()方法,因为Map不为空,

()方法。                                                     进入map.set(this,value)方法。

 在此方法中使用了INITAL_CAPACITY

这个选取与斐波那契散列有关 ,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂等取模,得到的结果分布很均匀,从而减少hash冲突。算出下标值,把对象和初始值放入。设置Threshold,默认是0,到达3分之2后会进行扩容。

3.2 set方法

//key为当前占用线程的对象,value为用户初始设定值
private void set(ThreadLocal<?> key, Object value) {
//得到stringThreadLocal对象的下标 
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//得到的下标位置是null 不进循环。循环为了用户在需求中设置值
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//同一个对象,直接进行值的覆盖
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//不进循环后,在下标的位置创建new Entry
tab[i] = new Entry(key, value);
//sz=前下标值
int sz = ++size;
//扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

3.3 扩容机制

默认>=长度的3分之2就会调用方法进行扩容,进入rehash()方法。

当size达到4分之3的threshold后会进行resize()方法。

扩大原来长度的2倍 ,最终entry达到2分之1就会扩容。

private void resize() {
Entry[] oldTab = table; //去拿到之前的数组
int oldLen = oldTab.length; //拿到之前的数组长度
int newLen = oldLen * 2; //大小变成以前的2倍
Entry[] newTab = new Entry[newLen]; //new一个2倍的数组
int count = 0;
//把数组原来的entry给到我的新数组
for (int j = 0; j < oldLen; ++j) {
//得到原来的entry
Entry e = oldTab[j];
//不等于null.要做迁移
if (e != null) {
//获取entry的key threadLocal对象
ThreadLocal<?> k = e.get();
//key可能被回收 被回收就直接是无效数据了
if (k == null) {
e.value = null; // Help the GC 帮助value
去GC
} else {
//正常的数据 根据新的数据长度获取下标
int h = k.threadLocalHashCode & (newLen -
1);
//线性探测去解决hash冲突
while (newTab[h] != null)
h = nextIndex(h, newLen);
//找到一个存放的位置
newTab[h] = e;
//数量++
count++;
}
}
}
//重新设置Threshold
setThreshold(newLen);
//赋值size
size = count;
//返回新的table
table = newTab;

3.4 清理机制

进入cleanSomeSlots()方法。

清除完毕后会整理entry数组

3 异常的场景

hash冲突:hash值相等时候

循环去找下一个值为null的entry,到底后从0位置继续循环,直至找到null后放入。

实质是使用线性探测(开放寻址法)解决hash冲突。

怎么get hash冲突的值:

e不为null,但e.get()的值为entry中原有的值,和要插入的值key不相同,进入getEntryAfterMIss()方法。

 循环找到后返回。

key被GC回收,但我需要set新的值

会进入set()方法中的replaceStaleEntry()方法。

//key为要插入的对象 value为用户设置的初始值 i为要插入的下标值
private void replaceStaleEntry(ThreadLocal<?> key, Object
value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//slotToExpunge为下表值
int slotToExpunge = staleSlot;
//向前找 找到第一个key为null的情况,为什么不找entry为null的值,因为
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//找到key为null的情况,slotToExpunge为第一个key为null的下标值
if (e.get() == null)
slotToExpunge= i;

//向后找,找到null为止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//获取循环到的k
ThreadLocal<?> k = e.get();

//如果k==我传进来的对象
if (k == key) {
//把value覆盖
e.value = value;
//把对象要添加的下标值的内容给到上面覆盖了的
tab[i] = tab[staleSlot];
//把搜索到key为k的内容给到插入的。上述两个操作就是进行了数据的调换。
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists

if (slotToExpunge == staleSlot)
//假如前面没有找到任何被GC回收的key的entry 那么slotToExpunge赋值
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan,
the
// first stale entry seen while scanning for key
is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run,
expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge),
len);
}
expungeStaleEntry
//搜索到key为null的entry的下标
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
//把搜索到的key为null的entry的value对象的强引用关闭 (方便value回收 防止内存泄漏)
tab[staleSlot].value = null;
tab[staleSlot] = null; //把entry赋值为null
size--; //数量减1 数组里面用到的entry
// Rehash until we encounter null
Entry e;
int i;
//从清除内容的entry的下标开始找,向后找,找到第一个null的为止
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
//都会去获取k
ThreadLocal<?> k = e.get();
//如果k为null 
if (k == null) {
//去清理
e.value = null;
tab[i] = null;
size--;
} else {
//rehash
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
scan until
have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

面试题:

什么时候会对entry数组清除?

发生hash冲突,冲突位置key被GC为null时,会向前或向后寻找第一个entry为null为止并把之间的所有key为null的entry清除,然后rehash进行整理。

key和value分别是强、弱引用的原因

-> key为强引用的意义

-> value为弱引用的意义

ThreadLocal内存泄露问题是什么?它是怎么去解决的?

-> threadlocal的数据结构:entry内key,value这个结构的 如何被回收的?

-> entry又是如何被回收的?

-> entry被系统自动回收需要在特定的条件下,那么如何保证在一般情况下进行回收?

ThreadLocal如何解决hash冲突?

-> ThreadLocal的数据结构

-> thread对象放入entry数组时如何确定下标值(黄金函数)来减少hash冲突

-> 发生冲突时,循环找null,把对象放入找到的位置,查询时也会如此,线性探测。

2.Lock

J.U.C(java.util.concurrent),其实质是接口,是在java层面,提供了一系列的方法。

ReentrantLock(重入锁)

ReentrantReadWriteLock(重入读写锁)[读写、写写互斥,读读不互斥]

数据结构:

原理实现,源码:

公平、非公平锁:默认为非公平锁

lock()方法

线程1:

final void lock() {
//cas 比较并替换,修改AbstractQueuedSynchronizer类的state字段,用volatile修饰
//并且jvm底层cas会有锁操作,只能有1个线程能更改成功
//如果从0改成1 说明能抢占到锁,就是修改标志,表明线程已经抢占了
if (compareAndSetState(0, 1)) 
//修改AbstractOwnableSynchronizer的exclusiveOwnerThread为当前线程

setExclusiveOwnerThread(Thread.currentThread());
else
//线程抢占不到的情况执行
acquire(1);
}

 然后执行业务逻辑代码

线程2:

无法修改state状态,进入acquire(1)方法。

默认进入 tryAcquire(arg)方法的NonfairSync inReentrantLock()方法。

final boolean nonfairTryAcquire(int acquires) {

//获取当前线程,现在线程2来抢占锁,并且线程1没有执行完,所以当前是线程2
final Thread current = Thread.currentThread();
//获取AbstractQueuedSynchronizer的值,因为线程1设为了1,所以c为1
int c = getState();
//再次尝试拿锁,如果满足,尝试着去抢占锁(把state改成1)
if (c == 0) {
if (compareAndSetState(0, acquires)) {
//设置当前执行代码块的线程为线程2
setExclusiveOwnerThread(current);
//返回ture,代表线程2拿到了锁,去正常执行业务代码
return true;
}
}
//重入操作,能够加锁多次。如果c!=0.说明有线程在占有锁,那么如果占有的线程跟当前的线程一致,说明是同一个线程抢占多次锁
else if (current == getExclusiveOwnerThread()) {
//将state 改成state +1 这个时候,state不止是一个状态,而是代表加锁了多少次(重入次数)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果抢占不到锁,并且当前线程不是占有锁线程及不是重入,返回false
return false;
}

进入acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法。

1.addWaiter(Node.EXCLUSIVE)方法,添加到等待队列。

 1.1 enq(node)方法 cas自旋,node结点有head、tail和t指针,形成双向链表。

 线程2node返回进入acquireQueued方法

2.acquiredQueued()方法,根据node节点去操作相关获取锁以及park操作
shouldParkAfterFailedAcquire方法

//node为当前线程的node节点
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取node节点的前一个节点
final Node p = node.predecessor();
//如果p==head 如果是thread2,那么满足,但是thread2tryAcquire失败,因为thread1占有锁;所以不满足
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//进入shouldParkAfterFailedAcquire与
parkAndCheckInterrupt
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

shouldParkAfterFailedAcquire方法

//pred为当前线程的前节点 node为当前线程节点
private static boolean shouldParkAfterFailedAcquire(Node
pred, Node node) {
int ws = pred.waitStatus; 
//得到节点的状态,默认为0 第二次进入为-1
if (ws == Node.SIGNAL) 
//waitStatus!=-1不满足 第二次满足条件,返回true

return true;
if (ws > 0) { //不满足

//该逻辑是当我前一个节点的线程状态取消时,将前后链表的关系取消
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { //进入else

//修改当前节点的前一个节点的状态为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false,进入外面的自旋,
return false;
}

parkAndCheckInterrupt方法

private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //如果没有拿到锁,线程park waiting状态 唤醒2个场景(释放锁唤醒第一个线程 interrupt优雅中
断)
return Thread.interrupted(); //获取中断状态并且复位
}

线程3执行,进入acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法。

线程3与线程2成双向链表。

unlock()方法

 

//node 为head节点
private void unparkSuccessor(Node node) {

//得到head的节点状态
int ws = node.waitStatus;
if (ws < 0) //现在的状态为-1 满足条件
//将头节点改成0
compareAndSetWaitStatus(node, ws, 0); 
//得到head节点的下一个节点,我们的场景为thread2的node
Node s = node.next;
//thread2node的状态为-1,正常状态
if (s == null || s.waitStatus > 0) {
s = null;
//如果需要释放锁的那个线程是取消状态或者是取消的或者为null,从后往前找到有效的那一个node。
for (Node t = tail; t != null && t != node; t =
t.prev)
if (t.waitStatus <= 0)
s = t;
}
//满足条件
if (s != null)
//唤醒thread2线程
LockSupport.unpark(s.thread);
}

 线程1执行完毕。

 面试题:

公平锁和非公平锁的区别

默认为非公平锁

修改state状态后,unpark线程时,中间有时间间隔,有可能其他新线程会进入抢占线程。

非公平锁是唤醒的那个线程自旋,拿锁却发现被抢占了。(thread2先抢占锁,却被thread4先拿到锁了)

公平锁是拿锁线程必须排队(必须是head的下一个节点)

代码之间的差别在于多了一个判断 [!hasQueuedPredecessors()]

 tail在前head在后是为了防止tail不为空,但head为空

一行代码所表达的含义:抢占锁判断是否有人等待:如果没人,拿到锁;如果有人,判断当前线程是否为head的下一个节点,如果是也能拿到否则就拿不到。

 什么是AQS?(AbstractQueuedSynchronizer)

JUC下的一个工具类,并发包下面的ReentrantLock conutDownLatch condition等等

1.作用

里面提供了许多模版方法,如加锁、解锁等。

2.数据结构

跟锁相关,维护了一个互斥条件(state)

需要线程排队,维护了一个双向队列,用来做相关线程的等待并且提供了相关队列的操作。

Lock与Synchronized的区别

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值