Java中的集合和线程安全

经过Java指南咱们知道Java集合框架(Collection Framework)如何为并发服务,咱们应该如何在单线程和多线程中使用集合(Collection)。
话题有点高端,咱们不是很好理解。因此,我会尽量的描述的简单点。经过这篇指南,你将会对Java集合由更深刻的了解,并且我敢保证,这会对你的平常编码很是有用。java

1. 为何大多数的集合类不是线程安全的?

你注意到了吗?为何多数基本集合实现类都不是线程安全的?好比:ArrayList, LinkedList, HashMap, HashSet, TreeMap, TreeSet等等。事实上,全部的集合类(除了Vector和HashTable之外)在java.util包中都不是线程安全的,只遗留了两个实现类(Vector和HashTable)是线程安全的为何?
缘由是:线程安全消耗十分昂贵!
你应该知道,Vector和HashTable在Java历史中,很早就出现了,最初的时候他们是为线程安全设计的。(若是你看了源码,你会发现这些实现类的方法都被synchronized修饰)并且很快的他们在多线程中性能表现的很是差。如你所知的,同步就须要锁,有锁就须要时间来监控,因此就下降了性能。
这就是为何新的集合类没有提供并发控制,为了保证在单线程中提供最大的性能。
下面测试的程序验证了Vector和ArrayList的性能,两个类似的集合类(Vector是线程安全,ArrayList非线程安全)算法

import java.util.*;
 
/**
 * This test program compares performance of Vector versus ArrayList
 * @author www.codejava.net
 *
 */
public class CollectionsThreadSafeTest {
 
    public void testVector() {
        long startTime = System.currentTimeMillis();
 
        Vector<Integer> vector = new Vector<>();
 
        for (int i = 0; i < 10_000_000; i++) {
            vector.addElement(i);
        }
 
        long endTime = System.currentTimeMillis();
 
        long totalTime = endTime - startTime;
 
        System.out.println("Test Vector: " + totalTime + " ms");
 
    }
 
    public void testArrayList() {
        long startTime = System.currentTimeMillis();
 
        List<Integer> list = new ArrayList<>();
 
        for (int i = 0; i < 10_000_000; i++) {
            list.add(i);
        }
 
        long endTime = System.currentTimeMillis();
 
        long totalTime = endTime - startTime;
 
        System.out.println("Test ArrayList: " + totalTime + " ms");
 
    }
 
    public static void main(String[] args) {
        CollectionsThreadSafeTest tester = new CollectionsThreadSafeTest();
 
        tester.testVector();
 
        tester.testArrayList();
 
    }
 
}

经过为每一个集合添加1000万个元素来测试性能,结果以下:数组

Test Vector: 9266 ms
Test ArrayList: 4588 ms

如你所看到的,在至关大的数据操做下,ArrayList速度差很少是Vector的2倍。你也拷贝上述代码本身感觉下。安全

2.快速失败迭代器(Fail-Fast Iterators)

在使用集合的时候,你也要了解到迭代器的并发策略:Fail-Fast Iterators
看下之后代码片断,遍历一个String类型的集合:多线程

List<String> listNames = Arrays.asList("Tom", "Joe", "Bill", "Dave", "John");
 
Iterator<String> iterator = listNames.iterator();
 
while (iterator.hasNext()) {
    String nextName = iterator.next();
    System.out.println(nextName);
}

这里咱们使用了Iterator来遍历list中的元素,试想下listNames被两个线程共享:一个线程执行遍历操做,在尚未遍历完成的时候,第二线程进行修改集合操做(添加或者删除元素),你猜想下这时候会发生什么?
遍历集合的线程会马上抛出异常“ConcurrentModificationException”,因此称之为:快速失败迭代器(随便翻的哈,没那么重要,理解就OK)
为何迭代器会如此迅速的抛出异常?
由于当一个线程在遍历集合的时候,另外一个在修改遍历集合的数据会很是的危险:集合可能在修改后,有更多元素了,或者减小了元素又或者一个元素都没有了。因此在考虑结果的时候,选择抛出异常。并且这应该尽量早的被发现,这就是缘由。(反正这个答案不是我想要的~)并发

下面这段代码演示了抛出:ConcurrentModificationException框架

import java.util.*;
 
/**
 * This test program illustrates how a collection's iterator fails fast
 * and throw ConcurrentModificationException
 * @author www.codejava.net
 *
 */
public class IteratorFailFastTest {
 
    private List<Integer> list = new ArrayList<>();
 
    public IteratorFailFastTest() {
        for (int i = 0; i < 10_000; i++) {
            list.add(i);
        }
    }
 
    public void runUpdateThread() {
        Thread thread1 = new Thread(new Runnable() {
 
            public void run() {
                for (int i = 10_000; i < 20_000; i++) {
                    list.add(i);
                }
            }
        });
 
        thread1.start();
    }
 
 
    public void runIteratorThread() {
        Thread thread2 = new Thread(new Runnable() {
 
            public void run() {
                ListIterator<Integer> iterator = list.listIterator();
                while (iterator.hasNext()) {
                    Integer number = iterator.next();
                    System.out.println(number);
                }
            }
        });
 
        thread2.start();
    }
 
    public static void main(String[] args) {
        IteratorFailFastTest tester = new IteratorFailFastTest();
 
        tester.runIteratorThread();
        tester.runUpdateThread();
    }
}

