Java集合框架中的Map类型的数据结构是非线程安全,在多线程环境中使用时需要手动进行线程同步。因此在java.util.concurrent包中提供了一个线程安全版本的Map类型数据结构:ConcurrentMap。本篇文章主要关注ConcurrentMap接口以及它的Hash版本的实现ConcurrentHashMap。

ConcurrentMap是Map接口的子接口

public interface ConcurrentMap<K, V> extends Map<K, V>

与Map接口相比,ConcurrentMap多了4个方法:

1)putIfAbsent方法:如果key不存在,添加key-value。方法会返回与key关联的value。

V putIfAbsent(K key, V value);

2)remove方法

boolean remove(Object key, Object value);

Map接口中也有一个remove方法:

V remove(Object key);

ConcurrentMap中的remove方法需要比较原有的value和参数中的value是否一致,只有一致才会删除。

3)Replace方法:有2个重载

boolean replace(K key, V oldValue, V newValue);
V replace(K key, V value);

两个重载的区别和2)中的两个remove方法的区别很类似,多了一个检查value一致。

通过ConcurrentMap多出来的方法可以看到多线程中一个很重要的概念:compare。compare的作用就是为了保证value的一致性。

重头戏来了:ConcurrentHashMap。

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable

ConcurrentHashMap和HashMap类似,这里重点关注的是如何实现线程安全,也就是如何加锁。

对于HashMap来说,有一个Entry数组,根据Key的hash值对数组长度取模得到数组下标,找到Entry,遍历整个Entry链表,用equals比较来确定key所在的Entry。

ConcurrentHashMap的基本思想是采取分块的方式加锁,分块数由参数“concurrencyLevel”来决定(和HashMap中的“initialCapacity”类似,实际块数是第一个大于concurrencyLevel的2的n次方)。每个分块被称为Segment,Segment的索引方式和HashMap中的Entry索引方式一致(hash值对数组长度取模)。

对Segment加锁的方式很简单,直接把Segment定义为ReentrantLock的子类。同时Segment又是一个特定实现的hash table。

static final class Segment<K,V> extends ReentrantLock implements Serializable

下面分析ConcurrentHashMap读写时如何加锁。

首先是读操作类的方法,来看get方法:

public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
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;
}

可以看到,读取的时候没有调用的Segment的获取锁的方法,而是通过hash值定位到Entry,然后遍历Entry的链表。为什么这里不用加锁呢?看看HashEntry的代码就会明白了。

    static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;

value和next属性是带有volatile修饰符的,可以大胆放心的遍历和比较。

接着是写操作,写操作是肯定要加锁的。因为Segment可以看成是一个hash table,因此ConcurrentHashMap直接调用Segment的对应的写入方法如put,replace等。

比如put方法

    public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}

因此这里直接关注Segment的对应写操作方法即可。在每个写操作的方法开头都这样的类似代码:

        final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
            HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);

也就是,首先尝试获取锁,如果成功则会带锁继续操作,失败则要通过scanAndLock或scanAndLockForPut获取锁,因此这里关注的重点也就转移到这两个方法了。

按照多线程环境的规则,如果尝试获取锁失败的话就会进入阻塞等待状态,那么这两个方法的作用应该是类似的。

        private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}

这两个方法的逻辑:在等待的时候闲着没事儿干把该做好的准备做好,查找一下目标entry,如果是新建entry就把entry创建好,然后如果一切没问题就用lock()方法把自己给阻塞了,也就是做好准备然后去等着了。

因为Segment本身就可以看成一个hash table,因此必然涉及rehash的问题,因为和HashMap中的rehash类似,在这里就省略了。

