Java-第十六部分-JUC-Unsafe和ConcurrentHashMap-jdk1.7

154 阅读5分钟

JUC全文

Unsafe

  • java后门类,提供硬件级别的原子操作,进行底层操作

部分Api

  • arrayBaseOffset获取数组的基础偏移量,在内存中的位置
  • arrayIndexScale获取数组元素中的偏移间隔,获取数组中对应的元素,offset + idx * scale
  • getObjectVolatile获取对象的属性值或数组中的元素
  • putOrderVolatile设置对象的属性值或数组中某个角标的元素,不保证线程间可见性,不保证线程安全,更高效
  • putObjectVolatile设置对象的属性值或者数组中的某个角标的元素,保证可见性

使用

  • 获取Unsafe
static Unsafe unsafe;
static {
    Field field = null;
    try {
        field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 使用
Integer[] arr = new Integer[]{1, 2, 3, 4};
//基础偏移
int baseOffset = unsafe.arrayBaseOffset(Integer[].class);
//数据间隔
int indexScale = unsafe.arrayIndexScale(Integer[].class);
//获取元素
Object o = unsafe.getObjectVolatile(arr, 2 * indexScale + baseOffset);
System.out.println(o);
//设置元素
unsafe.putOrderedObject(arr, 2 * indexScale + baseOffset, 6);
System.out.println(Arrays.toString(arr));
//如果 arr 的 2 * indexScale + baseOffset 的位置的值为 6 那么就设置为101
boolean res = unsafe.compareAndSwapObject(arr, 2 * indexScale + baseOffset, 3, 101);
System.out.println(res);
System.out.println(Arrays.toString(arr));

ConcurrentHashMap-jdk1.7

  • 内部结构有一个Segment<K,V>[] segments,每一个segment中有HashEntry<K,V>[] table,用来真正存放数据(被封装成HashEntry对象),每个Entry都可以存储一个链表

数组 ) 数组 ) 链表

  • 分段锁思想,可以为不同segment进行上锁动作,可以让多线程同时读写不同的segment

提高并发,保证线程安全

初始化

  • 最大容量为1 << 16segment*1 << 30HashEntry
  • 空构造,默认大小为32,16个segment * 2个HashEntry
public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, //默认为16
    DEFAULT_LOAD_FACTOR,  //默认为0.75,
    DEFAULT_CONCURRENCY_LEVEL); //默认为16,segments数组的大小
}
  • ssizesegement数组大小,必须为2的整数幂次 image.png
  • 每一个segmentHashEntry的数组大小默认为2,且也需要为2的整数幂次;并将s0设置给ss0位置,SBASE = UNSAFE.arrayBaseOffset(sc);,此时是没有线程安全问题的

此时segments只有一个元素,其中的HashEntry也只有一个初始容量为2,相当于模版对象 image.png

  • 确定位置时,保留的位置,保留高位的sshift,下标范围就在0 ~ 2 ^ sshift - 1
this.segmentShift = 32 - sshift;
  • segment下标范围
this.segmentMask = ssize - 1;
  • c为每个HashEntry的大小,int存在向下取底,需要保证最后的个数大于initialCapacity
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
    ++c;

Segment

  • 继承ReentrantLock

HashEntry

HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
    this.hash = hash;
    this.key = key;
    this.value = value;
    this.next = next;
}

获取位置

  • 需要确定放在哪个segment和哪个entry
  1. 高位确定哪个segment
  2. 低位确定哪个entry
  • 不允许有空值
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    //计算出hash值
    int hash = hash(key);
    //高位,先确定放哪个segment,无符号移动,前面补0
    int j = (hash >>> segmentShift) & segmentMask;
    //取出Segment
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        //初始化Segment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
  • hash(key)会报空指针异常 image.png
  • (j << SSHIFT) + SBASE)等价于j * scale + SBASE
Class sc = Segment[].class; //数组类
ss = UNSAFE.arrayIndexScale(sc);//偏移间隔
//numberOfLeadingZeros前面有多少个0
//确定最高位在第几位
//j << SSHIFT 可以保证正好移动了(j - 1)个Scale,指向segments[j]的起点地址
//如果能保证ss是2的整数次幂这里就能解释得通...不然想不太明白
SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);
  • ensureSegment尝试取了三次,保证多线程下其他线程创建了一个,通过不用锁的方式保证线程安全 image.png

put

  • 调用segmentput的方法,此时能够保证segment的安全。tryLock加锁成功则为null;加锁失败调用scanAndLockForPut image.png
  • 计算角标,调用entryAt取对应角标元素 image.png
  • 遍历当前HashEntry的链表节点,onlyIfAbsent如果只能没有时才能设置值,就不替换旧值 image.png
  • 待加入元素没有重复的添加逻辑 image.png
  • setEntryAt放置元素,此时是在上锁的条件下使用的,可以用putOrderedObject
static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i,
                                   HashEntry<K,V> e) {
    UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}
  • scanAndLockForPut加锁失败逻辑,自旋式抢锁,retries尝试的次数 image.png
  • 开始尝试抢锁,在自旋的过程中处理node,如果存在key相等,就不需要新建,也不会管返回的node是否为null,后续比对时,直接回替换相同的keyvalue;不存在则新建 image.png
  • 后续自旋操作,大于64次调用lock()直接阻塞,等待锁释放 image.png

cpu核数决定自旋次数,最多64次static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

扩容

  • put中,元素个数大于当前segmentthreshold
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
    rehash(node);
  • 准备工作
HashEntry<K,V>[] oldTable = table; //旧数组
int oldCapacity = oldTable.length; //旧数组容量
int newCapacity = oldCapacity << 1; //扩大两倍
threshold = (int)(newCapacity * loadFactor); //新阈值
HashEntry<K,V>[] newTable =
    (HashEntry<K,V>[]) new HashEntry[newCapacity]; //新数据
int sizeMask = newCapacity - 1; //新数据的下标范围
  • 迁移工作
for (int i = 0; i < oldCapacity ; i++) { //遍历该segment中每一个位置
    HashEntry<K,V> e = oldTable[i];
    if (e != null) {
        HashEntry<K,V> next = e.next;
        int idx = e.hash & sizeMask; 
        if (next == null)   //如果只有单个节点直接移动
            newTable[idx] = e;
        else { //加快迁移效率,目的是找到最下面连续的,且可以放在一个下标的链表
            HashEntry<K,V> lastRun = e; //起始点
            int lastIdx = idx; //起始坐标
            //节点迁移有两个位置可以放,原位置/原位置+旧长度
            for (HashEntry<K,V> last = next;
                 last != null;
                 last = last.next) {
                int k = last.hash & sizeMask;
                if (k != lastIdx) { 
                    //下一个跟上一个坐标不相等,那么lastRun指针向后移动
                    //直到找到末尾,连续的可以放在一个角标的链表头
                    //例子 原下标 0 原长度 2 原元素 2 4 8 6 10
                    //新长度 4 新下标 0 处的元素 4 8 新下标 2 处的元素 2 6 10
                    // 6 10 就可以一起迁移
                    lastIdx = k;
                    lastRun = last;
                }
            }
            //直接迁移这个头元素
            newTable[lastIdx] = lastRun;
            // 处理其他节点,直接复制一份,头插法,原下标后面的,会变到前面
            for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                V v = p.value;
                int h = p.hash;
                int k = h & sizeMask;
                HashEntry<K,V> n = newTable[k];
                newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
            }
        }
    }
}
  • 放新的元素
int nodeIndex = node.hash & sizeMask; // 新下标
node.setNext(newTable[nodeIndex]); //头插法
newTable[nodeIndex] = node;
table = newTable;

获取集合长度

  • 通过操作次数是否变动保证长度正确性,至少要循环两次/最多五次,才能获得长度,要先为last复制
  1. 要进行第四次时,会对所有segement进行上锁,而这期间也会存在操作修改,因此第五次时,
public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts操作次数总和上一次的操作次数
    int retries = -1; // 重试次数
    try {
        for (;;) { //死循环
            if (retries++ == RETRIES_BEFORE_LOCK) { //等于2,下面进行三次 -1 0 1
                for (int j = 0; j < segments.length; ++j) //全部加锁阻塞,防止其他线程继续修改集合
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) { //遍历所有segement
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount; //累加集合的操作次数
                    int c = seg.count; //size的累加,累加segment中的实际个数
                    if (c < 0 || (size += c) < 0) //越界会变成负数
                        overflow = true;
                }
            }
            if (sum == last) //是不是上一次操作次数的值,获取长度的过程中没有对集合进行操作
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) { //加过锁,就释放
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

get

public V get(Object key) {
    Segment<K,V> s;
    HashEntry<K,V>[] tab;
    int h = hash(key);
    //获取当前segment的位置
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    //这里获取需要保证可见性
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //遍历双向链表
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

remove

public V remove(Object key) {
    int hash = hash(key); //key的哈希
    Segment<K,V> s = segmentForHash(hash); //该hash对应的segment
    return s == null ? null : s.remove(key, hash, null);
}
  • remove
final V remove(Object key, int hash, Object value) {
    if (!tryLock()) //尝试加锁
        scanAndLock(key, hash);
    V oldValue = null;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash; //tab中的角标
        HashEntry<K,V> e = entryAt(tab, index); //获取角标的链表
        HashEntry<K,V> pred = null;
        while (e != null) { //遍历该双向链表
            K k;
            HashEntry<K,V> next = e.next;
            if ((k = e.key) == key ||
                (e.hash == hash && key.equals(k))) { //找到匹配的节点
                V v = e.value;
                if (value == null || value == v || value.equals(v)) {
                    if (pred == null)
                        setEntryAt(tab, index, next);
                    else
                        pred.setNext(next);
                    ++modCount;
                    --count;
                    oldValue = v;
                }
                break;
            }
            pred = e;
            e = next;
        }
    } finally {
        unlock();
    }
    return oldValue;
}

小结

  • 数据结构为segment[]+tab[]+单向链表
  • 确定位置
  1. 高位确定segment
  2. 低位确定tab
  • 容量
  1. segmenttab容量也要为2的整数幂次
  2. 默认容量segment为16,tab为2,最少为32个
  3. 最大容量segment为1<<16,tab为1<<30
  • 存放元素
  1. 上锁,上锁成功,正常遍历,头插法
  2. 上锁失败,自旋抢锁,默认自旋64/1次就进入阻塞,过程中搜索节点,key是否相等/新建节点
  • 获取size
  1. 通过比较操作次数,来确定是否发生变化
  2. 具体大小,累加每个segment的个数
  3. 最少循环两次,最多循环五次(第四次循环开始前,全部上锁,并且更新操作次数,第五次在计算操作次数,相等后,break)
  • 扩容
  1. 大小扩大两倍
  2. 迁移优化,找到一个链表最末端连续可以放在一个角标的部分,直接迁移,而不是单个迁移