转自:http://blog.csdn.net/huilangeliuxin/article/details/12615507

同步容器类

同步容器类包括Vector和Hashtable(二者是早期JDK的一部分),还包括JDK1.2中添加的一些相似的类。同步容器类实现线程安全的方式是:将状态封闭起来,并对每个公有方法进行同步,使得每次只有一个线程能访问容器状态。这里解释一下所谓“状态”指的就是成员变量,“封装起来”即将它们设不private,但是通过公有的方法外界仍然可以访问修改类的私有成员,所以要用synchronized将公有方法进行同步,使得每次只有一个线程能访问容器状态。在多线程环境下调用同步容器类自带的所有方法时,实际上都是在串行执行,所以这严重降低并发性和吞吐量。

像List、Set、Map这些原本不是同步容器类,也可以通过Collections.synchronizedXXX工厂方法将其变为同步容器类,即对其公有方法进行同步。

  1. List<Student> students=Collections.synchronizedList(new ArrayList<Student>());  

同步容器类的问题

容器上常见的操作包括:迭代访问、跳转(根据指定顺序找到当前元素的下一个元素)、条件运算(先检查再操作Check-And-Act,比如若没有则添加)。

  1. public static Object getLast(Vector vec){  
  2.     int lastIndex=vec.size()-1;  
  3.     return vec.get(lastIndex);  
  4. }  
  5. public static Object deleteLast(Vector vec){  
  6.     int lastIndex=vec.size()-1;  
  7.     return vec.remove(lastIndex);  
  8. }  

大线程的环境下,getLast()函数中第一行代码之后第二行代码之前如果执行了deleteLast(),那么getLast()继续执行第二行就会抛出ArrayIndexOutOfBoundException。所以要把getLast()和deleteLast()都变成原子操作:

  1. public static Object getLast(Vector vec){  
  2.     synchronized(vec){  
  3.         int lastIndex=vec.size()-1;  
  4.         return vec.get(lastIndex);  
  5.     }  
  6. }  
  7. public static Object deleteLast(Vector vec){  
  8.     synchronized(vec){  
  9.         int lastIndex=vec.size()-1;  
  10.         return vec.remove(lastIndex);  
  11.     }  
  12. }  

又比如容器上的迭代操作:

  1. for(int i=0;i<vec.size();i++)  
  2.     doSomething(vec.get(i));  

在size()之后get()之前,其他线程可能删除了vec中的元素,同样会导致抛出ArrayIndexOutOfBoundException。但这并不意味着Vector不是线程安全的,Vector的状态仍然是有效的,而抛出的异常也与其规范保持一致。

正确的做法是在迭代之前对vector加锁:

  1. synchronized(vec){  
  2.     for(int i=0;i<vec.size();i++)  
  3.         doSomething(vec.get(i));  
  4. }  

迭代器与ConcurrentModificationException

使用for或for-each循环对容器进行迭代时,javac内部都会转换成使用Iterator。在对同步容器类进行迭代时如果发现元素个数发生变化,那么hasNext和next将抛出ConcurrentModificationException,这被称为及时失效(fail-fast)。

  1. List<Student> students=Collections.synchronizedList(new ArrayList<Student>());  
  2. //可能抛出ConcurrentModificationException  
  3. for(Student student:students)  
  4.     doSomething(student);  

为了防止抛出ConcurrentModificationException,需要在迭代之前对容器进行加锁,但是如果doSomething()比较耗时,那么其他线程都在等待锁,会极大降低吞吐率和CPU的利用率。不加锁的解决办法是“克隆”容器,在副本上进行迭代,由于副本被封闭在线程内,其他线程不会在迭代期间对其进行修改。克隆的过程也需要对容器加锁,开发人员要做出权衡,因为克隆容器本身也有显著的性能开销。

隐藏的迭代器

注意,下面的情况会间接地进行迭代操作,也会抛出ConcurrentModificationException:

  1. 容器的toString()、hashCode()和equals()方法
  2. containsAll()、removeAll()、retainAll()等方法
  3. 调用以上方法的方法,比如StringBuildre.append(Object)会调用toString()
  4. 容器作为另一个容器的元素或者键
  5. 把容器作为参数的构造函数
扯句不相关的话,编译器会把字符串的连接操作转换为调用StringBuildre.append(Object)。《Effective Java》也提出当使用多个"+"进行字符串连接时考虑使用StringBuildre.append()代替,因为每次“+”操作都要完全地拷贝2个字符串,而StringBuilder.append()是在第一个字符串后面连接第2个字符串,跟C++中的Vector是一个原理。

并发容器

