JUC
1. JMM
什么是JMM
JMM(java内存模型Java Memory Model)本身是一种抽象的概念,描述的是一组规则或规范。通过这组规范定义了程序中各个变量的访问方式。
由于JVM运行程序的实体是线程,而每个线程创建JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而Java内存模型规定中的变量都存储在主内存。主内存是共享数据,所有线程都能访问,但线程对变量的操作(读写值)都必须在工作内存中完成。简单说,就是先读取,再操作,再写回。工作内存存放的是主内存中的副本,线程的通信都需要通过主内存来完成。
内存可见性问题:工作内存间互相不知道对方已经把数据进行了修改。
如何解决?
多线程什么情况下会触发安全问题?
- 当多个线程在操作同一个共享数据时,就有可能发生线程安全问题。
2. volatile
题目:谈谈你对volatile的理解
是什么?
轻量级的同步方案
-
可以解决内存可见性问题
-
volatile关键不能解决原子性问题 (CAS来解决)
原子性问题:要么都成功,要么都失败
-
volatile禁止了重排序
什么是原子性操作
示例代码
public class T1 {
int count=0;
public void add(){
count++;
}
public static void main(String[] args) {
new T1().add();
}
}
将其class字节码文件用javap -c T1.class
编译得到可读格式,摘取add()方法部分
public void add();
Code:
0: aload_0 //把对象的引用装载到操作数栈中
1: dup //
2: getfield #2 // Field count:I //获取指定类的实例域,并将其值压入栈顶(读)
5: iconst_1 //int型常量值1进栈
6: iadd // 栈顶两int型数值相加,并且结果进栈 (改)
7: putfield #2 // Field count:I //为指定的类的实例域赋值(写)
10: return //当前方法返回void
其原子性操作包括三步,读、改、写。
什么是指令重排
是什么?有多种,聚焦编译器的
编译器在不改变运行结果的情况下,按照自己的喜好对指令进行重排,重排的效果是提高代码执行效率。
main(){
// ...
int i=10;
int j=20;
int flag=true;
sop(i,j,flag);
//...
}
//多线程模型重排序
class JmmExample{
private boolean flag=false;
int i=5;
int j=5;
//...
read(){
i=10; //1
j=20; //2
flag=true; //3
}
// ...
write(){
while(flag){
int temp = i*j;
}
}
}
代码解读:正常情况,read()方法,先执行1,再执行2,再执行3,write()方法temp为10*20=200
;多线程情况,如果发生指令重排,read()方法先执行3,write()方法flag=true,temp=5*5=25
java针对这种情况,制定了一套规则,happen-before原则,保证多线程操作可见性,在这套规则下,默认不能指令重排。
这套规则,共有8个条件,常见的synchronized、volatile、lock操作顺序都属于happen-before
VolatileDemo
public class VolatileDemo {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(resource).start();
while(true){
if(resource.isFlag()){
System.out.println("OK...main is over");
break;
}
}
}
}
class Resource implements Runnable{
private boolean flag = false;// 在此处加volatile关键词解决该问题
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
Thread.sleep(1000);
setFlag(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
按理说,程序进入while循环会判断flag是否为true,线程先睡1s,然后true,打印,跳出循环。
但是,程序进入了死循环,没有打印,也没有退出
解决方案:
加volatile,private volatile boolean flag=false;
线程实现的三种方式
- 继承thread
- 实现Runnable
- 实现Callable
实现Callable的好处?
- 可以抛出异常
- 任务执行后可返回值
- 可以拿到一个Future对象,表示异步计算的结果。通过Future了解任务执行的情况。
3. CAS
volatile部分说cas可以解决原子性问题
什么是CAS?
compare and swap,比较并替换,如果这个值是它的期望值,就进行修改
CAS的底层原理是什么?
- CAS依赖于unsafe类,unsafe相当于一个后门,可以像操作C的指针一样直接操作内存,直接获得内存中的偏移量,并且用volatile保证获取到的值是最新值。确保执行效率非常高。
- 底层是做一个do while循环,源码部分getAndAddInt解读,
public final boolean compareAndSet(int expect, int update) {
// this 自己,valueOffset内存偏移量
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
内存偏移量如何得到的?AtomicInteger类的静态代码块中,unsafe类有个objectFieldOffset方法得到,里面有个值value是volatile修饰的,修改值的时候,一是能拿到内存中的最新值,二是拿到内存中的修改地址,就可以直接修改数据。
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
先来段代码解释原子性问题带来的线程安全
public class CasDemo {
public static void main(String[] args) {
Resources2 mr2 = new Resources2();
for (int i = 0; i < 4; i++) {
new Thread(mr2).start();
}
}
}
class Resources2 implements Runnable{
int count=0;
public void run() {
while(true){
try {
Thread.sleep(100);
if(count>=100){
break;
}
System.out.println(Thread.currentThread().getName()+"...."+ ++count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如果给count加上volatile关键字也不行,因为++count分为三步走,volatile只能解决内存不可见。
如何解决呢?JUC包里面有个atomic系列,用的就是CAS
public class CasDemo {
public static void main(String[] args) {
Resources2 mr2 = new Resources2();
for (int i = 0; i < 10; i++) {
new Thread(mr2).start();
}
}
}
class Resources2 implements Runnable{
// int count=0;
AtomicInteger atomic = new AtomicInteger(0);
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"...."+ atomic.getAndIncrement());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这样就不会发生线程安全问题
getAndIncrement()方法,底层是
public final int getAndIncrement() {
// this 自己 valueOffset 内存偏移量 1 要修改的值 count++就是+1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
getAndAddInt()方法,底层
// var1:this var2:内存偏移量 var4:修改值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 获得主内存中的数据-->在这个偏移量上对应的这个对象的volatile修饰的值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
// 模拟一个成功案例 var this var2 0x666 var4 1
return var5;
}
解读:var1是this,var2是内存偏移量 var4是修改值1 。首先获得var2内存偏移量上的var1对象所对应的主内存中的值var5,是5;拿到之后,在操作之前再用var1和var2抓取主内存的地址,两次值相同,没有被改过,安全,假设抓取的还是5,相等,因为CAS是一种系统原语级别的支持,判断相等后一定会执行将主内存的值加一的操作,将修改后的值写进去,5+1得到6,修改成功,为true,取反false,跳出循环;如果比较失败,false,取反true,继续进入循环,直到成功
注意:CAS是一种系统原语(硬件支持),整个过程是不允许被打断。
CAS的缺点
有可能自旋时间过长,对CPU造成极大的负担。
ABA问题
什么是ABA问题?
其他线程修改了值,让另一个线程觉得没有被修改,从而完成了cas操作,这就是ABA问题。
ABA问题出现在多线程或多进程计算环境中。
首先描述ABA。假设两个线程T1和T2访问同一个变量V,当T1访问变量V时,读取到V的值为A;此时线程T1被抢占了,T2开始执行,T2先将变量V的值从A变成了B,然后又将变量V从B变回了A;此时T1又抢占了主动权,继续执行,它发现变量V的值还是A,以为没有发生变化,所以就继续执行了。这个过程中,变量V从A变为B,再由B变为A就被形象地称为ABA问题了。
问题的主要还是因为“值是一样的”等同于“没有发生变化”的认知。
ABA问题的隐患
形象描述:你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。
如何解决
使用JDK的并发包中的AtomicStampedReference和 AtomicMarkableReference来解决。通过版本号(时间戳)来解决ABA问题,时间戳信息要做成自增的。
// 用int做时间戳
AtomicStampedReference<QNode> tail = new AtomicStampedReference<CompositeLock.QNode>(null, 0);
int[] currentStamp = new int[1];
// currentStamp中返回了时间戳信息
QNode tailNode = tail.get(currentStamp);
tail.compareAndSet(tailNode, null, currentStamp[0], currentStamp[0] + 1)
栈
接下来看atomic底层,先上案例
public class CasDemo02 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019));//true
System.out.println(atomicInteger.get());//2019
System.out.println(atomicInteger.compareAndSet(5,4000));//false
System.out.println(atomicInteger.get());//2019
}
}
Unsafe类
是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地native方法来访问。
unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sun.misc包,其内部方法可以像操作C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。
Unsafe类中的方法都是native修饰的,也就是说UNsafe类中的方法都直接调用了操作系统底层资源执行任务。
总结:使用了unsafe类,此类是通过内存的偏移地址,直接去操作这个变量,并且此变量又通过volatile来修饰,可获得内存中的最新值并直接修改。并且,CAS是一种系统原语(硬件支持),整个过程是不允许被打断。
栈
有两种,方法栈和本地方法栈。本地方法栈支持对本地方法的调用,执行的非java代码。unsafe执行是在本地方法栈中执行。
4. 乐观锁
乐观锁:制定了一个版本号,每次操作这个数据,对应版本号+1,提交数据时,要比安全的版本号大一,否则提交失败。
悲观锁:
- 读锁/共享锁:只要是读的线程都能获得这把锁–> 读时不会触发安全问题
lock in share mode
- 写锁/排他锁:一个人持有锁,其他人都不能拿到锁。
for update
lock和synchronized区别
-
原始组成:synchronized属于关键字,jvm层面;lock属于api级别的锁
-
使用方法:synchronized不需要手动释放;lock需要finally释放,可能导致死锁
-
synchronized不可中断;lock可中断,更灵活
- 方案1:
trylock(long timeout,TimeUnit unit)
- 方案2:lockInterpatibly 可中断
- 方案1:
-
synchronized是非公平锁;lock可以是公平锁,也可以是非公平锁,根据传入的参数决定
-
精确唤醒:synchronized无此方法;lock通过condition可以
public ReentrantLock() {
sync = new NonfairSync();//默认非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}// 公平锁
公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到
在并发情况中,会先查看锁维护的等待队列,如果为空,当前线程是等待队列中第一个就占有锁,否则就加入等待队列,等待线程召唤。
非公平锁:指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程优先获得锁,在高并发的情况下,有可能出现优先级反转或者饥饿现象。
可重入锁
可重入锁又称为递归锁
指的是:同一线程外层函数获得锁之后,内层递归函数仍能获得该锁的代码
在同一线程在外层方法获得锁的时候,在进入内层方法会自动获得锁。
也就是说,线程可以进入任何一个它已经拥有锁的同步代码块。
自旋锁:cas就是一个自旋锁。
手写一个自旋锁
public class Demo02 {
public static void main(String[] args) {
MyResource mr = new MyResource();
new Thread(mr,"线程A").start();
new Thread(mr,"线程B").start();
}
}
class MyResource implements Runnable{
int count=0;
MySpinLock lock = new MySpinLock();
public void run() {
while(true){
lock.lock();
try {
Thread.sleep(100);
if(count>=100){
break;
}
System.out.println(Thread.currentThread().getName()+"..."+ ++count);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//lock需要finally释放
}
}
}
}
class MySpinLock{
AtomicReference<Thread> reference = new AtomicReference<Thread>();
public void lock(){
Thread thread = Thread.currentThread();
while(!reference.compareAndSet(null,thread)){
}
}
public void unlock(){
Thread thread = Thread.currentThread();
reference.compareAndSet(thread,null);
}
}
AtomicReference是针对对象的atomic。
A:a首先进入到lock方法,此时reference为null,把thread赋值给了reference,while循环跳出;
B:b再次进入lock方法,此时reference为threadA,修改失败,while为true,B线程一直在里面循环。
精确唤醒
题目:多线程之间调用A->B->C A打印5次,B打印10次,C打印15次,来10轮
public class LockNotifyDemo {
public static void main(String[] args) {
// 多线程之间调用A->B->C A打印5次,B打印10次,C打印15次,来10轮
MyResource01 mr = new MyResource01();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
mr.print5();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
}
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
mr.print10();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
mr.print15();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
}
}
}
class MyResource01{
Lock lock = new ReentrantLock();
int count=1;
Condition con1 = lock.newCondition();
Condition con2 = lock.newCondition();
Condition con3 = lock.newCondition();
public void print5() throws InterruptedException {
try {
lock.lock();
while(count!=1){
con1.await();
}
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+"..."+i);
}
count=2;
con2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() throws InterruptedException {
try {
lock.lock();
while(count!=2){
con2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"..."+i);
}
count=3;
con3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() throws InterruptedException {
try {
lock.lock();
while(count!=3){
con3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName()+"..."+i);
}
count=1;
con1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
object类有什么方法
toString notify notifyAll finalize equals hashCode wait
覆盖了hashMap的equals hashCode;多线程的notify notifyAll wait;垃圾回收机制的finalize;基本的toString。
5. 安全集合类
题目:我们都知道ArrayList是线程不安全的,请编写一个不安全的案例,并给出解决方案
其实写个多线程就好了,添加一些垃圾对象,哪些容易产生垃圾对象?字符串的拼接
字符串的拼接
代码
String str = "ab";
String str2 = "abc";
String s3 = str + "c";
System.out.println(str2 == s3);
解读:二者不相等,为什么?
字符串串联("+")是通过 StringBuilder
(或 StringBuffer
)类及其 append
方法实现的。字符串转换是通过 toString
方法实现的
如图,str和str2都在常量池中,str+“c”,添加是通过StringBuilder类的append,底层1.8是char数组,1.9底层为byte数组,得到"abc",字符串转换是通过toString实现,如下,底层为new String,因此,str2是在常量池,而s3是在堆中。
public String toString(){
return new String(this.value,o,this.count);
}
arrayList不安全的案例
public class ArrayListDemo {
public static void main(String[] args) {
final ArrayList<String> arrayList = new ArrayList<String>();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
arrayList.add((UUID.randomUUID()+"").substring(0,8));
System.out.println(arrayList);
}
}).start();
}
}
}
报错,并发修改异常。
解决方法
- synchronized上锁
- 用vector替代ArrayList
- 用
Collections.synchronizedList(arrayList)
,将其转变为线程安全的集合 - 用
CopyOnWriteArrayList
public class ArrayListDemo {
public static void main(String[] args) {
final List<String> arrayList = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
arrayList.add((UUID.randomUUID()+"").substring(0,8));
System.out.println(arrayList);
}
}).start();
}
}
}
结果为:
[bd7e96e3, 6674da69, 6fa727e0]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b]
[bd7e96e3, 6674da69, 6fa727e0]
[bd7e96e3, 6674da69, 6fa727e0, 99396440]
[bd7e96e3, 6674da69, 6fa727e0]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b, 6411c994, ab8117ec, 48c7d831]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b, 6411c994, ab8117ec]
[bd7e96e3, 6674da69, 6fa727e0, 99396440, 18919252, fe123edd, a93c684b, 6411c994]
原因:只有集合是线程安全的,而线程本身不是线程安全的。
6. ConcurrentHashMap
1.7 底层用的是分段锁segment,1.8利用CAS+synchronized来保证并发安全,底层用数组+链表+红黑树的存储结构。
首先看put方法,最有可能发生线程安全的就是put,看如何解决
public V put(K key, V value) {
return putVal(key, value, false);
}
同HashMap一样,也是putVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key和value不能为null,和HashMap的区别
if (key == null || value == null) throw new NullPointerException();
// hash值的计算
int hash = spread(key.hashCode());
int binCount = 0;
// table就是链表+数组
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 初始化,易发生线程安全问题
tab = initTable();
// (n-1)&hash计算hash值落在数组的索引位置
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
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;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
spread计算key的hash值,HASH_BITS为0x7ffffff
,32bit的最大值,保证hash>=0
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
初始化如何保证线程安全?
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(); // lost initialization race; just spin
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 = sc;
}
break;
}
}
return tab;
}
解读:多个线程都满足(tab = table) == null || tab.length == 0条件,进入循环。sc初始化为0,而sizeCtl private transient volatile int sizeCtl;
初始化也是0,volatile也确保sizeCtl一定是内存中最新值。第一个进来的线程会进入else if逻辑。U 就是Unsafe类,CAS的核心类,compareAndSwapInt(this, SIZECTL, sc, -1)
,this就是table本身,SIZECTL
和sc比较,如果相同,修改sc-1,得到sc为-1。没有进来的线程会继续循环,走了if部分的逻辑。Thread.yield()
放手,释放CPU的执行权,不再初始化。继续看else if
部分的代码,判断未初始化后,设置默认容量为16。new底层的node数组,也就是table。紧接着sc = n - (n >>> 2)
sc就是16-4
得到12,最后再赋值给sizeCtl
,初始化完毕。
阈值的计算:在HashMap中是容量*装载因子
,在ConcurrentHashMap直接用n减去n右移两位算出,更快。
如何知道SIZECTL
是内存偏移量呢?底层private static final long SIZECTL;
,静态代码块中SIZECTL = U.objectFieldOffset(xxx);
,拿到的就是内存偏移量!
初始化完毕后put方法走else if ((f = tabAt(tab, i = (n - 1) & hash)) == null)
逻辑,tabAt是做什么呢?
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
tabAt调用的是unsafe类的volatile,由于多线程,必须确保内存中的最新值。假设i=3
,发现3号位置最新值为null,进行放值。放值的时候可能也会发生线程安全,又用casTabAt看第一次为null,放值的时候再检查i
还是null,将该值放到i
位置。
如果table的索引i
位置有值,走else逻辑,用synchronized,锁对象是f,就是索引i
对应的区域,锁细化,只锁住这块区域,确保线程安全。然后对这个索引位置的链表进行遍历,和HashMap一样,如果遍历走到最后,尾插。
扩容:多线程一起扩容。
- NCPU java类和CPU打交道的类–>获取CPU的核数–>以及性能如何
6。new底层的node数组,也就是table。紧接着sc = n - (n >>> 2)
sc就是16-4
得到12,最后再赋值给sizeCtl
,初始化完毕。
阈值的计算:在HashMap中是容量*装载因子
,在ConcurrentHashMap直接用n减去n右移两位算出,更快。
如何知道SIZECTL
是内存偏移量呢?底层private static final long SIZECTL;
,静态代码块中SIZECTL = U.objectFieldOffset(xxx);
,拿到的就是内存偏移量!
初始化完毕后put方法走else if ((f = tabAt(tab, i = (n - 1) & hash)) == null)
逻辑,tabAt是做什么呢?
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
tabAt调用的是unsafe类的volatile,由于多线程,必须确保内存中的最新值。假设i=3
,发现3号位置最新值为null,进行放值。放值的时候可能也会发生线程安全,又用casTabAt看第一次为null,放值的时候再检查i
还是null,将该值放到i
位置。
如果table的索引i
位置有值,走else逻辑,用synchronized,锁对象是f,就是索引i
对应的区域,锁细化,只锁住这块区域,确保线程安全。然后对这个索引位置的链表进行遍历,和HashMap一样,如果遍历走到最后,尾插。
扩容:多线程一起扩容。
- NCPU java类和CPU打交道的类–>获取CPU的核数–>以及性能如何
扩容resizing会先初始化transfer,里面就有NCPU,如何划分每个CPU扩容区域?用stride步伐。默认最小步伐为16。