目录
4.2.5:AtomicStampReference解决ABA问题
1:Java 运行过程大体介绍
其中运行时数据区主要由五部分组成:虚拟机栈,堆,方法区(永久堆),程序计数器(帮助执行虚拟机栈中的方法),本地方法栈(最底层方法);
2:运行时数据区详解
- 程序计数器:每个线程对应有一个程序计数器,各线程的程序计数器是私有的,互不影响,且线程安全的。程序计数器记录线程正在执行的内存地址,以便被中断线程恢复执行时再次继续执行;
- 虚拟机栈:每个线程会对应一个Java栈,每个Java栈由若干栈帧组成,每个方法对应一个栈帧,栈帧在方法运行时入栈,完成时弹出栈帧作为返回值,该栈帧被清除。栈顶的方法叫活动栈,表示当前执行的方法才可被cpu执行。线程请求的栈深度大于虚拟机所允许的栈深度,将抛出StackOverFlowError异常。栈扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常。栈中存的是引用,具体变量在堆中。
- 方法区:是Java堆的永久区,存放了要加载的类的信息(名称,修饰符等),类中的静态变量,定义的final常量,field信息,类中的方法等。方法区是Java线程共享的,方法区内存溢出时,会抛出OutOfMemoryError:PremGen space的错误信息。
- 常量池:是方法区的一部分,存储两类数据,字面量和引用量。字面量是:字符串,final类型等。引用量是:类/接口、方法字段的名称和描述符。常量池在编译期间就已经确定,并保存在了.class文件中;
- 本地方法栈:为jvm执行native方法服务,也会抛出StackOverFlowError和OutOfMemoryError异常;
Java内存模型工作示意图:
3:多线程三大特性
- 原子性: 一个或者多个操作,要么全部执行(不会被打断),要么都不执行
- 可见性:当多个线程访问一个变量时,一个线程修改了这个值,其他线程要立即能看到这个修改结果;(避免脏读)
- 有序性:即程序执行顺序按代码的先后顺序执行;
4:多线程控制类
4.1.1:ThreadLocal作用
ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本,当某些数据是以线程为作用域并且不同线程有不同副本的时候,就可以考虑ThreadLocal。比如数据库链接Connection,每个请求处理线程都需要,但又互不影响,就是要ThreadLocal实现。
4.1.2:ThreadLocal常用方法
- initialValue():副本创建方法
- get():获取副本方法
- set():设置副本方法
代码例子:线程2new一个person命名为zhangsan,线程1调用get方法打印null值。原因是ThreadLocal为每个线程单独创建区域,每个线程只能控制自己的变量信息。
import java.util.concurrent.TimeUnit;
/**
* ThreadLocal线程局部变量
*
* ThreadLocal是使用空间换时间,synchronized是使用时间换空间
* 比如在hibernate中的session就存在ThreadLocal中,避免synchronized的使用
*/
public class ThreadLocal2 {
//volatile static Person p =new Person();
static ThreadLocal<Person> t1=new ThreadLocal<>();
public static void main(String[] args) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t1.get());
}).start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.set(new Person());
}).start();
}
static class Person{
String name="zhangsan";
}
}
//打印结果为null
4.1.3:ThreadLocal底层实现
4.2.1:原子类分类
注:如果不适用原子类,比如在实现i++的时候没有上锁,i++可能分部执行,导致数据异常操作;
4.2.2:原子类解决非原子性操作问题
AtomicInteger类可以保证i++的原子性
- getAndIncrement()对应n++;
- incrementAndGet()对应++n;
- decrementAndGet()对应--n;
- getAndDecrement()对应n--;
4.2.3:原子类CAS原理分析
图示如下,相当于Mysql中的乐观锁(循环比较,直至和期望值相同返回True)
4.2.4:CAS的ABA问题
问题描述:线程1将A修改为B,又将B修改为A。其他线程判断此值没有修改过,在特定问题下会出错;
下图compareAndSet(A,B)方法为判断栈顶值,如果为A,则替换为B
t1初始为AB,转到t2,t2已经将整个栈修改了,但栈顶元素还是A,t1执行完方法后,栈内只剩一个B,和预期的BB不符。
解决方法:加时间戳,每次进行对比的时候同时对比时间戳,和自己上次时间戳相同时再执行操作。
4.2.5:AtomicStampReference解决ABA问题
实现每次操作时候对比时间戳,每次操作完成之后,时间戳也改变。循环对比直至成功。
4.3.1:Lock锁的关系实现图
- 可重入锁:在需要锁的时候调用方法,依然可以拿到锁
4.3.2:读写锁:可以同时读,不能同时写,写的时候不能读
实例代码如下:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写操作类
*/
public class ReadWriteLockDemo {
private Map<String, Object> map = new HashMap<String, Object>();
//创建一个读写锁实例
private ReadWriteLock rw = new ReentrantReadWriteLock();
//创建一个读锁
private Lock r = rw.readLock();
//创建一个写锁
private Lock w = rw.writeLock();
/**
* 读操作
*
* @param key
* @return
*/
public Object get(String key) {
r.lock();
System.out.println(Thread.currentThread().getName() + "读操作开始执行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
return map.get(key);
} finally {
r.unlock();
System.out.println(Thread.currentThread().getName() + "读操作执行完成......");
}
}
/**
* 写操作
*
* @param key
* @param value
*/
public void put(String key, Object value) {
try {
w.lock();
System.out.println(Thread.currentThread().getName() + "写操作开始执行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
} finally {
w.unlock();
System.out.println(Thread.currentThread().getName() + "写操作执行完成......");
}
}
public static void main(String[] args) {
final ReadWriteLockDemo d = new ReadWriteLockDemo();
d.put("key1", "value1");
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
}
}
执行结果如下:
4.4:volatile关键字
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)
- 禁止进行指令重排序。(保证变量所在行的有序性)
例如int a;a = 1;a = 2,a=3;在不用volatile修饰时,编译器会直接执行第三行,修饰之后,编译器会按步执行;
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
应用场景如下(其一,在其他多线程共享变量中,尽可能都加volatile):
//双重校验
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
5:容器
5.1:容器类关系图如下:
具体容器类详情参考:https://blog.csdn.net/qq_38869493/article/details/105015262
5.2:HashMap实现分析
HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
HashMap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易。
HashMap结构示意图如下:
注:jdk1.8之后将hashmap的链表进行了优化,当>8时,转为红黑树
5.3:hashmap底层实现
put方法:
// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
//在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用
//addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,
//那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失
remove方法:
final Entry<K,V> removeEntryForKey(Object key) {
// 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
// 删除链表中“键为key”的元素
// 本质是“删除单向链表中的节点”
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
//当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,
//然后各自去进行计算操作,之后再把结果写会到该数组位置去,
//其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。
数组大小大于初始值时扩容操作:
//resize操作,代码如下:
// 重新调整HashMap的大小,newCapacity是调整后的容量
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果就容量已经达到了最大值,则不能再扩容,直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
// 然后,将“新HashMap”赋值给“旧HashMap”。
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
// 这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,
//之后指向新生成的数组。当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,
//各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,
//其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,
//就会用已经被赋值的table作为原始数组,这样也会有问题。
5.4:hashmap并发问题解决方案
- Synchronized关键字
- Lock锁
- 同步类容器
- 并发类容器
6:同步容器
在Java中,同步容器主要包括2类:
1)Vector、Stack、HashTable(可以独立创建)
2)Collections类中提供的静态工厂方法创建的类(借助工具类创建)
- Vector:实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。
- Stack:也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类。
- HashTable:实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。HashTable的remove,put,get等public方法做成了同步方法,保证了HashTable的线程安全性。
- Collections:Collections类是一个工具提供类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类,如下图所示:
7:并发容器
7.1:并发容器简介
同步容器将几乎所有方法添加的synchronized进行同步,这样保证了线程的安全性,但代价就是严重降低了并发性能,当多个线程竞争容器时,吞吐量严重降低。Java5.0开始针对多线程并发访问重新设计,提供了并发性能较好的并发容器,引入了java.util.concurrent包。有如下:
- ConcurrentHashMap
- 对应的非并发容器:HashMap
- 目标:代替Hashtable、synchronizedMap,支持复合操作
- 原理:JDK6中采用一种更加细粒度的加锁机制Segment“分段锁”,JDK8中采用CAS无锁算法。
- CopyOnWriteArrayList
- 对应的非并发容器:ArrayList
- 目标:代替Vector、synchronizedList
- 原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了。
- CopyOnWriteArraySet
- 对应的费并发容器:HashSet
- 目标:代替synchronizedSet
- 原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
- ConcurrentSkipListMap
- 对应的非并发容器:TreeMap
- 目标:代替synchronizedSortedMap(TreeMap)
- 原理:Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过”空间来换取时间”的一个算法。ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上能够在O(log(n))时间内完成查找、插入、删除操作。
- ConcurrentSkipListSet
- 对应的非并发容器:TreeSet
- 目标:代替synchronizedSortedSet
- 原理:内部基于ConcurrentSkipListMap实现
- ConcurrentLinkedQueue
- 不会阻塞的队列
- 对应的非并发容器:Queue
- 原理:基于链表实现的FIFO队列(LinkedList的并发版本)
- LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue
- 对应的非并发容器:BlockingQueue
- 特点:拓展了Queue,增加了可阻塞的插入和获取等操作
- 原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒
- 实现类:
- LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
- ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
- PriorityBlockingQueue:按优先级排序的队列
下面以ConcurrentHashMap为例讲解并发包的数据结构和保证安全的方法
7.2:ConcurrentHashMap数据结构简介
1.8之前: 1.8及之后:加入了红黑树
7.3:ConcurrentHashMap同步原理
- 1.7同步原理:主要继承RenntrateLock进行加锁,采用cas模式,及悲观锁的方式进行循环加锁,直至成功;
- 1.8同步原理:对数组元素使用cas模式加锁,对数组后的元素使用synchornized关键字进行加锁
- ConcurrentHashMap初始化:
//初始化方法是 initTable():该方法通过sizeCtl实现CAS初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 初始化的"功劳"被其他线程"抢去"了
if ((sc = sizeCtl) < 0)
Thread.yield(); //放弃执行权
// CAS 一下,将 sizeCtl 设置为 -1,代表抢到了锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// DEFAULT_CAPACITY 默认初始容量是 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 初始化数组,长度为 16 或初始化时提供的长度
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 将这个数组赋值给 table,table 是 volatile 的
table = tab = nt;
// 如果 n 为 16 的话,那么这里 sc = 12
// 其实就是 0.75 * n
sc = n - (n >>> 2);
}
} finally {
// 设置 sizeCtl 为 sc,我们就当是 12 吧
sizeCtl = sc;
}
break;
}
}
return tab;
}
- ConcurrentHashMap扩容
//扩容方法是tryPresize:该方法通过sizeCtl实现CAS初始化
// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
private final void tryPresize(int size) {
// c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// 这个 if 分支和之前说的初始化数组的代码基本上是一样的,在这里,我们可以不用管这块代码
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
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); // 0.75 * n
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法
// 此时 nextTab 不为 null
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 1. 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)
// 调用 transfer 方法,此时 nextTab 参数为 null
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
- ConcurrentHashMap数据迁移
//数据迁移方法是transfer:该方法通过CAS和synchronized关键字实现同步。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
// stride 可以理解为”步长“,有 n 个位置是需要进行迁移的,
// 将这 n 个任务分为多个任务包,每个任务包有 stride 个任务
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果 nextTab 为 null,先进行一次初始化
// 前面我们说了,外围会保证第一个发起迁移的线程调用此方法时,参数 nextTab 为 null
// 之后参与迁移的线程调用此方法时,nextTab 不会为 null
if (nextTab == null) {
try {
// 容量翻倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
// nextTable 是 ConcurrentHashMap 中的属性
nextTable = nextTab;
// transferIndex 也是 ConcurrentHashMap 的属性,用于控制迁移的位置
transferIndex = n;
}
int nextn = nextTab.length;
// ForwardingNode 翻译过来就是正在被迁移的 Node
// 这个构造方法会生成一个Node,key、value 和 next 都为 null,关键是 hash 为 MOVED
// 后面我们会看到,原数组中位置 i 处的节点完成迁移工作后,
// 就会将位置 i 处设置为这个 ForwardingNode,用来告诉其他线程该位置已经处理过了
// 所以它其实相当于是一个标志。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance 指的是做完了一个位置的迁移工作,可以准备做下一个位置的了
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
/*
* 下面这个 for 循环,最难理解的在前面,而要看懂它们,应该先看懂后面的,然后再倒回来看
*
*/
// i 是位置索引,bound 是边界,注意是从后往前
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 下面这个 while 真的是不好理解
// advance 为 true 表示可以进行下一个位置的迁移了
// 简单理解结局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
// 将 transferIndex 值赋给 nextIndex
// 这里 transferIndex 一旦小于等于 0,说明原数组的所有位置都有相应的线程去处理了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 看括号中的代码,nextBound 是这次迁移任务的边界,注意,是从后往前
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 所有的迁移操作已经完成
nextTable = null;
// 将新的 nextTab 赋值给 table 属性,完成迁移
table = nextTab;
// 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75 倍
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
// 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
// 这里使用 CAS 操作对 sizeCtl 进行减 1,代表做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 任务结束,方法退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果位置 i 处是空的,没有任何节点,那么放入刚刚初始化的 ForwardingNode ”空节点“
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置处是一个 ForwardingNode,代表该位置已经迁移过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 对数组该位置处的结点加锁,开始处理数组该位置处的迁移工作
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 头结点的 hash 大于 0,说明是链表的 Node 节点
if (fh >= 0) {
// 下面这一块和 Java7 中的 ConcurrentHashMap 迁移是差不多的,
// 需要将链表一分为二,
// 找到原链表中的 lastRun,然后 lastRun 及其之后的节点是一起进行迁移的
// lastRun 之前的节点需要进行克隆,然后分到两个链表中
int runBit = fh & n;
Node<K,V> lastRun = f;
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;
}
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);
}
// 其中的一个链表放在新数组的位置 i
setTabAt(nextTab, i, ln);
// 另一个链表放在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
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;
}
}
// 如果一分为二后,节点数少于 8,那么将红黑树转换回链表
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;
// 将 ln 放置在新数组的位置 i
setTabAt(nextTab, i, ln);
// 将 hn 放置在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程一旦看到该位置的 hash 值为 MOVED,就不会进行迁移了
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
advance = true;
}
}
}
}
}
}
线程创建与线程通讯:https://blog.csdn.net/qq_38869493/article/details/105032702
线程池:https://blog.csdn.net/qq_38869493/article/details/105054955
视频地址:https://www.bilibili.com/video/BV1m4411u7xK