并发容器

本文深入探讨了Java中线程安全的集合实现,包括通过Collections工具类实现同步化的各种方法及其实现原理,对比了JDK1.5后新增的java.util.concurrent包中并发容器如CopyOnWriteArrayList和ConcurrentHashMap等的特性和适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://chenzehe.iteye.com/blog/1779990

Java在JDK1.5之前基本上对所有集合都实现了线程同步版本synchronized*,用集合工具类Collections即可得到,如下都为Collections中的方法:

static <T> Collection<T> 
 synchronizedCollection(Collection<T> c) 
          返回指定 collection 支持的同步(线程安全的)collection。 
static <T> List<T> 
 synchronizedList(List<T> list) 
          返回指定列表支持的同步(线程安全的)列表。 
static <K,V> Map<K,V> 
 synchronizedMap(Map<K,V> m) 
          返回由指定映射支持的同步(线程安全的)映射。 
static <T> Set<T> 
 synchronizedSet(Set<T> s) 
          返回指定 set 支持的同步(线程安全的)set。 
static <K,V> SortedMap<K,V> 
 synchronizedSortedMap(SortedMap<K,V> m) 
          返回指定有序映射支持的同步(线程安全的)有序映射。 
static <T> SortedSet<T> 
 synchronizedSortedSet(SortedSet<T> s) 
          返回指定有序 set 支持的同步(线程安全的)有序 set。 

其内部实现是在内部维护一个对应的集合类型,然后相应所有的操作都加上同步关键字synchronized,同步方法内再调用内部集合的方法,如下为synchronizedMap的实现:

http://xyiyy.iteye.com/blog/361905

不过在使用Iterator遍历对象时,仍必须实现同步化。因为这样的List使用iterator()方法返回的Iterator对象,并没有保证线程安全。一个实现遍历的例子如下:

List list = Collections.synchronizedList(new ArrayList());
...
synchronized(list)
{
	Iterator i = list.iterator();
	     while(i.hasNext())
	     {
	          foo(i.next());
	     }
}

J2SE5.0之后,新增了java.util.concurrent这个包,其中包括一些确保线程安全的Collection类,如ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet等。这些对象与先前介绍的Map、List、Set等对象是相同的,不同的是增加了同步的功能,而且依对象存取时的需求不同而有不同的同步化实现,以同时确保效率和安全性。例如ConcurrentHashMap对Hash Table中不同的区段进行同步化。

使用Collections.synchronizedList/Set/Map可能获得一个线程安全的容器,但是效率并不是最高

对于Map而言,推荐使用ConcurrentHashMap

ConcurrentlinkedQueue的效率也高于BlockingQueue,BlockingQueue是一个接口,对应有两个具体的类,ArrayBlockingQueue和LinkedBlockingQueue


LinkedBlockingDeque没有进行读写锁的分离,同一时间只能有一个线程访问,因此,效率低于LinkedBlockingQueue

这是因为Concurrent系列都通过冗余空间获得了近乎无锁的效率

1. CopyOnWriteArrayList

适用于大多是读,而写很少的环境下。

CopyOnWriteArrayList在进行数据修改时,都不会对数据进行锁定,每次修改时, 先拷贝整个数组,然后修改其中的一些元素,完成上述操作后,替换整个数组的指针。
对CopyOnWriteArrayList进行读取时,也不进行数据锁定,直接返回需要查询的数据,如果需要返回整个数组,那么会将整个数组拷贝一份,再返回,保证内部array在任何情况下都是只读的。

应用场景
正因为上述读写特性,如果需要频繁对CopyOnWriteArrayList进行修改,而很少读取的话,那么会严重降低系统性能。
因为没有锁的干预,所以CopyOnWriteArrayLIst在少量修改,频繁读取的场景下,有很好的并发性能。

在那些遍历操作大大地多于插入或移除操作的并发应用程序中,一般用 CopyOnWriteArrayList 类替代 ArrayList 。如果是用于存放一个侦听器(listener)列表,例如在AWT或Swing应用程序中,或者在常见的JavaBean中,那么这种情况很常见(相关的CopyOnWriteArraySet 使用一个 CopyOnWriteArrayList 来实现 Set 接口) 。

