多线程之同步类与并非容器
一:为什么会出现同步容器?
平时我们使用的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