非线程安全的HashMap 和 线程安全的ConcurrentHashMap(转载)

在平时开发中,我们经常采用HashMap来作为本地缓存的一种实现方式,将一些如系统变量等数据量比较少的参数保存在HashMap中,并将其作 为单例类的一个属性。在系统运行中,使用到这些缓存数据,都可以直接从该单例中获取该属性集合。但是,最近发现,HashMap并不是线程安全的,如果你 的单例类没有做代码同步或对象锁的控制,就可能出现异常。

首先看下在多线程的访问下,非现场安全的HashMap的表现如何,在网上看了一些资料,自己也做了一下测试:

public class MainClass {
 2    
 3    public static final HashMap<String, String> firstHashMap=new HashMap<String, String>();
 4    
 5    public static void main(String[] args) throws InterruptedException {
 6        
 7        //线程一
 8        Thread t1=new Thread(){
 9            public void run() {
10                for(int i=0;i<25;i++){
11                    firstHashMap.put(String.valueOf(i), String.valueOf(i));
12                }
13            }
14        };
15        
16        //线程二
17        Thread t2=new Thread(){
18            public void run() {
19                for(int j=25;j<50;j++){
20                    firstHashMap.put(String.valueOf(j), String.valueOf(j));
21                }
22            }
23        };
24        
25        t1.start();
26        t2.start();
27        
28        //主线程休眠1秒钟,以便t1和t2两个线程将firstHashMap填装完毕。
29        Thread.currentThread().sleep(1000);       这是下边省略号里边的:System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));

for(int l=0;l<50;l++){
            //如果key和value不同,说明在两个线程put的过程中出现异常。
            if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){

System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));

      }

    }

  }

}

上面的代码在多次执行后,发现表现很不稳定,有时没有异常文案打出,有时则有个异常出现:

为什么会出现这种情况,主要看下HashMap的实现:

public V put(K key, V value) {
 2    if (key == null)
 3        return putForNullKey(value);
 4        int hash = hash(key.hashCode());
 5        int i = indexFor(hash, table.length);
 6        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
 7            Object k;
 8            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
 9                V oldValue = e.value;
10                e.value = value;
11                e.recordAccess(this);
12                return oldValue;
13            }
14        }
15
16        modCount++;
17        addEntry(hash, key, value, i);
18        return null;
19    }

我觉得问题主要出现在方法addEntry,继续看:

int hash, K key, V value, int bucketIndex) {
2    Entry<K,V> e = table[bucketIndex];
3        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
4        if (size++ >= threshold)
5            resize(2 * table.length);
6    }

从代码中,可以看到,如果发现哈希表的大小超过阀值threshold,就会调用resize方法,扩大容量为原来的两倍,而扩大容量的做法是新建一个Entry[]:

void resize(int newCapacity) {
 2        Entry[] oldTable = table;
 3        int oldCapacity = oldTable.length;
 4        if (oldCapacity == MAXIMUM_CAPACITY) {
 5            threshold = Integer.MAX_VALUE;
 6            return;
 7        }
 8
 9        Entry[] newTable = new Entry[newCapacity];
10        transfer(newTable);
11        table = newTable;
12        threshold = (int)(newCapacity * loadFactor);
13    }

一般我们声明HashMap时,使用的都是默认的构造方法:HashMap<K,V>,看了代码你会发现,它还有其它的构造方法:HashMap(int initialCapacity, float loadFactor),其中参数initialCapacity为初始容量,loadFactor为加载因子,而之前我们看到的threshold = (int)(capacity * loadFactor);

如果在默认情况下,一个HashMap的容量为16,加载因子为0.75,那么阀值就是12,所以在往HashMap中put的值到达12时,它将自动扩
容两倍,如果两个线程同时遇到HashMap的大小达到12的倍数时,就很有可能会出现在将oldTable转移到newTable的过程中遇到问题,从
而导致最终的HashMap的值存储异常。

JDK1.0引入了第一个关联的集合类HashTable,它是线程安全的。HashTable的所有方法都是同步的。
JDK2.0引入了HashMap,它提供了一个不同步的基类和一个同步的包装器synchronizedMap。synchronizedMap被称为有条件的线程安全类。
JDK5.0util.concurrent包中引入对Map线程安全的实现ConcurrentHashMap,比起synchronizedMap,它提供了更高的灵活性。同时进行的读和写操作都可以并发地执行。

所以在开始的测试中,如果我们采用ConcurrentHashMap,它的表现就很稳定,所以以后如果使用Map实现本地缓存,为了提高并发时的稳定性,还是建议使用ConcurrentHashMap。

====================================================================

另外,还有一个我们经常使用的ArrayList也是非线程安全的,网上看到的有一个解释是这样:
一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B
得到运行的机会。线程B也将元素放在位置0,(因为size还未增长),完了之后,两个线程都是size++,结果size变成2,而只有
items[0]有元素。
util.concurrent包也提供了一个线程安全的ArrayList替代者CopyOnWriteArrayList。