如你所见,在thread1遍历list的时候,thread2执行了添加元素的操做,这时候异常被抛出。
须要注意的是,使用iterator遍历list,快速失败的行为是为了让我更早的定位问题所在。咱们不该该依赖这个来捕获异常,由于快速失败的行为是没有保障的。这意味着若是抛出异常了,程序应该马上终止行为而不是继续执行。
如今你应该了解到了ConcurrentModificationException是如何工做的,并且最好是避免它。性能

同步封装器

至此咱们明白了,为了确保在单线程环境下的性能最大化,因此基础的集合实现类都没有保证线程安全。那么若是咱们在多线程环境下如何使用集合呢?
固然咱们不能使用线程不安全的集合在多线程环境下,这样作会致使出现咱们指望的结果。咱们能够手动本身添加synchronized代码块来确保安全,可是使用自动线程安全的线程比咱们手动更为明智。
你应该已经知道,Java集合框架提供了工厂方法建立线程安全的集合,这些方法的格式以下:测试

Collections.synchronizedXXX(collection)

这个工厂方法封装了指定的集合并返回了一个线程安全的集合。XXX能够是Collection、List、Map、Set、SortedMap和SortedSet的实现类。好比下面这段代码建立了一个线程安全的列表:编码

List<String> safeList = Collections.synchronizedList(new ArrayList<>());

若是咱们已经拥有了一个线程不安全的集合,咱们能够经过如下方法来封装成线程安全的集合:

Map<Integer, String> unsafeMap = new HashMap<>();
Map<Integer, String> safeMap = Collections.synchronizedMap(unsafeMap);

如你锁看到的,工厂方法封装指定的集合,返回一个线程安全的结合。事实上接口基本都一直,只是实现上添加了synchronized来实现。因此被称之为:同步封装器。后面集合的工做都是由这个封装类来实现。

提示:
在咱们使用iterator来遍历线程安全的集合对象的时候,咱们仍是须要添加synchronized字段来确保线程安全,由于Iterator自己并非线程安全的,请看代码以下:

List<String> safeList = Collections.synchronizedList(new ArrayList<>());
 
// adds some elements to the list
 
Iterator<String> iterator = safeList.iterator();
 
while (iterator.hasNext()) {
    String next = iterator.next();
    System.out.println(next);
}

事实上咱们应该这样来操做:

synchronized (safeList) {
    while (iterator.hasNext()) {
        String next = iterator.next();
        System.out.println(next);
    }
}

同时提醒下,Iterators也是支持快速失败的。
尽管通过类的封装可保证线程安全,可是他们依然有着本身的缺点,具体见下面部分。

并发集合

一个关于同步集合的缺点是,用集合的自己做为锁的对象。这意味着,在你遍历对象的时候,这个对象的其余方法已经被锁住,致使其余的线程必须等待。其余的线程没法操做当前这个被锁的集合,只有当执行的线程释放了锁。这会致使开销和性能较低。
这就是为何jdk1.5+之后提供了并发集合的缘由,由于这样的集合性能更高。并发集合类并放在java.util.concurrent包下,根据三种安全机制被放在三个组中。

  • 第一种为:写时复制集合:这种集合将数据放在一成不变的数组中;任何数据的改变,都会从新建立一个新的数组来记录值。这种集合被设计用在,读的操做远远大于写操做的情景下。有两个以下的实现类:CopyOnWriteArrayList 和 CopyOnWriteArraySet.
    须要注意的是,写时复制集合不会抛出ConcurrentModificationException异常。由于这些集合是由不可变数组支持的,Iterator遍历值是从不可变数组中出来的,不用担忧被其余线程修改了数据。

  • 第二种为:比对交换集合也称之为CAS(Compare-And-Swap)集合:这组线程安全的集合是经过CAS算法实现的。CAS的算法能够这样理解:
    为了执行计算和更新变量,在本地拷贝一份变量,而后不经过获取访问来执行计算。当准备好去更新变量的时候,他会跟他以前的开始的值进行比较,若是同样,则更新值。
    若是不同,则说明应该有其余的线程已经修改了数据。在这种状况下,CAS线程能够从新执行下计算的值,更新或者放弃。使用CAS算法的集合有:ConcurrentLinkedQueue and ConcurrentSkipListMap.
    须要注意的是,CAS集合具备不连贯的iterators,这意味着自他们建立以后并非全部的改变都是重新的数组中来。同时他也不会抛出ConcurrentModificationException异常。

  • 第三种为:这种集合采用了特殊的对象锁(java.util.concurrent.lock.Lock):这种机制相对于传统的来讲更为灵活,能够以下理解:
    这种锁和经典锁同样具备基本的功能,但还能够再特殊的状况下获取:若是当前没有被锁、超时、线程没有被打断。
    不一样于synchronization的代码,当方法在执行,Lock锁一直会被持有,直到调用unlock方法。有些实现经过这种机制把集合分为好几个部分来提供并发性能。好比:LinkedBlockingQueue,在队列的开后和结尾,因此在添加和删除的时候能够同时进行。
    其余使用了这种机制的集合有:ConcurrentHashMap 和绝多数实现了BlockingQueue的实现类
    一样的这一类的集合也具备不连贯的iterators,也不会抛出ConcurrentModificationException异常。

咱们来总结下今天咱们所学到的几个点:

  1. 大部分在java.util包下的实现类都没有保证线程安全为了保证性能的优越,除了Vector和Hashtable之外。
  2. 经过Collection能够建立线程安全类,可是他们的性能都比较差。
  3. 同步集合既保证线程安全也在给予不一样的算法上保证了性能,他们都在java.util.concurrent包中。 

翻译来自:
https://www.codejava.net/java-core/collections/understanding-collections-and-thread-safety-in-java

相关文章
相关标签/搜索