今天正式开始自己的分布式学习,在第一章介绍多线程工作模式时,作者抛出了一段关于ConcurrentHashMap代码让我很是疑惑,代码如下:

public class TestClass {

    private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();

    public void add(String key){
Integer value = map.get(key);
if(value == null){
map.put(key, 1);
}else{
map.put(key, value + 1);
}
} }

作者的结论是这样婶的:即使使用线程安全的ConcurrentHashMap来统计信息的总数,依然存在线程不安全的情况。

笔者的结论是这样婶的:ConcurrentHashMap本来就是线程安全的呀,读虽然不加锁,写是会加锁的呀,讲道理的话上面的代码应该没啥问题啊。

既然持怀疑态度,那笔者只有写个测试程序咯,因为伟大的毛主席曾说过:“实践是检验真理的唯一标准” =_=

/**
* @Title: TestConcurrentHashMap.java
* @Describe:测试ConcurrentHashMap
* @author: Mr.Yanphet
* @Email: mr_yanphet@163.com
* @date: 2016年8月1日 下午4:50:18
* @version: 1.0
*/
public class TestConcurrentHashMap { private static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>(); public static void main(String[] args) {
DoWork dw = new DoWork(map);
ExecutorService pool = Executors.newFixedThreadPool(8);
try {
for (int i = 0; i < 20; i++) {
pool.execute(new Thread(dw));// 开启20个线程
}
Thread.sleep(5000);// 主线程睡眠5s 等待子线程完成任务
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();// 关闭线程池
}
System.out.println("统计的数量:" + map.get("count"));
} static class DoWork implements Runnable { private ConcurrentHashMap<String, Integer> map = null; public DoWork(ConcurrentHashMap<String, Integer> map) {
this.map = map;
} @Override
public void run() {
add("count");
} public void add(String key) {
Integer value = map.get(key);// 获取map中的数值
System.out.println("当前数量" + value);
if (null == value) {
map.put(key, 1);// 第一次存放
} else {
map.put(key, value + 1);// 以后次存放
}
} } }

debug输出一下:

当前数量null
当前数量null
当前数量null
当前数量1
当前数量null
当前数量1
当前数量2
当前数量3
当前数量4
当前数量5
当前数量6
当前数量7
当前数量7
当前数量5
当前数量6
当前数量7
当前数量8
当前数量8
当前数量8
当前数量6
统计的数量:7

这结果并不是20呀,瞬间被打脸有木有啊?满满的心塞有木有啊?

秉承着打破砂锅问到底的精神,必须找到原因,既然打了脸,那就别下次还打脸啊.....

翻开JDK1.6的源码:

public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);// segmentFor(hash)用于精确到某个段
}

map的put方法调用了segment(类似hashtable的结构)的put方法,此外该方法涉及的另外两个方法,笔者一并放在一起分析。

 V get(Object key, int hash) {
if (count != 0) { // segment存在值 继续往下查找
HashEntry<K,V> e = getFirst(hash);// 根据hash值定位 相应的链表
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;// 值不为null 立即返回
return readValueUnderLock(e); // 值为null 重新读取该值
}
e = e.next;// 循环查找链表中的下一个值
}
}
return null;// 如果该segment没有值 直接返回null
}
// 定位链表
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
// 再次读取为null的值
V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}

这里需要总结一下了:

1 ConcurrentHashMap读取数据不加锁的结论是不正确的,当读取的值为null时,这时候ConcurrentHashMap是会加锁再次读取该值的(上面粗体部分)。至于读到null就加锁再读的原因如下:

  ConcurrentHashMap的put方法value是不能为null的(稍后代码展示),现在get值为null,那么可能有另外一个线程正在改变该值(比如remove),为了读取到正确的值,所以采取加锁再读的方法。在此对Doug Lee大师的逻辑严密性佩服得五体投地啊有木有......

2 读者大概也知道为啥不是20了吧,虽然put加锁控制了线程的执行顺序,但是get没有锁,也就是多个线程可能拿到相同的值,然后相同的值+1,结果就不是预期的20了。

既然知道了原因,那么修改一下add(String key)这个方法,加锁控制它get的顺序即可。

public void add(String key) {
lock.lock();
try {
Integer value = map.get(key);
System.out.println("当前数量" + value);
if (null == value) {
map.put(key, 1);
} else {
map.put(key, value + 1);
}
} finally {
lock.unlock();
}
}

再次debug输出一下:

当前数量null
当前数量1
当前数量2
当前数量3
当前数量4
当前数量5
当前数量6
当前数量7
当前数量8
当前数量9
当前数量10
当前数量11
当前数量12
当前数量13
当前数量14
当前数量15
当前数量16
当前数量17
当前数量18
当前数量19
统计的数量:20

得到正确的结果。

附上put方法的源码:

