当前位置: 首页 > 科技观察

Java中ConcurrentHashMap原理分析

时间:2023-03-12 05:48:53 科技观察

一、Java并发基础当一个对象或变量可以被多个线程共享时,可能会导致程序的逻辑出现问题。一个对象中有一个变量i=0,有两个线程A和B都想给i加1。这时候就有问题了。关键是i加1的过程不是原子操作。要增加i,第一步是获取i的值。当A获取到i的值为0时,在A将新值写入A之前,B也获取到A的值为0,然后A写入,i变为1,然后B也写入i,i为此时还是1。当然java的内存模型没有上面这么简单。在JavaMemoryModel中,Memory分为两类,mainmemory和workingmemory,mainmemory是所有线程共享的,workingmemory存放的是线程所需要的变量的副本(如果线程要对其中的内容进行操作)主内存,它首先需要将其复制到自己的工作内存中,一般为了速度,工作内存通常在缓存中的cpu中)。volatile变量在操作时不会生成工作内存的副本,而是直接操作主内存。当然,volatile虽然解决了变量可见性的问题,但是并没有解决变量操作的原子性问题。这就需要同步或者配合CAS相关的操作。多线程中的几个重要概念:可见性是指假设一个对象中有一个变量i,那么i就存在主存中。我被加载到这个线程的工作内存中。这时候,工作内存中就有了一份i。这个时候这个线程对i的修改是在它的工作内存中,直到它把i从工作内存写回主内存,i的新值可以被其他线程读取。从某种意义上说,可见性保证了每个线程工作内存的数据一致性。可见性遵循一些规则:当一个线程结束运行时,所有写入的变量将被刷新回主内存。当一个线程第一次读取一个变量时,它从主存中读取最新的。易失性变量将立即写入主存。在jsr133中,增强了volatile的语义。后面会提到,当一个线程释放锁时,所有的变量变化都会被刷新到主内存,然后一个持有相同同步锁的use进程会重新加载所有使用过的变量,从而保证可见性。原子性也拿上面的例子来说。原子性是指当某个线程修改了i的值时,在取出i到将i的新值写入i之间,其他线程不能对i进行任何操作。也就是说保证了某个线程对i的操作是原子的,这样就可以避免数据的脏读。可以通过锁机制或CAS(CompareAndSet需要硬件CPU支持)操作来保证操作的原子性。有序性假设主存中有两个变量i和j,初始值为0,在某线程A的代码中,依次对i和j进行自增操作(操作ofi和j不相互依赖)i++;j++;由于,i,j修改操作的顺序可能会重新排序。那么修改后的ij写入主存的时候,顺序可能不是i和j的顺序。这就是所谓的重新排序。在单线程的情况下,当线程A运行完后,i和j的值都加1。从线程本身的角度来看,线程似乎是按照顺序运行的代码(这些操作是基于as-if-serial语义的),即使在实际运行过程中,i和j的自增也可能被重新排序了。当然,计算机不能帮你随机排序。上下逻辑连接的运行顺序肯定不会改变。但是在多线程环境下,问题就不一样了。比如另一个线程B的代码如下if(j==1){System.out.println(i);}按照我们的思路,当j为1时,i也一定为1,因为代码中的i在j之前是自增的,但是在实际情况下,j为1的时候i可能还是0,这就是重排序带来的不良后果,所以我们需要一些必要的策略来避免某些时候出现这样的问题,从而保证多线程协同工作时有一定的顺序。JMM提供了一种happens-before排序策略。通过这种方式,我们可以在多线程环境中获得如同串行的语义。happens-before这里就不做详细解释了,详情请看这里http://www.ibm.com/developerworks/cn/java/j-jtp03304/,这里主要说一下java新内存模型下volatile的变化,jsr133之前,下面的代码可能有问题(!initialized)sleep();//InThreadBwhile(!initialized)sleep();//使用configOptionsjsr133,虽然对volatile变量的读写不能和对其他volatile变量的读写重排序,但是还是可以用read和write重排序的写入非易失性变量排序,所以上面线程A的操作可能是在initialized变为true的时候,但是configOptions还没有初始化,所以initialized在configOptions之前被线程B看到了,问题就出现了。JSR133专家组决定易失性读取和写入不能与其他内存操作一起重新排序。在新的内存模型下,如果线程A写volatile变量V,线程B读V,那么在写V的时候,A的所有变量值现在都保证对B可见。结果是更强大的volatile语义在访问易变字段时性能受到较大影响的成本。这个用在统计ConcurrentHashMap中segment元素个数的count变量中。2.线程安全的HashMap我们什么时候需要使用线程安全的hashmap?比如一个hashmap在运行的时候只有读操作,那么显然不会有问题,但是当涉及到同时发生变化和读的时候,就要考虑线程安全问题了。在不考虑性能问题的情况下,我们的解决方案包括Hashtable或Collections.synchronizedMap(hashMap)。这两种方式基本上都是锁住整个哈希表结构,这样在锁表期间,其他线程需要等待,性能无疑不高。3.ConcurrentHashMap实现原理数据结构ConcurrentHashMap的目标是实现一个支持高并发、高吞吐的线程安全的HashMap。当然不能直接锁住整个hashtable,所以在ConcurrentHashMap中,数据的组织结构和HashMap是不一样的。一个ConcurrentHashMap由多个Segment组成,每个Segment包含一个HashEntry数组的哈希表,每个Segment包含对自己的Hashtable的操作,如get、put、replace等,当这些操作发生时,对自己的hashtable的操作进行加锁.由于每个segment写操作只锁定自己的hashtable,可能会出现多个线程同时写入的情况,性能无疑比只锁定一个hashtable的情况要好。源码分析ConcurrentHashMap的remove和put操作比较简单,remove或put操作都交给key对应的segment,所以当几个操作不在同一个segment时,可以并发执行。publicVremove(Objectkey){inthash=hash(key.hashCode());returnsegmentFor(hash).remove(key,hash,null);}段中的remove操作与HashMap中的remove操作基本相同,除了锁定不同。/***Remove;matchonkeyonlyifvaluenull,elsematchboth.*/Vremove(Objectkey,inthash,Objectvalue){lock();try{intc=count-1;HashEntry[]tab=table;intindex=hash&(tab.length-1);HashEntryfirst=tab[index];HashEntrye=first;while(e!=null&&(e.hash!=hash||!key.equals(e.key)))e=e.next;VoldValue=null;if(e!=null){Vv=e.value;if(value==null||value.equals(v)){oldValue=v;//Allentriesfollowingremovednodecanstay//inlist,butallprecedingonesneedtobe//cloned.++modCount;HashEntrynewFirst=e.next;for(HashEntryp=first;p!=e;p=p.next)newFirst=newHashEntry(p.key,p.hash,newFirst,p.value);tab[index]=newFirst;count=c;//write-volatile}}returnoldValue;}finally{unlock();}}上面代码中volatile类型的变量count值得一提。这里充分利用了Java5中volatile语义的增强。count=c的操作必须跟在modCount和table等操作之后,这样才能保证这些变量操作的可见性。Segment类继承自ReentrantLock,主要是为了使用ReentrantLock的锁。在多线程争用的情况下,ReentrantLock的实现比synchronized的整体开销要小。put操作类似于remove操作。接下来,让我们看一下get操作。publicVget(Objectkey){inthash=hash(key.hashCode());returnsegmentFor(hash).get(key,hash);}也使用相应的段getVget(Objectkey,inthash){if(count!=0){//read-volatileHashEntrye=getFirst(hash);while(e!=null){if(e.hash==hash&&key.equals(e.key)){Vv=e.value;if(v!=null)returnv;returnreadValueUnderLock(e);//recheck}e=e.next;}}returnnull;}上面代码中一开始就读取volatile变量count进行比较,这还是java5对的volatile语义增强的作用,从而可以获得变量的可见性。所以在count!=0之后,我们可以认为对应的hashtable是唯一的。当然,由于读的时候没有加锁,在get的过程中可能会有更新。当你根据key找到一个元素,但是发现找到的key对应的value为null,此时可能有其他线程在写入这个元素,所以需要在使用锁的同时读取value,确保最终价值。其他涉及读取的相关操作也类似。