List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。
本篇博客介绍常见的关于Java中线程安全的ConcurrentHashMap集合的面试问题,结合源码分析题目背后的知识点。
关于List的博客文章如下:
关于的Set的博客文章如下:
关于HaseMap的博客文章如下:
其他关于 数据结构 以及 多线程 的文章如下:
1.从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构;
2.从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的;
3.ConcurrentHashMap 不支持 key 或者 value 为 null ,避免歧义;
4.JDK1.7和1.8的区别:
5.CAS是一种乐观锁机制,也被称为无锁机制
核心:线程安全
从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构
从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的
从线程安全来说,jdk7中,ConcurentHashMap 内部维护的是一个Segment(段)数组,Segment数组中每个元素又是一个HashEntry数组
Segment继承了ReentrantLock这个类,所以segment自然就可以扮演锁的角色,每一个segment相当于一把锁,这就是分段锁
当其他线程在需要进行put操作时,需要先去获取该对象的锁资源,然而当发现锁资源被占用的时候,该线程会先去进行节点的创建避免线程的空闲,这种思想也叫作预创建的思想
因为segment在初始化后是不会扩容的,HashEntry数组是会扩容的,与HashMap机制一样,所以HashEntry是依靠于segment锁来维护安全,所以HashEntry的扩容也是线程安全的
jdk8中,ConcurentHashMap 因为结构变为了 数组+链表+红黑树 结构,所以维护线程安全的机制页相对发生了一些变化,和HashMap同样的,ConcurentHashMap 在put第一个元素时,才会执行初始化
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); //根据KEY计算hash值
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(); //如果数组为null时,表示第一次put,则先进行初始化
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
}
...
此处核心为sizeCtl
sizeCtl 这个值,就保证了多线程并发状态下,数组的初始化安全
核心为compareAndSwapInt,比较主存中数据和当前内存中数据是否相同,如果不同代表有别的线程正在操作这个数据那么就返回false,退回重新争取时间片,此处就是保证并发时线程安全的核心。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//循环判断数组是否为空/null
while ((tab = table) == null || tab.length == 0) {
//此处核心为sizeCtl
/*
sizeCtl的不同值表示不同的含义
sizeCtl = 0 代表数组还未初始化
sizeCtl > 0 如果数组已经初始化,那么表示 扩容阈值
sizeCtl = -1 表示数组正在初始化
sizeCtl < -1 表示数组正在扩容,并且正在被多线程初始化中
*/
if ((sc = sizeCtl) < 0) //此处表示数组正在被某个线程初始化
Thread.yield(); // lost initialization race; just spin //释放CPU时间片
//核心为compareAndSwapInt,比较主存中数据和当前内存中数据是否相同,如果不同代表有别的线程正在操作这个数据那么就返回false,退回重新争取
//时间片
//此处就是保证并发时线程安全的核心
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //得到了数组长度是否为自己设置还是默认16
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; //new出数组
table = tab = nt;//数组赋值
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
CAS + sizeCtl 保证了初始化数据的安全
数组的初始完成后,回到putval方法存入元素
真正存入元素时,是加入了synchronized来加锁保证线程安全
CAS + sizeCtl 和 synchronized 两者共同保证了ConcurentHashMap 的线程安全!
CAS(CompareAnd Swap),就是比较并交换,是解决多线程情况下,解决使用锁造成性能损耗问题的一种机制。
CAS是一种乐观锁机制,也被称为无锁机制。全称: Compare-And-Swap。它是并发编程中的一种原子操作,通常用于多线程环境下实现同步和线程安全。CAS操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。由于CAS是一种无锁机制,因此它避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。
CAS包含三个操作数:
当要对变量进行修改时,先会将内存位置的值与预期的变量原值进行比较,如果一致则将内存位置更新为新值,否则不做操作,无论哪种情况都会返回内存位置当前的值。
package com.tianju.test2;
import java.util.concurrent.atomic.AtomicInteger;
public class CASTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019));
System.out.println(atomicInteger);
System.out.println(atomicInteger.compareAndSet(5,2020));
System.out.println(atomicInteger.compareAndSet(2019,2020));
System.out.println(atomicInteger);
System.out.println(atomicInteger.compareAndSet(2020,5));
System.out.println(atomicInteger);
}
}
优点:
CAS是一种无锁机制,因此它避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。
缺点:
CAS 有自旋锁,如果不成功会一直循环,可能会给 cpu 带来很大开销;
问题就是可能会造成 “ABA”;
解决方案:解决的思路就是引入类似乐观锁的版本号控制,不止比较预期值和内存位置的值,还要比较版本号是否正确。
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(5, 1);
package com.tianju.test2;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019));
System.out.println(atomicInteger);
System.out.println(atomicInteger.compareAndSet(5,2020));
System.out.println(atomicInteger.compareAndSet(2019,2020));
System.out.println(atomicInteger);
System.out.println(atomicInteger.compareAndSet(2020,5));
System.out.println(atomicInteger);
System.out.println(" ########################################## ");
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(5, 1);
boolean flag1 = atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), 2019,
atomicStampedReference.getStamp(), 2);
System.out.println("从5修改为2019:"+flag1);
System.out.println(atomicStampedReference.getReference());
boolean flag2 = atomicStampedReference.compareAndSet(atomicStampedReference.getReference(), 2020,
atomicStampedReference.getStamp(), 3);
System.out.println("从2019修改为2020:"+flag2);
System.out.println(atomicStampedReference.getReference());
boolean flag3 = atomicStampedReference.compareAndSet(2020, 5, 3, 4);
System.out.println("从2020修改回5:"+flag3);
System.out.println(atomicStampedReference.getReference());
}
}
CAS是Java中Unsafe类里面的方法,它的全称是CompareAndSwap,比较并交换的意思。它的主要功能是能够保证在多线程环境下,对于共享变量的修改的原子性。
如下,成员变量state,默认值是0,定义了一个方法doSomething(),这个方法的逻辑是判断state是否为0,如果为0就修改成1。这个逻辑看起来没有任何问题,但是在多线程环境下,会存在原子性的问题,因为这里是一个典型的,Read-Write的操作。
一般情况下,我们会在doSomething()这个方法上加同步锁来解决原子性问题。
package com.tianju.test2;
public class Demo1 {
private int state = 0;
public void doSomething(){
if (state==0){
state=1;
}
}
}
但是,加同步锁,会带来性能上的损耗,所以,对于这类场景,我们就可以使用CAS机制来进行优化
package com.tianju.test2;
import sun.misc.Unsafe;
public class Demo2 {
private volatile int state = 0;
private static final Unsafe UNSAFE = Unsafe.getUnsafe();
private static final long stateOffset;
static {
try {
stateOffset = UNSAFE.objectFieldOffset(
Demo2.class.getDeclaredField("state")
);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public void doSomething(){
if (UNSAFE.compareAndSwapInt(this,stateOffset,0,1)){
state=1;
}
}
}
在doSomething()方法中,我们调用了unsafe类中的compareAndSwapInt()方法来达到同样的目的,这个方法有四个参数,分别是:
当前对象实例、成员变量state在内存地址中的偏移量、预期值0、期望更改之后的值1。
CAS机制会比较state内存地址偏移量对应的值和传入的预期值0是否相等,如果相等,就直接修改内存地址中state的值为1。否则,返回false,表示修改失败,而这个过程是原子的,不会存在线程安全问题。
CompareAndSwap是一个native方法,实际上它最终还是会面临同样的问题,就是先从内存地址中读取state的值,然后去比较,最后再修改。
这个过程不管是在什么层面上实现,都会存在原子性问题。所以,CompareAndSwap的底层实现中,在多核CPU环境下,会增加一个Lock指令对缓存或者总线加锁,从而保证比较并替换这两个指令的原子性。
CAS主要用在并发场景中,比较典型的使用场景有两个:
1.从结构上来说jdk7中,ConcurentHashMap是采用Segment段数组 + Entry数组 + 单链表结构;
2.从结构上来说jdk8中,ConcurentHashMap 与 HashMap的结构是一模一样的;
3.ConcurrentHashMap 不支持 key 或者 value 为 null ,避免歧义;
4.JDK1.7和1.8的区别:
5.CAS是一种乐观锁机制,也被称为无锁机制