public V put(K key, V value) {
if (value == null)
throw new NullPointerException();// 值为空抛出NullPointerException异常
int hash = hash(key.hashCode());// 根据key的hashcode 然后获取hash值
return segmentFor(hash).put(key, hash, value, false); //定位到某个segment
} V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // 该segment总的key-value数量+ 大于threshold阀值
rehash(); // segment扩容
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);// hash值与数组长度-1取&运算
HashEntry<K,V> first = tab[index]; // 定位到某个数组元素(头节点)
HashEntry<K,V> e = first;// 头节点
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next; V oldValue;
if (e != null) {// 找到key 替换旧值
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}else {// 未找到key 生成节点
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}

程序员最怕对技术似懂非懂,在此与君共勉,慎之戒之!!!

  

Java ConcurrentHashMap的小测试的更多相关文章

  1. java.lang.String小测试

    还记得java.lang.String么,如果现在给你一个小程序,你能说出它的结果么 public static String ab(String a){ return a + "b&quo ...

  2. 关于JAVA SESSION的小测试

    手生就要多练啊... package com.jeelearning.servlet; import java.io.IOException; import java.io.PrintWriter; ...

  3. java基础知识小总结【转】

    java基础知识小总结 在一个独立的原始程序里,只能有一个 public 类,却可以有许多 non-public 类.此外,若是在一个 Java 程序中没有一个类是 public,那么该 Java 程 ...

  4. Java ConcurrentHashMap

    通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占, ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术. ...

  5. Java 面向对象编程小练习(曾经)

    最近打算将之前学习过的小练习分享出来,算是巩固知识.虽然是小练习,但是越看越觉得有趣,温故而知新. 练习:功能跳水比赛,8个评委评分.运动员成绩去掉最高和最低之后的平均分 代码实例: 1.导包 imp ...

  6. 纯 Java 开发 WebService 调用测试工具(wsCaller.jar)

    注:本文来自hacpai.com:Tanken的<纯 Java 开发 WebService 调用测试工具(wsCaller.jar)>的文章 基于 Java 开发的 WebService ...

  7. Java学习笔记二十九:一个Java面向对象的小练习

    一个Java面向对象的小练习 一:项目需求与解决思路: 学习了这么长时间的面向对象,我们只是对面向对象有了一个简单的认识,我们现在来做一个小练习,这个例子可以使大家更好的掌握面向对象的特性: 1.人类 ...

  8. Java ConcurrentHashMap 源代码分析

    Java ConcurrentHashMap jdk1.8 之前用到过这个,但是一直不清楚原理,今天抽空看了一下代码 但是由于我一直在使用java8,试了半天,暂时还没复现过put死循环的bug 查了 ...

  9. Java or Python?测试开发工程师如何选择合适的编程语言?

    很多测试开发工程师尤其是刚入行的同学对编程语言和技术栈选择问题特别关注,毕竟掌握一门编程语言要花不少时间成本,也直接关系到未来的面试和就业(不同企业/项目对技术栈要求也不一样),根据自身情况做一个相对 ...

随机推荐

  1. DOM操作技术之动态脚本与动态样式(兼容版)

    动态脚本 使用<script>元素可以向页面中插入Javascript代码,一种方式是通过其src特性包含外部文件,另一种方式就是用这个元素本身来包含代码. 而我们要说的动态脚本,指的是在 ...

  2. linux防火墙(四)—— SNET和DNET原理及应用

    图(1) 一.SNAT策略 应用环境:局域网主机共享单个公网IP地址接入Internet,简单的说就是企业内部局域网想访问外部服务器时,网关型防火墙需要开启的SNAT应用策略. SNAT策略原理:网关 ...

  3. [Microsoft] 微软技术平台的Cloud Building平台AppVeyor

    Link: http://www.tuicool.com/articles/uMBZba http://www.appveyor.com/ 随着云技术的不断完善,基于云的应用越发丰富起来.AppVey ...

  4. JavaWeb学习笔记(六)—— Cookie&Session

    一.会话技术简介 会话可简单理解为:用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一个会话. 在日常生活中,从拨通电话到挂断电话之间的一连串的你问我答的过程 ...

  5. struts2学习笔记(六)—— 拦截器

    一.拦截器概述 拦截器,在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前进行拦截,然后在之前或之后加入某些操作.拦截器是AOP的一种实现策略. 在We ...

  6. Android 基于TCP多线程通信实现群聊天的功能

    1.TCP多线程原理图 2.实现方法 (1)服务器端 (2)客户端 3.java后台代码 主界面 package com.lucky.test50socket2; import android.ann ...

  7. CodeForces - 1110C-Meaningless Operation(打表找规律)

    Can the greatest common divisor and bitwise operations have anything in common? It is time to answer ...

  8. [转] 【译】让人倾倒的 11 个 npm trick

    [From] https://segmentfault.com/a/1190000006804410 本文转载自:众成翻译译者:文蔺链接:http://www.zcfy.cc/article/1206 ...

  9. 方格填数--蓝桥杯---dfs

    答案:1580 相似题目:N皇后问题 注意要枚举的是什么 #include<iostream> #include<string.h> using namespace std; ...

  10. git平台创建项目(码云)

    一.在码云创建项目 1.新建仓库 2,项目名称等 3.创建后的界面 4.克隆远程项目到本地(项目地址和用户名密码) git clone 仓库地址 注意,如果当前目录下出现git仓库同名目录时,会克隆失 ...