zoukankan      html  css  js  c++  java
  • 【转载】Java容器的线程安全

    转自: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接口。
  • 相关阅读:
    Elasticsearch Query DSL 整理总结(三)—— Match Phrase Query 和 Match Phrase Prefix Query
    Elasticsearch Query DSL 整理总结(二)—— 要搞懂 Match Query,看这篇就够了
    Elasticsearch Query DSL 整理总结(一)—— Query DSL 概要,MatchAllQuery,全文查询简述
    Elasticsearch Java Rest Client API 整理总结 (三)——Building Queries
    Elasticsearch date 类型详解
    python 历险记(五)— python 中的模块
    python 历险记(四)— python 中常用的 json 操作
    python 历险记(三)— python 的常用文件操作
    Elasticsearch Java Rest Client API 整理总结 (二) —— SearchAPI
    Elasticsearch Java Rest Client API 整理总结 (一)——Document API
  • 原文地址:https://www.cnblogs.com/YangtzeYu/p/6216511.html
Copyright © 2011-2022 走看看