且如果该槽位(也就是桶)上的数据结构如果是链表,则按照链表的插入方式,直接接在当前的链表的后面。如果数量大于了树化的阈值就会转为红黑树。
如果这个key存在,就会直接覆盖。
判断是否需要扩容
看到这你可能会有一堆的疑问。
例如在多线程的情况下,几个线程同时来执行put操作时,怎么保证只执行一次初始化,或者怎么保证只执行一次扩容呢?万一我已经写入了数据,另一个线程又初始化了一遍,岂不是造成了数据不一致的问题。同样是多线程的情况下, 怎么保证put值的时候不会被其他线程覆盖。CAS又是什么?
接下来我们就来看一下在多线程的情况下,ConcurrentHashMap是如何保证线程安全的。
初始化的线程安全首先我们来看初始化的源码。
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; }可以看到有一个关键的变量,sizeCtl,其定义如下。
private transient volatile int sizeCtl;sizeCtl使用了关键字volatile修饰,说明这是一个多线程的共享变量,可以看到如果是首次初始化,第一个判断条件if ((sc = sizeCtl) < 0)是不会满足的,正常初始化的话sizeCtl的值为0,初始化设定了size的话sizeCtl的值会等于传入的size,而这两个值始终是大于0的。
CAS然后就会进入下面的U.compareAndSwapInt(this, SIZECTL, sc, -1)方法,这就是上面提到的CAS,Compare and Swap(Set),比较并交换,Unsafe是位于sun.misc下的一个类,在Java底层用的比较多,它让Java拥有了类似C语言一样直接操作内存空间的能力。
例如可以操作内存、CAS、内存屏障、线程调度等等,但是如果Unsafe类不能被正确使用,就会使程序变的不安全,所以不建议程序直接使用它。
compareAndSwapInt的四个参数分别是,实例、偏移地址、预期值、新值。偏移地址可以快速帮我们在实例中定位到我们要修改的字段,此例中便是sizeCtl。如果内存当中的sizeCtl是传入的预期值,则将其更新为新的值。这个Unsafe类的方法可以保证这个操作的原子性。当你在使用parallelStream进行并发的foreach遍历时,如果涉及到修改一个整型的共享变量时,你肯定不能直接用i++,因为在多线程下,i++每次操作不能保证原子性。所以你可能会用到如下的方式。
AtomicInteger num = new AtomicInteger(); arr.parallelStream().forEach(item -> num.getAndIncrement());你可能会好奇,为什么使用了AtomicInteger就可以保证原子性,跟Unsafe类和CAS又有什么关系,让我们接着往下,看getAndIncrement方法的底层实现。
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }可以看到,底层调用的是Unsafe类的方法,这不就联系上了吗,而getAndIncrement的实现又长这样。
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }没错,这里底层调用了compareAndSwapInt方法。可以看到这里加了while,如果该方法返回false就一直循环,直到成功为止。这个过程有个