如果您正在使用一个普通的 ArrayList 来存放一个侦听器列表,那么只要该列表是可变的,而且可能要被多个线程访问,您 就必须要么在对其进行迭代操作期间,要么在迭代前进行的克隆操作期间,锁定整个列表,这两种做法的开销都很大。当对列表执行会引起列表发生变化的操作时, CopyOnWriteArrayList 并不是为列表创建一个全新的副本,它的迭代器肯定能够返回在迭代器被创建时列表的状态,而不会抛出 ConcurrentModificationException 。在对列表进行迭代之前不必克隆列表或者在迭代期间锁 定列表,因为迭代器所看到的列表的副本是不变的。换句话说, CopyOnWriteArrayList 含有对一个不可变数组的一个可变的引用,因此,只要保留好那个引用,您就可以获得不可变的线程安全性的好处,而且不用锁 定列表。


package javatest;

import java.io.*;
import java.io.FileInputStream;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;

import info.*;



public class test {
	
	 class writer implements Runnable {
		List<Integer> ls;
		public writer(List<Integer> ls) {
			this.ls = ls;
		}
		public void run(){
			for (int i = 6; i < 12; ++i) {
				ls.add(i);
				System.out.println("add " + i);
				try {
				Thread.sleep(10);
				}
				catch(InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	class del implements Runnable {
		List<Integer> ls;
		public del(List<Integer> ls) {
			this.ls = ls;
		}
		public void run() {
			int i = 0;
			while (i < 6 && !ls.isEmpty()) {//unsafe
				++i;
				int len = ls.size();
				if (len >= 2) {
					System.out.println("rm " + ls.get(len - 2));
					ls.remove(len - 2);//unsafe
				}
				try {
					Thread.sleep(1000);
				}
				catch(InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public static void main(String[] args) {
		test t = new test();
		CopyOnWriteArrayList<Integer> ls = new CopyOnWriteArrayList<Integer>(Arrays.asList(1,2,3,4));
		
		new Thread(t.new writer(ls)).start();
		new Thread(t.new del(ls)).start();
		Iterator<Integer> it = ls.iterator();
		while (it.hasNext()) {
			System.out.println(it.next());
			//it.remove();
			try {
				Thread.sleep(1000);
			}
			catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}

创建迭代器时, 迭代器就初始化了当前数组的快照. 就算迭代期间进行了写操作, 也不会影响到迭代器中的snapshot数组. 所以CopyOnWriteArrayList返回的迭代器只反应迭代发生时CopyOnWriteArrayList对象所持有的集合, 迭代期间发生的改变不会反应出来.所以在用iterator遍历的时候不需要额外的同步操作,但是it.remove不是同步操作,如果第66行不被注释,会产生异常


http://ifeve.com/why-is-there-not-concurrent-arraylist-in-java-util-concurrent-package

问:JDK 5在java.util.concurrent里引入了ConcurrentHashMap,在需要支持高并发的场景,我们可以使用它代替HashMap。但是为什么没有ArrayList的并发实现呢?难道在多线程场景下我们只有Vector这一种线程安全的数组实现可以选择么?为什么在java.util.concurrent 没有一个类可以代替Vector呢?

答:我认为在java.util.concurrent包中没有加入并发的ArrayList实现的主要原因是:很难去开发一个通用并且没有并发瓶颈的线程安全的List。

像ConcurrentHashMap这样的类的真正价值(The real point / value of classes)并不是它们保证了线程安全。而在于它们在保证线程安全的同时不存在并发瓶颈。举个例子,ConcurrentHashMap采用了锁分段技术弱一致性的Map迭代器去规避并发瓶颈。

所以问题在于,像“Array List”这样的数据结构,你不知道如何去规避并发的瓶颈。拿contains() 这样一个操作来说,当你进行搜索的时候如何避免锁住整个list?

另一方面,Queue 和Deque (基于Linked List)有并发的实现是因为他们的接口相比List的接口有更多的限制,这些限制使得实现并发成为可能。

CopyOnWriteArrayList是一个有趣的例子,它规避了只读操作(如get/contains)并发的瓶颈,但是它为了做到这点,在修改操作中做了很多工作和修改可见性规则。 此外,修改操作还会锁住整个List,因此这也是一个并发瓶颈。所以从理论上来说,CopyOnWriteArrayList并不算是一个通用的并发List。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值