多线程之同步类与并非容器

  一:为什么会出现同步容器?

    平时我们使用的ArrayList,HashSet,HashMap都是非线程安全的,如果有多个线程同时操作集合,就会出现线程安全问题。

        下面举个例子来说明一下,为什么是是非线程安全的。

public class ArrayListDemo {

    public static void main(String[]args){
        final HashMap<String,String> map=new HashMap<String, String>();
        map.put("A", "A");
        Thread t1=new Thread(new Runnable() {
            public void run() {
                map.put("A", "B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1");
        Thread t2=new Thread(new Runnable() {
            public void run() {
                System.out.println(map.get("A"));
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

  运行结果有时候为A,有时候为B,出现这种不确定的结果。所以在多线程环境下就需要对集合容器进行同步。

  二: Java中的同步容器主要有以下两种

  (1)Vector,HashTable,stack

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

  Vector和ArrayList类似,但是Vector里面的方法都加了同步synchronized,从而实现了线程安全。源码如下:

     

   而Collections主要是传入一个非线程安全的集合后,返回一个线程安全的集合。

  

  三:同步容器的缺陷

  同步容器的方法都使用了synchronized关键字进行同步,这必然会影响到性能问题。下面写个例子测试一下。

public class VectorDemo {

    public static void main(String []args){
        
        ArrayList<Integer> list = new ArrayList<Integer>();
        long start = System.currentTimeMillis();
        for(int i=0;i<1000000;i++)
            list.add(i);
        for(int i=0;i<list.size();i++)
            list.get(i);
        long end = System.currentTimeMillis();
        System.out.println("ArrayList进行10000次插入与读取操作耗时:"+(end-start)+"ms");
        
        Vector<Integer> vector = new Vector<Integer>();
        start = System.currentTimeMillis();
        for(int i=0;i<1000000;i++)
            vector.add(i);
        for(int i=0;i<vector.size();i++)
            vector.get(i);
        end = System.currentTimeMillis();
        System.out.println("Vector进行10000次插入与读取操作耗时:"+(end-start)+"ms");
        
    }
}

  显示结果如下:

  

  另外,由于vector中add方法与get方法都加了synchronized进行同步,所以在多线程情况,同一时间只有一个线程获得锁,其他线程只能等待,竞争同一把锁。

  四:并发容器之ConcurrentMap与CopyOnWrite

  ConcurrentMap代替HashTable。

  CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set。

  (1)ConcurrentMap

  HashTable容器使用了synchronized关键字来同步(get与put方法都使用了synchronized),在多线程情况下,效率比较十分低下,如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素。HashTable是根据散列值分段存储的,在同步的时候锁住了所有的段,而ConcurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。如下图所示:

  

  ConcurrentHashMap是一种细粒度的加锁方式,当线程同时访问一个数据的时候,才会进入阻塞状态。当线程访问不同数据块的数据时候,线程之间使用不同的锁对象,所以不会进入阻塞状态,这样大大提高了性能。

  (2)CopyOnWrite  

  CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

  查看CopyOnWriteArrayList的读取与插入方法,源码如下:

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

  public E get(int index) {
        return get(getArray(), index);
    }

  添加的时候加了锁,读取的时候不加锁,所以当多线程同时往集合中添加元素的时候,会读取到旧的数据,因为写的时候不会锁住旧的容器。

  五:CopyOnWrite的缺点:

  内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。

  数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

   参考网址:http://blog.csdn.net/hechurui/article/details/49508473

        http://ifeve.com/java-copy-on-write/

        http://www.cnblogs.com/dolphin0520/p/3932905.html

原文地址:https://www.cnblogs.com/gdpuzxs/p/6724276.html