Java ConcurrentHashMap分析
2015-11-17 20:38
781 查看
今天项目里面出现了一个bug.原因是在多线程的环境下使用了HashMap。
HashMap是一个非线程安全的类。
举一个例子,在多线程中,如果有当一个线程在遍历HashMap时,另一个线程执行了put或者remove操作会发生ConcurrentModificationException
[align=left]我们知道可以用线程安全的Hashtable来代替HashMap。但问题是Hashtable的做法是在所有的方法前都加入了synchronized关键字来实现同步[/align]
[align=left]这样效率是很低的,因为所有线程对这个map的任何操作都要竞争这个锁[/align]
[align=left]
[/align]
[align=left]所以这个时候ConcurrentHashMap就是很好的替代了[/align]
[align=left]首先还是之前的例子 把HashMap换成ConcurrentHashMap后代码可以很好的运行不会有任何错误[/align]
[align=left]那么HashMap是如何实现同步,而又比Hashtable高效呢[/align]
[align=left]
[/align]
[align=left]JAVA1.7版本:[/align]
[align=left][/align]
[align=left]一。ConcurrentHashMap把整个Hash表切割成了很多块(Segment)。[/align]
[align=left]每一个块用一个重入锁保证它的线程安全性。[/align]
[align=left]
[align=left]每一个Segment管理一个hash表[/align]
[align=left]当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。[/align]
[align=left]那么具体的的结构如下:[/align]
[align=left]二。ConcurrentHashMap的get()操作是无锁的而且能保证线程一致性。[/align]
[align=left]ConcurrentHashMap中Entry的value值是用volatile关键字修饰的。[/align]
[align=left]定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get() 操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。——深入分析ConcurrentHashMap [/align]
[align=left]
[/align]
[align=left]三。对ConcurrentHashMap进行遍历的时候。[/align]
[align=left]期间其他线程对这个ConcurrentHashMap做了修改(比如执行remove或者put)不会出现错误。[/align]
[align=left]ConcurrentHashMap 中的每一个Entry的hash值,key值,next指针(指向下一个Entry)都是是final关键字定义的。这就保证了一旦通过put()操作往hash表里面插入了一个Entry以后,就无法把它从链表上删除了。[/align]
[align=left]
[/align]
[align=left]那remove()操作怎么办呢?[/align]
[align=left]它的办法就是——创建一个新的链表。[/align]
当我们执行remove()操作的时候。把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。下面通过图例来说明remove()操作。假设写线程执行
remove() 操作,要删除链表的 C 节点,另一个读线程同时正在遍历这个链表。
[align=left]
[/align]
执行删除之前的原链表:
执行删除之后的新链表:
——摘自:探索 ConcurrentHashMap 高并发性的实现机制
[align=left]
[/align]
从上图可以看出,删除节点 C 之后的所有节点原样保留到新链表中;删除节点 C 之前的每个节点被克隆到新链表中,注意:它们在新链表中的链接顺序被反转了。
在执行 remove 操作时,原始链表并没有被修改,也就是说:读线程不会受同时执行 remove 操作的并发写线程的干扰。
综合上面的分析我们可以看出,写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。
参考:
jdk1.7源码
《java并发编程实战》
HashMap源码分析
探索 ConcurrentHashMap 高并发性的实现机制
深入分析ConcurrentHashMap
再谈重入锁
JAVA1.8版本:
java1.8版本中对ConcurrentHashMap做了很大的改变
改进一:取消了单独独立出来的segments字段,而是直接采用Array中每个链表的第一个节点作为锁,从而实现了对每一行数据进行加锁。
以下两行代码提炼自Put()函数
改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
HashMap是一个非线程安全的类。
举一个例子,在多线程中,如果有当一个线程在遍历HashMap时,另一个线程执行了put或者remove操作会发生ConcurrentModificationException
public class TestHashMap { public static void main(String[] args) { final Map<String,String> sessionMap = new HashMap<String,String>(); for(int i=0;i<3;i++){ sessionMap.put(i+ "",i+"" ); } Thread t = new Thread(new Runnable(){ public void run() { Iterator<Entry<String, String> iter = sessionMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<String,String> entry = (Map.Entry<String,String>)iter.next(); try { Thread. sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } String key = (String)entry.getKey(); String value = (String)entry.getValue(); } } }); t.start(); Thread t2 = new Thread(new Runnable(){ public void run() { try { Thread. sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } sessionMap.remove( "0"); } }); t2.start(); } }
[align=left]我们知道可以用线程安全的Hashtable来代替HashMap。但问题是Hashtable的做法是在所有的方法前都加入了synchronized关键字来实现同步[/align]
[align=left]这样效率是很低的,因为所有线程对这个map的任何操作都要竞争这个锁[/align]
[align=left]
[/align]
[align=left]所以这个时候ConcurrentHashMap就是很好的替代了[/align]
[align=left]首先还是之前的例子 把HashMap换成ConcurrentHashMap后代码可以很好的运行不会有任何错误[/align]
[align=left]那么HashMap是如何实现同步,而又比Hashtable高效呢[/align]
[align=left]
[/align]
[align=left]JAVA1.7版本:[/align]
[align=left][/align]
[align=left]一。ConcurrentHashMap把整个Hash表切割成了很多块(Segment)。[/align]
[align=left]每一个块用一个重入锁保证它的线程安全性。[/align]
[align=left]
static final class Segment<K,V> extends ReentrantLock implements Serializable[/align]
[align=left]每一个Segment管理一个hash表[/align]
[align=left]当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。[/align]
[align=left]那么具体的的结构如下:[/align]
[align=left]二。ConcurrentHashMap的get()操作是无锁的而且能保证线程一致性。[/align]
[align=left]ConcurrentHashMap中Entry的value值是用volatile关键字修饰的。[/align]
[align=left]定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get() 操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。——深入分析ConcurrentHashMap [/align]
[align=left]
[/align]
[align=left]三。对ConcurrentHashMap进行遍历的时候。[/align]
[align=left]期间其他线程对这个ConcurrentHashMap做了修改(比如执行remove或者put)不会出现错误。[/align]
[align=left]ConcurrentHashMap 中的每一个Entry的hash值,key值,next指针(指向下一个Entry)都是是final关键字定义的。这就保证了一旦通过put()操作往hash表里面插入了一个Entry以后,就无法把它从链表上删除了。[/align]
[align=left]
[/align]
[align=left]那remove()操作怎么办呢?[/align]
[align=left]它的办法就是——创建一个新的链表。[/align]
当我们执行remove()操作的时候。把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。下面通过图例来说明remove()操作。假设写线程执行
remove() 操作,要删除链表的 C 节点,另一个读线程同时正在遍历这个链表。
[align=left]
[/align]
执行删除之前的原链表:
执行删除之后的新链表:
——摘自:探索 ConcurrentHashMap 高并发性的实现机制
[align=left]
[/align]
从上图可以看出,删除节点 C 之后的所有节点原样保留到新链表中;删除节点 C 之前的每个节点被克隆到新链表中,注意:它们在新链表中的链接顺序被反转了。
在执行 remove 操作时,原始链表并没有被修改,也就是说:读线程不会受同时执行 remove 操作的并发写线程的干扰。
综合上面的分析我们可以看出,写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。
参考:
jdk1.7源码
《java并发编程实战》
HashMap源码分析
探索 ConcurrentHashMap 高并发性的实现机制
深入分析ConcurrentHashMap
再谈重入锁
JAVA1.8版本:
java1.8版本中对ConcurrentHashMap做了很大的改变
改进一:取消了单独独立出来的segments字段,而是直接采用Array中每个链表的第一个节点作为锁,从而实现了对每一行数据进行加锁。
以下两行代码提炼自Put()函数
f = tabAt(tab, i = (n - 1) & hash) synchronized (f)
改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
相关文章推荐
- 【java】IO流
- 从头认识java-9.3 向容器添加一组数据与容器的打印
- Jsp+Servlet+JavaBean原生态开发问题集锦
- spirng中的asm与jdk不兼容<已解决>
- 第12周-显示一个棋盘
- 关于java的volatile关键字与线程栈的内容以及单例的DCL
- 简易遍历输出C盘java文件夹所有文件,并且拷贝到E盘。
- Struts2 XML配置
- 如果有人问你Java垃圾回收机制你这么答就好了!
- Eclipse设置自动生成注释
- java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
- java继承&子父类成员域加载及执行过程
- Java 文件监控,实时监控文件加载
- 使用WebMagic爬新浪博客
- 第12周-Swing通用特性-六个色彩标签的框架
- 无法载入共享目标对象‘...rJava.dll’
- 【Struts】服务器文件的上传和下载
- JavaWeb开发基础:JDBC
- POI 操作excel注意事项
- Spring框架进行Service和DAO单元测试