《java.util.concurrent 包源码阅读》04 ConcurrentMap的更多相关文章

  1. 《java.util.concurrent 包源码阅读》 结束语

    <java.util.concurrent 包源码阅读>系列文章已经全部写完了.开始的几篇文章是根据自己的读书笔记整理出来的(当时只阅读了部分的源代码),后面的大部分都是一边读源代码,一边 ...

  2. 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

    这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...

  3. 《java.util.concurrent 包源码阅读》02 关于java.util.concurrent.atomic包

    Aomic数据类型有四种类型:AomicBoolean, AomicInteger, AomicLong, 和AomicReferrence(针对Object的)以及它们的数组类型, 还有一个特殊的A ...

  4. 《java.util.concurrent 包源码阅读》17 信号量 Semaphore

    学过操作系统的朋友都知道信号量,在java.util.concurrent包中也有一个关于信号量的实现:Semaphore. 从代码实现的角度来说,信号量与锁很类似,可以看成是一个有限的共享锁,即只能 ...

  5. 《java.util.concurrent 包源码阅读》06 ArrayBlockingQueue

    对于BlockingQueue的具体实现,主要关注的有两点:线程安全的实现和阻塞操作的实现.所以分析ArrayBlockingQueue也是基于这两点. 对于线程安全来说,所有的添加元素的方法和拿走元 ...

  6. 《java.util.concurrent 包源码阅读》09 线程池系列之介绍篇

    concurrent包中Executor接口的主要类的关系图如下: Executor接口非常单一,就是执行一个Runnable的命令. public interface Executor { void ...

  7. 《java.util.concurrent 包源码阅读》05 BlockingQueue

    想必大家都很熟悉生产者-消费者队列,生产者负责添加元素到队列,如果队列已满则会进入阻塞状态直到有消费者拿走元素.相反,消费者负责从队列中拿走元素,如果队列为空则会进入阻塞状态直到有生产者添加元素到队列 ...

  8. 《java.util.concurrent 包源码阅读》10 线程池系列之AbstractExecutorService

    AbstractExecutorService对ExecutorService的执行任务类型的方法提供了一个默认实现.这些方法包括submit,invokeAny和InvokeAll. 注意的是来自E ...

  9. 《java.util.concurrent 包源码阅读》22 Fork/Join框架的初体验

    JDK7引入了Fork/Join框架,所谓Fork/Join框架,个人解释:Fork分解任务成独立的子任务,用多线程去执行这些子任务,Join合并子任务的结果.这样就能使用多线程的方式来执行一个任务. ...

随机推荐

  1. 表单处理的方案与注意事项(servlet)

    摘要 表单是后端程序员用的与接触最多的,我这里例举了常用处理办法,与注意事项 sevlet处理代码 package myform; import java.io.IOException; import ...

  2. Debian 9 中手动设置有线网络

    multi-user.target中不使用networkmanager,上网需要手动设置后才可以,进行有线网线的设置: 首先得到网卡名称:ip addr or ls /sys/class/net/,以 ...

  3. vue-cli中如何引入jquery

    前言 虽然vue不推荐直接操作DOM,而且也提供了操作DOM的方式.但是在某些时候还是要用到jquery(囧),那么如何在使用vue-cli的时候引入jquery呢? 安装 国内镜像 cnpm 安装 ...

  4. emacs 配置

    个人的Emacs配置,环境是archlinux,参考了不少网上资料,因为太多,就不一一列举了,在这里感谢那些作者的辛苦经验劳动. (custom-set-variables ;; custom-set ...

  5. MySQL技术内幕汇总

    MySql技术内幕之MySQL入门(1) MySql技术内幕之MySQL入门(1) 检查系统中是否已经安装了MySQL sudo netstat -tap | grep mysql 若没有显示已安装结 ...

  6. 网络库Alamofire使用方法学习笔记

    Github地址 由于Alamofire是swift网络库,所以,以下的所有介绍均基于swift项目 导入Alamofire 以下为使用cocoapods导入,其余的方式请参考官网 source 'h ...

  7. Linux上安装二进制文件MySQL详解

    前言:昨天晚上搞了很久,终于搞清楚mysql的安装配置了,我真是太low了.当我在云服务器上登进Mysql时,真是高兴哈哈,咱一步一步来,彻底搞懂Mysql的安装配置. 我的安装环境: 阿里云服务器 ...

  8. 无所不会的fiddler遇到的尴尬

    昨天测试项目时,遇到一个尴尬的事 预期功能:点击页面某个按钮会post2个请求 实际情况:点了按钮,fiddler抓包没有看到任何请求 后来经过他人提醒在PC端浏览器打开此页面,点击按钮后看到页面有j ...

  9. HBase数据备份及恢复(导入导出)的常用方法

    一.说明 随着HBase在重要的商业系统中应用的大量增加,许多企业需要通过对它们的HBase集群建立健壮的备份和故障恢复机制来保证它们的企业(数据)资产.备份Hbase时的难点是其待备份的数据集可能非 ...

  10. 删除链表中倒数第n个节点

    给定一个链表,删除链表中倒数第n个节点,返回链表的头节点. 样例 给出链表1->2->3->4->5->null和 n = 2. 删除倒数第二个节点之后,这个链表将变成1 ...