同步容器将对状态的访问都串行化,以实现他们的线程安全性。这样做的代价是严重降低了并发性和吞吐量。
并发容器类提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代时对容器进行加锁。
下面介绍Java5.0中新增加的几个并发容器类。

并发Map

同步容器类在执行每个操作期间都加了一个锁,在一些操作中例如HashMap.get()可能包含大量的工作,如何hashCode不能均匀地分布散列值,那就需要在很多元素上调用equals,而equals本身就有一定的计算量。HashMap将每个方法都在同一个锁上同步使得每次只能有一个线程访问容器,ConcurrentHashMap与HashMap不同,它采用了粒度更细的分段锁(Lock
Striping)。结果是任意的读线程可以并发地访问Map,读线程和写线程可以并发地访问Map,一定数量的写线程可以并发地访问Map。而且在单线程的环境下,ConcurrentHashMap比HashMap性能损失很小。
对于需要在整个Map进行的计算,例如size和isEmpty,ConcurrentHashMap会返回一个近似值而非精确值,因为size()返回的值在计算时可能已经过期了。
ConcurrentHashMap接口中增加了对一些常见复合操作的支持,例如“若没有则添加”、“若相等则替换”、“若相等则移除”等等,在ConcurrentMap接口中已经声明为原子操作。

Copy-On-Write容器

CopyOnWriteArrayList用于替代同步List,同理CopyOnWriteAeeaySet用于替代同步Set,这里就以CopyOnWriteArrayList为例。“写时复制(Copy-On-Write)”容器的线程安全性在于:只要发布一个事实不可变的对象,那么在访问对象时就不需要再进一步同步。当需要修改对象的时候都会重新复制发布一个新的容器副本。
如果一个对象在被创建之后其状态就不能被修改,那这个对象就是不可变对象。不可变对象一定是线程安全的。但要注意即使将所有域声明为final,这个对象仍然有可能是可变的,因为final域中可以保存可变对象的引用。下面举个例子,在可变对象上构建不可变类:

  1. public final class ThreeStorage{  
  2.     private final Set<String> storages=new HashSet<String>();  
  3.     public ThreeStorage(){  
  4.         storages.add("One");  
  5.         storages.add("Two");  
  6.         storages.add("Three");  
  7.     }  
  8.     public boolean isStorage(String name){  
  9.         return storages.contains(name);  
  10.     }  
  11. }  

虽然Set对象是可变的,但从ThreeStorage的设计上来看,Set对象在构造完成后无法对其进行修改。

每当需要修改容器时都是复制底层数组,看一下set()函数的的源代码:

  1. public class CopyOnWriteArrayList<E>{  
  2.     private volatile transient Object[] array;  
  3.     final Object[] getArray() {  
  4.         return array;  
  5.     }  
  6.     final void setArray(Object[] a) {  
  7.         array = a;  
  8.     }  
  9.     public E set(int index, E element) {  
  10.         //获取重入锁  
  11.     final ReentrantLock lock = this.lock;  
  12.     lock.lock();  
  13.     try {  
  14.         Object[] elements = getArray();  
  15.         Object oldValue = elements[index];  
  16.                 //使用的是==而非equals  
  17.         if (oldValue != element) {  
  18.             int len = elements.length;  
  19.                         //复制底层数组  
  20.             Object[] newElements = Arrays.copyOf(elements, len);  
  21.             newElements[index] = element;  
  22.                         //把底层数组写回  
  23.             setArray(newElements);  
  24.         } else {  
  25.             setArray(elements);  
  26.         }  
  27.         return (E)oldValue;  
  28.     } finally {  
  29.                 //释放锁  
  30.         lock.unlock();  
  31.     }  
  32.     }  
  33. }  

一进入set()函数就加了锁,函数结束时才释放锁。
因为复制底层数组本身就有一定的开销,所以仅当迭代操作远远多于修改操作时,才应该使用CopyOnWrite容器。

并发Queue

基本的Collection就是List、Set和Map,Queue底层是由LinkedList来实现的,因为它去除了List的随机访问功能,因此更高效。
ConcurrentinkedQueue是一个传统的先进先出队列。PriorityQueue是一个非并发的优先队列(说这个似乎与本文的主题无关)。
Queue上的操作是阻塞的,如果队列为空,那么获取元素的操作会立即返回空值。BlockingQueue的插入和获取操作是阻塞式的,如果队列为空,那么获取操作将一直阻塞队列中有一个可用的元素;如果队列已满,那么插入操作将一直阻塞。

Sorted容器

ConcurrentSkipListMap用于替代SortedMap,ConcurrentSkipListSet用于替代SortedSet。TreeMap和TreeSet分别实现了SortedMap和SortedSet。By the way,既然是"sorted",那么这类容器的元素就必须实现Comparable接口。

