15-同步容器

同步容器

为什么会出现同步容器

在Java的集合框架中,主要有四大类别:List、Set、Queue、Map。

List、Set、Queue接口分别继承了Collection接口,Map本身是一个接口。注意Collection和Map是一个顶层接口,而List、Set、Queue则继承了Collection接口,分别代表数组、集合和队列这三大类容器。像ArrayList、LinkedList都是实现了List接口,HashSet实现了Set接口,而Deque(双向队列:允许在队首、队尾进行入队和出队操作)继承了Queue接口,PriorityQueue实现了Queue接口。另外LinkedList(实际上是双向链表)实现了了Deque接口。

像ArrayList、LinkedList、HashMap这些容器都是非线程安全的。如果有多个线程并发地访问这些容器时,就会出现问题。因此,在编写程序时,必须要求程序员手动地在任何访问到这些容器的地方进行同步处理,这样导致在使用这些容器的时候非常地不方便。所以,Java提供了同步容器供用户使用。

java中的同步容器类

在Java中,同步容器主要包括2类:

1)Vector、Stack、HashTable

2)Collections工具类中提供的静态工厂方法创建的类

Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。

Stack也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是继承于Vector类。

HashTable实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。

Collections是一个工具类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类:

 public static Collection synchronizedCollention(Collection c)
 public static List synchronizedList(list l)
 public static Map synchronizedMap(Map m)
 public static Set synchronizedSet(Set s)
 public static SortedMap synchronizedSortedMap(SortedMap sm)
 public static SortedSet synchronizedSortedSet(SortedSet ss)

Collections同步工具类

为了创建线程安全且由ArrayList支持的List,可以使用如下代码:

List list = Collection.synchronizedList(new ArrayList());

注意,ArrayList实例马上封装起来,不存在对未同步的ArrayList的直接引用(即直接封装匿名实例)。这是一种最安全的途径。如果另一个线程可以直接引用ArrayList实例,它可以执行非同步修改。

下面给出一段多线程中安全遍历集合元素的示例。我们使用Iterator逐个扫描List中的元素,在多线程环境中,当遍历当前集合中的元素时,一般希望阻止其他线程添加或删除元素。安全遍历的实现方法如下:

public class Test {

    public static void main(String[] args) {
        // 为了安全起见,仅使用同步列表的一个引用,这样可以确保控制了所有访问  
        // 集合必须同步化,这里是一个List  
        List<String> wordList = Collections.synchronizedList(new ArrayList<String>());  

        //wordList中的add方法是同步方法,会获取wordList实例的对象锁  
        wordList.add("Iterators");  
        wordList.add("require");  
        wordList.add("special");  
        wordList.add("handling");  

        // 获取wordList实例的对象锁,  
        // 迭代时,阻塞其他线程调用add或remove等方法修改元素  
        synchronized (wordList) {  
            Iterator<String> iter = wordList.iterator();  
            while (iter.hasNext()) {
                String s = iter.next();  
                System.out.println("found string: " + s + ", length=" + s.length());  
            }
        }
    }
}

同步容器的缺陷

从同步容器的具体实现源码可知,同步容器中的方法采用了synchronized进行同步,那么很显然,这必然会影响到执行性能,另外,同步容器就一定是真正地完全线程安全吗?不一定,这个在下面会讲到。

我们首先来看一下传统的非同步容器和同步容器的性能差异,我们以ArrayList和Vector为例:

性能问题

我们先通过一个例子看一下Vector和ArrayList在插入数据时性能上的差异:

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        Vector<Integer> vector = new Vector<Integer>();

        long start = System.currentTimeMillis();
        for(int i=0; i<100000; i++)
            list.add(i);
        long end = System.currentTimeMillis();
        System.out.println("ArrayList进行100000次插入操作耗时:" + (end - start) + "ms");

        start = System.currentTimeMillis();
        for(int i=0; i<100000; i++)
            vector.add(i);
        end = System.currentTimeMillis();
        System.out.println("Vector进行100000次插入操作耗时:" + (end - start) + "ms");
    }
}

执行结果:

进行同样多的插入操作,Vector的耗时是ArrayList的两倍。这只是其中的一方面性能问题上的反映。

另外,由于Vector中的add方法和get方法都进行了同步,因此,在有多个线程进行访问时,如果多个线程都只是进行读取操作,那么每个时刻就只能有一个线程进行读取,其他线程便只能等待,这些线程必须竞争同一把锁。

同步容器真的是安全的吗

也有人认为Vector中的方法都进行了同步处理,那么一定就是线程安全的,事实上这可不一定。看下面这段代码:

public class Test {

    public static void main(String[] args) {

        final Vector<Integer> vector = new Vector<Integer>();

        while(true) {
            for(int i=0; i<10; i++)
                vector.add(i);

            Thread thread1 = new Thread() {
                public void run() {
                    for(int i=0; i<vector.size(); i++)
                        vector.remove(i);
                };
            };
            Thread thread2 = new Thread() {
                public void run() {
                    for(int i=0; i<vector.size(); i++)
                        vector.get(i);
                };
            };

            thread1.start();
            thread2.start();

            while(Thread.activeCount() > 10)   {

            }
        }
    }
}

执行结果:

正如大家所看到的,这段代码报错了:数组下标越界。

也许有朋友会问:Vector是线程安全的,为什么还会报这个错?很简单,对于Vector,虽然能保证每一个时刻只能有一个线程访问它,但是不排除这种可能:

当某个线程在某个时刻执行这句时:

for(int i=0; i<vector.size(); i++)
    vector.get(i);

假若此时vector的size方法返回的是10,i的值为9。然后另外一个线程执行了这句:

for(int i=0; i<vector.size(); i++)
    vector.remove(i);

将下标为9的元素删除了。那么通过get方法访问下标为9的元素肯定就会出问题了。

因此为了保证线程安全,必须在方法调用端做额外的同步措施,如下面所示:

public class Test {

    public static void main(String[] args) {

        final Vector<Integer> vector = new Vector<Integer>();

        while(true) {
            for(int i=0; i<10; i++)
                vector.add(i);

            Thread thread1 = new Thread() {
                public void run() {
                    synchronized (Test.class) { // 额外的同步
                        for(int i=0; i<vector.size(); i++)
                            vector.remove(i);
                    }
                };
            };
            Thread thread2 = new Thread() {
                public void run() { 
                    synchronized (Test.class) { // 额外的同步
                        for(int i=0; i<vector.size(); i++)
                            vector.get(i);
                    }
                };
            };

            thread1.start();
            thread2.start();

            while(Thread.activeCount() > 10)   {

            }
        }
    }
}

这个例子也说明了:由两个原子操作组成的复合操作不再是原子操作,如果需要保持原子性,则需要进行额外的同步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章