论坛首页 入门技术论坛

ConcurrentHashMap并不是绝对线程安全的

浏览 5298 次
该帖已经被评为新手帖
作者 正文
   发表时间:2011-11-09  
ConcurrentHashMap是线程安全的概念已经深入人心,让我们在使用的时候有些大意了,我也懒得动脑子,直接使用,结果碰到钉子了.
这个问题让我很郁闷,程序逻辑全是对的,但是问题却明明摆在那边,最后怀疑是HashMap的问题。
package com.taobao.mmp.test;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.taobao.mmp.dataobject.ServiceDO;

public class TTTT {

	private static Map<Long, ServiceDO> widgetCacheMap = new ConcurrentHashMap<Long, ServiceDO>();
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		for(int i=0;i<10000;i++){
			Thread tt = new Thread(new Rund());
			tt.start();
		}
	}

	static class Rund implements Runnable{

		public void run() {
			// TODO Auto-generated method stub
			test();
		}

		/**
		 * 1W次,总有那么几次线程不安全
		 */
		public void test(){
				TTTT tt = new TTTT();
				tt.set();
				int s1 = widgetCacheMap.get(1L).getStatus();
				tt.change();
				int s2 = widgetCacheMap.get(1L).getStatus();
				if(s1==s2){
					System.out.println(s1+":"+s2);
				}
		}

	}



	public void set() {
			Map mm= new HashMap();
			ServiceDO ss = new ServiceDO();
			ss.setStatus(1);
			mm.put(1L, ss);
			widgetCacheMap = mm;
	}
	public void change(){
			Map mm= new HashMap();
			ServiceDO ss = new ServiceDO();
			ss.setStatus(2);
			mm.put(1L, ss);
			widgetCacheMap = mm;
	}

}


执行10000次,多执行几次,或许你会发现,真的一般情况下是线程安全的,但是在大量并发的时候,线程就变得不那么安全了.
输出结果如下:
2:2
2:2
2:2

为什么出现这种情况,我在第一个地方设置值,然后取值,第二个地方再设置值,然后取值,两个值应该不同的,判断相同的时候,既然出现了。有人怀疑是ConcurrentHashMap,那你可以换成HashMap试试.结果一样.
为什么是2,2不是1,1;当然一般情况下是1:2,并发情况下就变成2,2了.
有人怀疑是初始化widgetCacheMap的问题,那么改代码如下:
	public void set() {
			//Map mm= new HashMap();
			ServiceDO ss = new ServiceDO();
			ss.setStatus(1);
			widgetCacheMap.put(1L, ss);
			//widgetCacheMap = mm;
	}
	public void change(){
			//Map mm= new HashMap();
			ServiceDO ss = new ServiceDO();
			ss.setStatus(2);
			widgetCacheMap.put(1L, ss);
			//widgetCacheMap = mm;
	}

真是不改不知道,一改吓一跳,这回出现刚才说的情况1,1

1:1
2:2
2:2
2:2
2:2

而且改了之后其并发问题更严重了,因为这里每一次put都需要加行锁,其并发的概念也就上升了.
推荐写法还是按第一次方法,对象的覆盖是原子的,最好加一把锁,否则你第一次覆盖了,第二次又被别人覆盖了.
于是代码如下:
	public void set() {
		synchronized (widgetCacheMap) {

			Map mm= new HashMap();
			ServiceDO ss = new ServiceDO();
			ss.setStatus(1);
			mm.put(1L, ss);
			widgetCacheMap = mm;

		}
	}
	public void change(){
		synchronized (widgetCacheMap) {
			Map mm= new HashMap();
			ServiceDO ss = new ServiceDO();
			ss.setStatus(2);
			mm.put(1L, ss);
			widgetCacheMap = mm;
		}

	}

保持widgetCacheMap的变更成原子状态。当然还会出现上面的情况,这是为什么呢。
因为每一个线程获取的时候,可能取的是原子1,也可能是原子2,如果在多线程获取的时候加一把锁,那么获取的就是原子X,但至少是一个原子,要么1,要么2.
于是代码如下:
	public void test(){
			synchronized (widgetCacheMap) {
				TTTT tt = new TTTT();
				tt.set();
				int s1 = widgetCacheMap.get(1L).getStatus();
				tt.change();
				int s2 = widgetCacheMap.get(1L).getStatus();
				if(s1==s2){
					System.out.println(s1+":"+s2);
				}
			}
		}


结果又出现如上现象,这是为什么呢,因为锁里面还加着锁,锁最好是原子化,尽量保持最小范围,不能价懒,像我一样就悲剧了.


		/**
		 * 1W次,总有那么几次线程不安全
		 */
		public void test(){
				TTTT tt = new TTTT();
				tt.set();
				int s1 = -1;
				synchronized (widgetCacheMap) {
				  s1 = widgetCacheMap.get(1L).getStatus();
				}
				tt.change();
				int s2 = -2;
				synchronized (widgetCacheMap) {
				  s2 = widgetCacheMap.get(1L).getStatus();
				}
				if(s1==s2){
					System.out.println(s1+":"+s2);
				}
		}