【转载】Java容器的线程安全的更多相关文章

  1. [转载]java自带线程池和队列详细讲解

    FROM:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中 ...

  2. java容器的线程安全性

    参考:https://www.cnblogs.com/yjd_hycf_space/p/7760248.html 线程安全的: Vector HashTable StringBuffer 线程不安全的 ...

  3. [转载]Java线程的两种实现方式

    转载:http://baijiahao.baidu.com/s?id=1602265641578157555&wfr=spider&for=pc 前言 线程是程序的一条执行线索,执行路 ...

  4. java容器中 哪些是线程安全的

    容器中线程安全的如:vectory,hashtable,非线程安全的如:hashmap,arrylist等.      对于原定义非线程的容器如:hashmap,arraylist可以使用Collec ...

  5. (转载)new Thread的弊端及Java四种线程池的使用

    介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端 执行一个异步任务你还只是如下new ...

  6. [ 转载 ] Java基础14--创建线程的两个方法

    http://www.cnblogs.com/whgw/archive/2011/10/03/2198506.html Java提供了线程类Thread来创建多线程的程序.其实,创建线程与创建普通的类 ...

  7. Java 容器(list, set, map)

    java容器类库的简化图: (虚线框表示接口, 实线框表示普通的类, 空心箭头表示特定的类实现了接口, 实心箭头表示某个类可以生成箭头所指的类对象) 继承Collection的主要有Set 和 Lis ...

  8. Java - 容器详解

    一.ArrayList 长度可变数组,类似于c++ STL中的vector. 元素以线性方式连续存储,内部允许存放重复元素. 允许对元素进行随机的快速访问,但是向ArrayList中插入和删除元素的速 ...

  9. [转载] Java集合框架之小结

    转载自http://jiangzhengjun.iteye.com/blog/553191 1.Java容器类库的简化图,下面是集合类库更加完备的图.包括抽象类和遗留构件(不包括Queue的实现): ...

随机推荐

  1. IDEA 找不到包或者找不到符号的一些解决办法

    有时使用IDE导入项目后,启动时会发生找不到包或者找不到符号的情况,下面有一些处理方法 1.右键项目Maven→Reimport 2.IDEA窗口左上角File→Invalidate and Rest ...

  2. 《Python学习手册 第五版》 -第14章 迭代和推导

    承接上一章for循环的讲解,迭代和推导,是对for循环的一种深入的探索和扩展 本章重点内容 1.迭代 1)什么是迭代?都有哪些分类 2)常规的使用方法 3)多遍迭代器VS单遍迭代器 2.列表推导 1) ...

  3. Fiddler抓取https方法

    基本配置 菜单 -> menu -> tool -> https 勾选"捕获https连接" 勾选"捕获https流量" 勾选"检查 ...

  4. Educational Codeforces Round 81 (Rated for Div. 2) C. Obtain The String

    题目链接:http://codeforces.com/contest/1295/problem/C 题目:给定字符串s,t.  给定一个空串z,需要按照规则把z构造成 string z == stri ...

  5. [CQOI2015] 网络吞吐量 - 最大流,最短路

    在第i个点只能选A[i]次的情况下,能选出多少条1-n的最短路 Solution 我们造出最短路DAG,然后对每个点拆点限流,跑最大流即可 双向边警告!(有悖直觉 #include <bits/ ...

  6. 解决Oracle ORA-01033: ORACLE initialization or shutdown in progress错误 和 ORA-01589错误 要打开数据库则必须使用 RESETLOGS 或 NORESETLOGS 选项

    要打开数据库则必须使用 RESETLOGS 或 NORESETLOGS 选项 SQL> startupORACLE 例程已经启动. Total System Global Area  13533 ...

  7. Mac下Charles的安装和配置

    一.安装与破解 官网下载,破解方法参考其他,此处略 二.配置 1.电脑端安装 Charles 的根证书 注意:此时钥匙串默认为不信任,需设置为始终信任 2.配置代理:勾选enable transpre ...

  8. Selenium自动化发送163邮箱

    自动化发送163邮件 方法一: import time import datetime from selenium import webdriver from selenium.webdriver.s ...

  9. Django | Unable to get repr for <class 'django.db.models.query.QuerySet'>

    问题:在mysql中查询数据时,代码如下: skus = category.sku_set.filter(is_launched=True).order_by(sort_field) skus 取不到 ...

  10. socket编程(python)

    交互原理: 服务端和客户端通过底层socket接口编程通信,交互的信息都是通过byte字节形式传递,网络传输中不能保证信息完整传输有可能是分片传输,所以可能从缓冲区获取的信息需要分段拼接或拆分组合成一 ...