ConcurrentHashMap HashTable

学习 敖丙大神的https://juejin.im/post/5df8d7346fb9a015ff64eaf9博客笔记

一、多线程场景下map的使用方案

  1. Collections.synchronizedMap(Map) 创建线程安全的map集合
  2. HashTable
  3. ConcurrentHashMap

二、Conllections.synchronizedMap实现线程安全的方法

在SynchronizedMap内部维护了一个普通对应Map,还有排斥锁mutex
SynchronizedMap内部有两种构造器
在这里插入图片描述
内部方法实现上都是同步代码块 强行代码同步 保证了多线程下的map使用安全

二、HashTable
HashTable实现线程安全的方式是在数据操作的方法上都会加上synchronized限制方法同步安全 保证了线程安全

HashTable与HashMap异同

  1. HashMap允许key-value为null,HashTable的key-value都不允许为null
  2. 实现方式不同:Hashtable继承了Dictionary类,HashMap继承的是AbstractMap类
  3. 初始容量不同:HashMap是16,Hashtable是11 负载因子都默认为:0.75
  4. 扩容机制不同:HashMap是翻倍,Hashtable扩容规则是翻倍+1
  5. 迭代器不同:HashMap是Iterator迭代器 是fail-fast的,而HashTable的Enumerator不是fail-fast的

二、一 fail-fast和fail-safe
快速失败 是java集合中的一种机制,再用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增删改),则会抛出ConcurrentModificationException异常

原理:迭代器在遍历是直接访问集合中的内容时,并且在遍历过程开始时会使用一个modCount遍历,并标识为expectedmodCount变量

而在集合进行增删操作的时候的时候,就会触发modCount变量改变,而在每次迭代器遍历下一个元素之前,都会检查modCount遍历是否为expectedmodCount值,是的化就返回遍历,否则就抛出异常,终止遍历

解决方案:
方法一:

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
   ArrayList.this.remove(lastRet);
   cursor = lastRet;
   lastRet = -1;
   expectedModCount = modCount;

} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
这个方法是迭代器中的remove方法 这个方法并不会改变modcount值 以此保证不会触发异常
方法二:
使用fail-safe机制,使用java并发包(java.util.concurrent)中的CopyOnWriterArrayList类来代替ArrayList,使用 ConcurrentHashMap来代替hashMap。

fail-safe
fail-safe:这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。

原理:

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

三、 ConcurrentHashMap
ConcurrentHashMap底层还是基于数组+链表组成的,不过在jdk1.7和1.8中具体的实现稍有不同

1、jdk1.7
是由Segment数组、HashEntry组成,和HashMap一样,仍然是数组加链表。
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {    
private static final long serialVersionUID = 2249069246763182397L;    
// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶    
transient volatile HashEntry<K,V>[] table;   
 transient int count;        
 // 记得快速失败(fail—fast)么?    
 transient int modCount;        
 // 大小    
 transient int threshold;        
 // 负载因子    
 final float loadFactor;}

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。
volatile的特性是啥?

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  • 禁止进行指令重排序。(实现有序性)
  • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

concurrentHashMap的核心就是采用了分段锁技术,其中Segment继承于ReentrantLock。
不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。
每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

put逻辑:

public V put(K key, V value) {    
    Segment<K,V> s;    
    if (value == null)        
        throw new NullPointerException(); //这就是为啥他不可以put null值的原因
    int hash = hash(key);    
    int j = (hash >>> segmentShift) & segmentMask;    
    if ((s = (Segment<K,V>)UNSAFE.getObject                   
        (segments, (j << SSHIFT) + SBASE)) == null)         
        s = ensureSegment(j);    
    return s.put(key, hash, value, false);
}

他先定位到Segment,然后在进行put操作
我们看看他的put源代码,你就知道他是怎么做到线程安全的了,关键句子我注释了。

    final V put(K key, int hash, V value, boolean onlyIfAbsent) {          
        // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry            
        HashEntry<K,V> node = tryLock() ? null :                
                scanAndLockForPut(key, hash, value);            
        V oldValue;            
        try {                
            HashEntry<K,V>[] tab = table;                
            int index = (tab.length - 1) & hash;                
            HashEntry<K,V> first = entryAt(tab, index);                
            for (HashEntry<K,V> e = first;;) {                    
                if (e != null) {                        
                    K k; 
                    // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。                        
                    if ((k = e.key) == key ||                            
                        (e.hash == hash && key.equals(k))) {                           
                        oldValue = e.value;                           
                        if (!onlyIfAbsent) {                               
                            e.value = value;                                
                            ++modCount;                            
                        }                            
                        break;                        
                    }                        
                    e = e.next;                    
                }                    
                else {                 
                    // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。                        
                    if (node != null)                            
                        node.setNext(first);                        
                    else                            
                        node = new HashEntry<K,V>(hash, key, value, first);                        
                    int c = count + 1;                        
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)                            
                        rehash(node);                        
                    else                            
                        setEntryAt(tab, index, node);                       
                        ++modCount;                        
                        count = c;                        
                        oldValue = null;                        
                        break;                    
                }                
            }          
        } finally {               
            //释放锁                
            unlock();            
        }           
        return oldValue;        
    }

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

尝试自旋获取锁。
如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

get逻辑
get 逻辑比较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

2、 jdk1.8
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。

put逻辑
ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:

根据 key 计算出 hashcode 。
判断是否需要进行初始化。
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
如果都不满足,则利用 synchronized 锁写入数据。
如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
在这里插入图片描述
CAS?自旋?
CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。

CAS 操作的流程如下图所示,线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名

CAS无法保证数据没被别的线程修改过 ABA问题
例如来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。但CAS锁并没有办法对这种场景进行判断

ABA问题解决方案:
1、加版本号 版本号和版本同时修改
2、增加时间戳
3、记录操作

几种锁机制!!!!(学习)

jdk1.8版本为什么会又变更为synchronized?
synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。
针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

get逻辑:

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。
    在这里插入图片描述
    小结:1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

另外参考博客:
https://blog.csdn.net/striner/article/details/86375684 快速失败及安全失败

发布了31 篇原创文章 · 获赞 8 · 访问量 482

猜你喜欢

转载自blog.csdn.net/tiantang_zy/article/details/104945907