还是出现上面这种情况,通阅全码,发现每一次都是原子了,应该没问题了。
但是还需要考虑run方法是多线程的,只有一个线程进入test,那就算原子了.如下:
唉,这是为什么呢,syn不起作用?
开始怀疑,于是去掉所有的syn,只添加run方法中的如下:
	/**
		 * 1W次,总有那么几次线程不安全
		 */
		public   void test(){
			synchronized (widgetCacheMap) {
				TTTT tt = new TTTT();
				tt.set();
				int s1 = -1;

				  s1 = widgetCacheMap.get(1L).getStatus();
				tt.change();
				int s2 = -2;
				  s2 = widgetCacheMap.get(1L).getStatus();

				if(s1==s2){
					System.out.println(s1+":"+s2);
				}
			}
		}


整个进行原子操作,结果让人晕死。还是出现在,最后想了想,原来Hash或者CurrentHashMap也一样,在中间change了一下,而syn锁定的是一个不变的东西。
于如改代码如下:

	/**
		 * 1W次,总有那么几次线程不安全
		 */
		public   void test(){
			synchronized ("") {
				TTTT tt = new TTTT();
				tt.set();
				int s1 = -1;

				  s1 = widgetCacheMap.get(1L).getStatus();
				tt.change();
				int s2 = -2;
				  s2 = widgetCacheMap.get(1L).getStatus();

				if(s1==s2){
					System.out.println(s1+":"+s2);
				}
			}
		}

这回你怎么执行都是原子操作了。

总结:ConcurrentHashMap是线程安全的,那是在他们的内部操作,其外部操作还是需要自己来保证其同步的,特别是静态的ConcurrentHashMap,其有更新和查询的过程,要保证其线程安全,需要syn一个不可变的参数才能保证其原子性
   发表时间:2011-11-09  
无语了,
widgetCacheMap = mm;  

这句话在更改全局共享变量,这才是问题所在,跟什么ConcurrentHashMap线程安全什么关系也没有。
0 请登录后投票
   发表时间:2011-11-09  
楼主,你对线程的执行根本就没搞清楚。好好再想想。

倒数第二个还会错,是因为你加锁的对象在change的时候被替换了,等于没加锁。
最后一个正确是因为你所有线程的执行都串行了,而且用字符串做加锁也不是推荐的行为。

0 请登录后投票
   发表时间:2011-11-09  
dennis_zane 写道
楼主,你对线程的执行根本就没搞清楚。好好再想想。

倒数第二个还会错,是因为你加锁的对象在change的时候被替换了,等于没加锁。
最后一个正确是因为你所有线程的执行都串行了,而且用字符串做加锁也不是推荐的行为。


1. 完全正确,在维护静态的HashMap的时候,只能替换Map对象,楼上的有好的解决方案吗?
2. 字符串加锁不推荐,我知道可以用JDK并发包的lock锁进行加锁,没考虑太多。

0 请登录后投票
   发表时间:2011-11-09  
看了前面的测试代码,如果按照楼主的测试,现在没有类是线程安全的。并且这么测试都能测出内存溢出,真是一举两得。
0 请登录后投票
   发表时间:2011-11-09  
Technoboy 写道
看了前面的测试代码,如果按照楼主的测试,现在没有类是线程安全的。并且这么测试都能测出内存溢出,真是一举两得。

过奖了,真理都是在错误中成立的。
0 请登录后投票
   发表时间:2011-11-09   最后修改:2011-11-09
噗,到底是想数据一致性还是线程安全?搞清楚了没?

不是ConcurrentHashMap的错,ConcurrentHashMap是线程安全,只是lz用错方向而已。

lz主要针对的是并发一致性而已,看看并发设计方面的书,提供一个切入点:不变性设计。
0 请登录后投票
   发表时间:2011-11-09  
javaeye现在的帖子很久没有加精了,误导很多人的标题,内容太不负责。
0 请登录后投票
   发表时间:2011-11-09   最后修改:2011-11-09
caoyangx 写道
javaeye现在的帖子很久没有加精了,误导很多人的标题,内容太不负责。

淘宝杯具了
0 请登录后投票
   发表时间:2011-11-09  
ConcurrentHashMap的主要作用是防止多线程要并发的修改一个数据结构的时候破坏这个数据结构。可以通过锁来保护共享数据结构,但是选择线程安全的实现作为替代可能更容易些。

另外:
1.widgetCacheMap = mm; 默默无语中,这还叫没有问题!!!
2.请使用 putIfAbsent 方法好么。。。

楼主需要好好看看多线程
0 请登录后投票
论坛首页 入门技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics