在Java的同步容器中一般情况下都是使用的公共锁来保持同步,比如HashTable ,这种方法确实可以确保同时只能由一个线程来对容器中的同步方法进行操作,但是却不可避免的导致了吞吐量的下降,特别是对于容器的get、contains等操作,需要遍历整个容器,同时调用equals方法来查找特定的对象,这步骤往往要花费大量的时间,导致其它线程不能访问容器,在竞争比较激烈的情况下性能会受到严重的影响。那么如何在不使用这种公共锁的同时又确保线程安全性呢?
对于这个问题,影响性能最关键的原因是因为有一个线程操作时,其它线程由于没有竞争到公共锁,就不能对容器进行操作。所以这里引入一个更加细化的加锁机制,就是分离锁(分段加锁技术)。这种方式可以让任意多的线程同时并发访问,甚至可以同时进行读写操作以及允许任意多个的线程并发修改,从而可以极大地提高性能。
分离锁的思路是分段加锁,前面说到过如果容器只有一把锁,那同时就只能有一个线程操作容器,而分离锁是首先把容器中的数据进行分段,每一段有一把锁,这样整个容器就有多把锁。当多个线程并发访问容器时,每个线程只占有其中一段数据的锁,其它线程可以对其它数据段进行操作,这样就能够提高容器的并发访问效率。
如果理解了上面的原理的话理解ConcurrentHashMap就会相应的轻松多了,ConcurrentHashMap就是使用了分离锁的机制,它把hashtable 分成了多个段(Segment),每个段有一把锁,只要多个线程操作的不是同一个段,那么就可以并发的对ConcurrentHashMap进行操作。
其中的Segment继承自ReentrantLock,同时实现了Serializable接口
static final class Segment<K,V> extendsReentrantLock implements Serializable
Segment数组里面包含的是HashEntry数组,而HashEntry是一个链表结构的元素,源码如下:
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
@SuppressWarnings("unchecked")
static final <K,V> HashEntry<K,V>[] newArray(int i) {
return new HashEntry[i];
}
}
每一个HashEntry包含了final的key、next以及hash,其中的next是指向下一个HashEntry。value 被设置为volatile,所以可以使得读取到的是最新被修改的值。同时由于next被设置为final,不能修改引用地址,所以所有的put操作都必须放在链表的头部,同时next指向原链表的头部。
了解了HashEntry后再来看看详细看看Segment的实现原理。
transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
其包含的主要属性有以上几个,其中count是用来统计该段所包含的HashEntry的个数,从而协调修改和读取操作,只要添加或者删除节点就修改count的值,每一次读取之前需要读取count的值,使读取操作能够读取到最新的值,modCount是用来统计该段被修改的次数,用于在多个段遍历的过程中检测某个段是否被修改过。threashold用来表示需要进行rehash的界限值,table数组存储段中节点,每个数组元素是个hash链,用HashEntry表示。loadFactor表示负载因子。