2种办法让HashMap线程安全

HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全.

方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现.

方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进.

下面对这2中实现方法从各个角度进行分析和比较.

实现原理
锁机制的不同
如何得到/释放锁
优缺点

1)实现原理

方法一原理:

通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装. 封装的关键点有2处,1)使用了经典的synchronized来进行互斥, 2)使用了代理模式new了一个新的类,这个类同样实现了Map接口.

private static class SynchronizedMap<K,V>

implements Map<K,V>, Serializable {

// use serialVersionUID from JDK 1.2.2 for interoperability

private static final long serialVersionUID = 1978198479659022715L;

private final Map<K,V> m; // Backing Map

final Object mutex;// Object on which to synchronize

SynchronizedMap(Map<K,V> m) {

if (m==null)

throw new NullPointerException();

this.m = m;

mutex = this;

}

SynchronizedMap(Map<K,V> m, Object mutex) {

this.m = m;

this.mutex = mutex;

}

public int size() {

synchronized(mutex) {return m.size();}

}

//***

//节省空间,删除了大量类似代码

//***

public String toString() {

synchronized(mutex) {return m.toString();}

}

private void writeObject(ObjectOutputStream s) throws IOException {

synchronized(mutex) {s.defaultWriteObject();}

}

}

方法二原理:

重新写了HashMap,比较大的改变有如下几点.

使用了新的锁机制(可以理解为乐观锁)稍后详细介绍

把HashMap进行了拆分,拆分成了多个独立的块,这样在高并发的情况下减少了锁冲突的可能

public V put(K key, V value) {

if (value == null)

throw new NullPointerException();

int hash = hash(key.hashCode());

return segmentFor(hash).put(key, hash, value, false);

}

2)锁机制的不同

方法一使用的是的synchronized方法,是一种悲观锁.在进入之前需要获得锁,确保独享当前对象,然后做相应的修改/读取.

方法二使用的是乐观锁,只有在需要修改对象时,比较和之前的值是否被人修改了,如果被其他线程修改了,那么就会返回失败.锁的实现,使用的是NonfairSync. 这个特性要确保修改的原子性,互斥性,无法在JDK这个级别得到解决,JDK在此次需要调用JNI方法,而JNI则调用CAS指令来确保原子性与互斥性.读者可以自行Google JAVA CAS来了解更多. JAVA的乐观锁是如何实现的.

当如果多个线程恰好操作到ConcurrentHashMap同一个segment上面,那么只会有一个线程得到运行,其他的线程会被LockSupport.park(),稍后执行完成后,会自动挑选一个线程来执行LockSupport.unpark().

    V put(K key, int hash, V value, boolean onlyIfAbsent) {

        lock();

        try {

            int c = count;

            if (c++ > threshold) // ensure capacity

                rehash();

            HashEntry<K,V>[] tab = table;

            int index = hash & (tab.length - 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) {

                oldValue = e.value;

                if (!onlyIfAbsent)

                    e.value = value;

            }

            else {

                oldValue = null;

                ++modCount;

                tab[index] = new HashEntry<K,V>(key, hash, first, value);

                count = c; // write-volatile

            }

            return oldValue;

        } finally {

            unlock();

        }

    }

3)如何得到/释放锁

得到锁:

方法一:在Hashmap上面,synchronized锁住的是对象(不是Class),所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒.

方法二:检查AbstractQueuedSynchronizer.state,如果为0,则得到锁,或者申请者已经得到锁,则也能再辞得到锁,并且state也加1.

释放锁:

都是得到锁的逆操作,并且使用正确,二种方法都是自动选取一个队列中的线程得到锁可以获得CPU资源.

4)优缺点

方法一:

优点:代码实现十分简单,一看就懂.

缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差.

方法二:

优点:需要互斥的代码段比较少,性能会比较好. ConcurrentHashMap把整个Map切分成了多个块,发生锁碰撞的几率大大降低,性能会比较好.

缺点:代码实现稍稍复杂些.

原文地址:http://blog.51cto.com/13963665/2174850

时间: 2024-12-13 04:19:42

2种办法让HashMap线程安全的相关文章

(转载)两种方法让HashMap线程安全

HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全. 方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现. 方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进.

[转]MFC子线程中更新控件内容的两种办法

一.概述 每个系统中都有线程(至少都有一个主线程),而线程最重要的作用就是并行处理,提高软件的并发率.针对界面来说,还能提高界面的响应能力.一般的,为了应用的稳定性,在数据处理等耗时操作会单独在一个线程中运行,而所有与主UI线程有关的控件数据刷新应该到主UI线程中处理.也就是数据处理线程发消息,让界面UI去更新控件.在MFC中线程分为界面线程和工作者线程,界面实际就是一个线程画出来的东西,这个线程维护一个"消息队列","消息队列"也是界面线程和工作者线程的最大区别,

HashMap线程不安全的体现

1.多线程put操作后,get操作导致死循环.2.多线程put非NULL元素后,get操作得到NULL值.3.多线程put操作,导致元素丢失. 参考:多线程下HashMap的死循环问题 比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成: 1. 在 Items[Size] 的位置存放此元素:2. 增大 Size 的值.在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1:而如果是在多线程情况下,比如有两个线程,线程 A 先将

都说知道 HashMap 线程不安全,那它为啥就不安全?

我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密. 1.jdk1.7中的HashMap在jdk1.8中对HashMap做了很多优化,这里先分析在jdk1.7中的问题,相信大家都知道在jdk1.7多线程环境下HashMap容易出现死循环,这里我们先用代码来模拟出现死循环的情况: public static void main(String[] args) { HashMapThread thread0 = new Has

两种办法提交表单

办法1.同一个页面中建立两个表单 各自提交: <form action="?" name="form1" id="form1">  <!-- 表单内容 --> <input type="submit" /> </form> <form action="?" name="form1" id="form1">  

Cocos2d-x 3.1.1 学习日志4--cocos2d-x解决中文乱码问题的几种办法

做个打飞机的游戏,由于版本太新,网上基本没有教教程,我的版本是cocos2d-x 3.1.1的,今天遇到cocos2dx中中文乱码的问题.无奈只好Google百度寻求答案,明白了这个问题的缘由.因为cocos2d-x内部是以utf8处理文本的,而VS直接输入时文本编码为GBK,如果添加L标志,则为Unicode编码. 解决这个问题有三种办法: 将源代码文件保存为utf8编码,不过由于编译器的问题,这种方式会导致很多无法预测的问题 将字符串用utf8编码集中存到一文件中,然后用代码读取这些字符串来

读取硬盘上被覆盖数据的两种办法

文件管理的真谛在于方便保存和迅速提取,所有的文件将通过文件夹分类被很好地组织起来,放在你最能方便找到的地方.解决这个问题目前最理想的方法就是分类管理,从硬盘分区开始到每一个文件夹的建立,我们都要按照自己的工作和生活需要,分为大大小小.多个层级的文件夹,建立合理的文件保存架构. 此外所有的文件.文件夹,都要规范化地命名,并放入最合适的文件夹中.这样,当我们需要什么文件时,就知道到哪里去寻找.这种方法,对于相当数量的人来说,并不是一件轻松的事,因为他们习惯了随手存放文件和辛苦.茫无头绪地查找文件.下

两种unix网络编程线程池的设计方法

unp27章节中的27.12中,我们的子线程是通过操作共享任务缓冲区,得到task的,也就是通过线程间共享的clifd[]数组,这个数组其实就是我们的任务数组,得到其中的connfd资源. 我们对这个任务数组的操作,需要互斥量+条件变量达到同步的目的..每个线程是无规律的从clifd得到任务,然后执行的.任务和线程之间没有对应关系.线程完成本次任务之后,如果任务数组中任然有任务,则再次运行下一个任务. 而另外的一个线程池模型中,pthread_create (&temp[i].tid, NULL

收起键盘的四种办法

1 除了调用相应控件的resignFirstResponder方法外,还有另外三种办法: 2 重载UIViewController中的touchesBegin方法,然后在里面执行在[self.view endEditing:YES];这样单击UIViewController的任意地方,就可以收起键盘. 3 直接执行[[UIApplication sharedApplication]sendAction:@selector(resignFirstResponder)to:nil from:nil