【读书笔记】《大型网站系统与Java中间件实践》第一章1.2.2.3 同步陷阱

这一小节给了一段代码,

 1 public class TestClass {
 2     private HashMap<String, Integer> map = new HashMap<String, Integer>();
 3     public synchronized void add(String key){
 4           Integer value = map.get(key);
 5           if(value == null){
 6               map.put(key,1);
 7           }else{
 8              map.put(key, value + 1);
 9           }
10     }  
11 }                

可以看出,这是对一个不保证线程安全的容器做写入同步。书中该代码前的语境是这样:

。。。不过,需要在这里提一点的是,有时通过加锁把使用线程不安全容器的代码改为使用线程安全容器的代码时,会遇到笔者之前遇到过的一个陷阱,即在一个使用 map 存储信息后统计总数的例子中,map 中的 value 整型使用线程不安全的 HashMap 代码是这样写的。。。

由此可见,一开始贴的代码是正确也推荐使用的。但之后作者在这贴了一段代码并叙述

。。。如果我们使用 ConcurrentHashMap 来替换 HashMap,并且仅仅是去掉 synchronized 关键字,那么就出问题了。问题不复杂,大家可以自己来思考答案。

代码如下

 1 public class TestClass {
 2     private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
 3     public void add(String key){
 4           Integer value = map.get(key);
 5           if(value == null){
 6               map.put(key,1);
 7           }else{
 8              map.put(key, value + 1);
 9           }
10     }  
11 }       

这里的 ConcurrentHashMap 是 Java 1.5 版本出的线程安全容器,采用分段锁(lock striking)做细粒度的同步,针对对并发的访问,动作的吞吐量也不至于大打折扣。

乍一看好像没有什么不对劲的,既然选择一个线程安全容器,那么监视器可以移除也不会有什么问题吧?

也不会有什么问题吧?(确认+1)

也不会有什么问题吧?(确认 again)

 

 

如果不是作者提到这是个陷阱,笔者作为个小菜鸟将来有120%的可能会掉这个坑里。。。那就来分析下这段代码究竟是有什么问题吧!

 

可以看出的是,这个 add 方法是个 check then act 的动作,而这就会造成常见的 race condition 。来看一段 stackoverflow 上的解释

A race condition occurs when two or more threads can access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data. Therefore, the result of the change in data is dependent on the thread scheduling algorithm, i.e. both threads are "racing" to access/change the data.

Problems often occur when one thread does a "check-then-act" (e.g. "check" if the value is X, then "act" to do something that depends on the value being X) and another thread does something to the value in between the "check" and the "act". E.g:

接下来也给出简洁的代码阐述这一现象:

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"

   // If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
   // y will not be equal to 10.
}

上面说,并发的访问或者更新操作,如果不加锁控制同步,那 if 内的代码的先决条件并不一定就是显式 check 的,也许其他线程已经对那个数据动了手脚。到这就很明显了,有点并发意识的朋友大概都能理解了吧。嘻嘻。

 

由这个代码陷阱让笔者想起了一些概念,一方面是本文所说的访问操作的同步,另一方面呢,就是这个 map 的发布,也就是常说的暴露引用什么的,要留心即使这里锁了这个方法,当别处的代码也可以访问更新这个 map 时,依旧会导致同样的陷阱。当然,最好是直接
synchronized(map){...},直接对 map 加锁,避免使用代码的内置锁。

时间: 2017-02-04 15:26

 

update 

额外补充:上面提到的 synchronized(map){...}这个方法不是一个良好的解决方案,即使他能提供对 map 的串行访问,但这是用 ConcurrentHashMap 的优势就不大了,访问吞吐量和代码可伸缩性都大大降低。

 

 

有什么稍微好一点的方法呢?

 

posted @ 2017-02-04 15:26  奔馬  阅读(347)  评论(0编辑  收藏  举报