非线程安全的HashMap 和 线程安全的ConcurrentHashMap(转载)

时间: 2024-10-08 13:42:30

非线程安全的HashMap 和 线程安全的ConcurrentHashMap(转载)的相关文章

线程安全的HashMap

一.一般模式下线程安全的HashMap 默认情况常用的HashMap都是线程不安全的,在多线程的环境下使用,常常会造成不可预知的,莫名其妙的错误.那么,我们如何实现一个线程安全的HashMap呢?其中一个可行的方式是使用Collectons.synchronizedMap() 方法来包装我们的HashMap.如下: Map<String, String> map = Collections.synchronizedMap(new HashMap<String,String>());

PoEdu - Windows阶段班 【Po学校】Lesson006_线程_线程的启动到消亡 &amp;线程状态 &amp; 线程安全 &amp; CONTEXT结构体 &amp; 令牌锁

011_线程启动到死亡的详细讲解 1. 线程内核对象 使用计数 2 ##决定当前线程何时销毁 暂停计数 1 ##UINT类型初始为1,可以暂停多次,如置为0则取消暂停. 退出代码 STILL_ACTIVE Signaled FALSE CONTEXT 为空 2. 栈##在隶属于当前进程的空间中,分配一块"栈"空间,以供线程使用 参数 lpParam 入口地址 lpfnAddr 3. CONTEXT##线程上一次运行时的寄存器 IP(指令寄存器) void RtlUserThreadSt

java多线程系类:JUC线程池:03之线程池原理(二)(转)

概要 在前面一章"Java多线程系列--"JUC线程池"02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包括:线程池示例参考代码(基于JDK1.7.0_40)线程池源码分析(一) 创建"线程池"(二) 添加任务到"线程池"(三) 关闭"线程池" 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3509954.h

Net线程足迹 传递参数至线程

方法一:应用ParameterizedThreadStart这个委托来传递输入参数,这种方法适用于传递单个参数的情况. [c-sharp] view plaincopy using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using Sys

使用线程池而不是创建线程

在我们开发程序时,若存在耗性能.高并发处理的任务时,我们会想到用多线程来处理.在多线程处理中,有手工创建线程与线程池2种处理方式,手工创建线程存在管理与维护的繁琐..Net线程池能够帮我们完成线程资源的管理工作,使用我们专注业务处理,而不是代码的细微实现.在你创建了过多的任务,线程池也能用列队把无法即使处理的请求保存起来,直至有线程释放出来. 当应用程序开始执行重复的后台任务,且并不需要经常与这些任务交互时,使用.Net线程池管理这些资源将会让性能更佳.我们可以使用ThreadPool.Queu

posix 线程(一):线程模型、pthread 系列函数 和 简单多线程服务器端程序

posix 线程(一):线程模型.pthread 系列函数 和 简单多线程服务器端程序 一.线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属于1:1模型. (一).N:1用户线程模型 “线程实现”建立在“进程控制”机制之上,由用户空间的程序库来管理.OS内核完全不知道线程信息.这些线程称为用户空间线程.这些线程都工作在“进 程竞争范围”(process contention scope):各个线程在同一进程竞争“被调度的CPU时间”(但

进击的Python【第九章】:paramiko模块、线程与进程、各种线程锁、queue队列、生产者消费者模型

一.paramiko模块 他是什么东西? paramiko模块是用python语言写的一个模块,遵循SSH2协议,支持以加密和认证的方式,进行远程服务器的连接. 先来个实例: 1 import paramiko 2 # 创建SSH对象 3 ssh = paramiko.SSHClient() 4 5 # 允许连接不在know_hosts文件中的主机 6 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 7 # 连接服务器 8 ss

Android性能优化之线程池策略和对线程池的了解

线程的运行机制 1. 开启线程过多,会消耗cpu 2. 单核cpu,同一时刻只能处理一个线程,多核cpu同一时刻可以处理多个线程 3. 操作系统为每个运行线程安排一定的CPU时间----`时间片`,系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因为时间相当短,多个线程频繁地发生切换,因此给用户的感觉就是好像多个线程同时运行一样,但是如果计算机有多个CPU,线程就能真正意义上的同时运行了. 线程池的作用 1. 线程池是预先创建线程的一种技术.线程池在还没有任务到来之前,创建一定数

线程与进程3-守护线程

join(),等待子线程执行完了,主线程才继续往下执行,等所有线程执行完了,才退出程序. 当把子线程变成守护线程以后,只要主线程(非守护线程)执行完就退出程序,不管子线程完了没完. 主线程没法设置成守护线程. #主线程启动子线程之后,两者是并行的,相互之间是独立的. import threading,time def run(n): print('task',n) time.sleep(2) print('task done',threading.current_thread()) #打印当前线