Java多线程编程--(11)学习Java5.0 并发编程包--支持并发的集合类

来源:互联网 发布:云计算开发工程师 编辑:程序博客网 时间:2024/05/22 14:48

记得刚接触程序设计时,书上就说:程序 = 数据结构 + 算法! 算法通俗来讲就是一些分析模型的代码实现,比如我们的排序算法等。数据结构,就是组织存储数据的方式,Java语言通过集合类为我们提供了基本的数据结构的支持。这些类在java.util包中。比如顺序存储结构:ArrayList,LinkedList,Vector;集合:HashSet,TreeSet;Map:Hashtable,HashMap,TreeMap 等。其中Vector、Hashtable属于最早期提供的重量级数据结构,集合类内部就直接支持同步(利用关键字synchronized,效率很差),其余几种为轻量级数据结构,集合类本身的方法不支持并发,如果用户要在并发环境中使用,需要自己进行并发控制。

因为在实际应用中,几乎所有数据结构都会涉及到并发操作,也就是如果用户使用了轻量级数据结构,并发控制的代码是不可少的。所以在工具类Collections中,Java又通过静态代理的形式提供了轻量级集合类一一对应的可并发集合类(我们可以想象就是在原始方法周围加了synchronized同步块而已),这虽然简化了程序员的工作,但在大并发的情况下,数据结构的性能真是苦不堪言!

为了彻底改善Java中数据结构对高并发的支持,在java.util.concurrent并发包中,Java又提供了一些支持并发并且性能优越的集合类,我们这里就使用和原理进行简单介绍:

【ConcurrentHashMap】

支持并发操作的Map数据结构,我们来看看如何构造ConcurrentHashMap对象:

package cn.test;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapTest {public static void main(String[] args) {/** *  3个参数分别为: *  int initialCapacity 初始大小,  *  float loadFactor 当容量达到这个界限开始扩容,  *  int concurrencyLevel 最大支持并发的数量 */Map<String, String> map1 = new ConcurrentHashMap<String, String>(32, 0.8f, 16);/** * 使用默认构造函数,initialCapacity、loadFactor、concurrencyLevel * 均采用默认值,为 16, 0.75, 16 */Map<String, String> map2 = new ConcurrentHashMap<String, String>();}}

上例,我们看,创建ConcurrentHashMap比创建HashMap多了一个可选参数,concurrencyLevel,即代表当前结构最大支持的并发数量。那么ConcurrentHashMap的原理是什么呢?我们都知道HashMap的内部实现就是一个Entry数组,如果我们要通过synchronized进行并发控制,其实是锁定了整个Entry数组,对于ConcurrentHashMap,其内部是一个大小为concurrencyLevel大小的Segment数组,键值对都是存储在Segment对象上的。每个线程操作,会先找到要操作的Segment对象,然后只是锁定这个Segment对象!因此,ConcurrentHashMap理论上最多支持concurrencyLevel个线程并发!上例得到ConcurrentHashMap对象后,其余操作和普通Map是一样的。

对于ConcurrentHashMap,我们和HashMap做一个性能比较测试,100个线程并发,每个线程将10000个数据存储到Map中,代码和结果如下;

package cn.test;import java.util.Collections;import java.util.HashMap;import java.util.Map;import java.util.Random;import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapTest {public static void main(String[] args) throws InterruptedException {// 填充一份数据char[] chars = "abcdefghijklmnopqrstuvwxyz1234567890".toCharArray();int length = chars.length;Random random = new Random();final String[][] names = new String[100][10000];for(int i=0;i<100;i++){for(int j=0;j<10000;j++){int randomIndex0 = (int)(random.nextDouble() * length);int randomIndex1 = (int)(random.nextDouble() * length);int randomIndex2 = (int)(random.nextDouble() * length);int randomIndex3 = (int)(random.nextDouble() * length);int randomIndex4 = (int)(random.nextDouble() * length);names[i][j] = new String(new char[]{chars[randomIndex0],chars[randomIndex1],chars[randomIndex2],chars[randomIndex3],chars[randomIndex4],});}}// 放置最后完成时间的数据结构:final MyBigValSet bigValSet = new MyBigValSet();// 对可并发的HashMap进行操作final Map<String, String> oldMap = Collections.synchronizedMap(new HashMap<String, String>());// 起100个线程,将数据存放到oldMap中long startTime = System.currentTimeMillis();System.out.println("开始时间时:" + startTime);for(int i=0; i<100; i++){final int index = i;new Thread(new Runnable(){@Overridepublic void run() {String[] values = names[index];for(String value : values){oldMap.put(value, value);}bigValSet.addValue(System.currentTimeMillis());}}).start();}// 主线程休息,等待完成Thread.sleep(3000);System.out.println("Old Map 耗时 : " + (bigValSet.getValue() - startTime) + " ms ");// 放置最后完成时间的数据结构:final MyBigValSet bigValSet2 = new MyBigValSet();// 对可并发的HashMap进行操作,采用默认支持50个线程并发的结构final Map<String, String> newMap = new ConcurrentHashMap<String, String>(16, 0.75f, 50);// 起100个线程,将数据存放到oldMap中long startTime2 = System.currentTimeMillis();System.out.println("开始时间时:" + startTime);for(int i=0; i<100; i++){final int index = i;new Thread(new Runnable(){@Overridepublic void run() {String[] values = names[index];for(String value : values){newMap.put(value, value);}bigValSet2.addValue(System.currentTimeMillis());}}).start();}// 主线程休息,等待完成Thread.sleep(3000);System.out.println("New Map 耗时 : " + (bigValSet2.getValue() - startTime2) + " ms ");}// 自定义一个数据结构,放置最大的值。支持并发static class MyBigValSet{long originalValue = Long.MIN_VALUE;public synchronized void addValue(long value){if(value > originalValue){originalValue = value;}}public long getValue() {return originalValue;}}}

结果为:

开始时间时:1345172746421Old Map 耗时 : 1047 ms 开始时间时:1345172746421New Map 耗时 : 578 ms 

经过多次比较,对于上述这种并发情况,ConcurrentHashMap在支持50个线程并发的前提下,比Collections工具类提供的并发HashMap有大概1倍的效率提升!

【CopyOnWriteArrayList】

这是一个线程安全、并且读操作时无锁的ArrayList。创建CopyOnWriteArrayList对象是,初始是创建一个长度为0的Object数组。对于增加add(E),CopyOnWriteArrayList内部通过ReentrantLock来进行线程并发控制,并且先创建一个新的长度加1的数组,将最元素放置在最后,最后切换引用即可。对于删除元素remove(E),内部同样通过ReentrantLock来控制并发,并且创建一个长度减1的新数组,然后遍历原数组,如果找到找到这个元素,则剔除这个元素,切换引用,返回true,如果没有找到,则直接返回false。对于get(int),CopyOnWriteArrayList没有进行任何加锁控制,因为这个方法有时可能会返回脏数据,对于写少读多并且脏数据影响不大的场景,比较适合!

CopyOnWriteArrayList对于Iterator的支持也有所改进,我们知道,ArrayList在通过Iterator进行遍历时,不能对ArrayList进行写操作(包括插入和删除元素),否则会报ConcurrentModificationException,我们看一下:

package cn.test;import java.util.ArrayList;import java.util.Iterator;import java.util.List;public class ArrayListTest {public static void main(String[] args) {List<String> objs = new ArrayList<String>();objs.add("A");objs.add("B");objs.add("C");// 先得到到这个ArrayList的迭代器对象Iterator<String> iterator = objs.iterator();// 向这个ArrayList对象中插入元素objs.add("D");// 利用迭代器对象遍历这个ArrayList,在调用next方法时会报异常while(iterator.hasNext()){System.out.println(iterator.next());}}}

异常信息为:

Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.AbstractList$Itr.checkForComodification(Unknown Source)at java.util.AbstractList$Itr.next(Unknown Source)at cn.test.ArrayListTest.main(ArrayListTest.java:22)


CopyOnWriteArrayList调用iterator方法会返回COWIterator对象,并保存一个当前数组的快照,在使用迭代器遍历时,遍历的是这个数组快照,永远不会抛ConcurrentModificationException:

package cn.test;import java.util.Iterator;import java.util.List;import java.util.concurrent.CopyOnWriteArrayList;public class CopyOnWriteArrayListTest {public static void main(String[] args) {List<String> objs = new CopyOnWriteArrayList<String>();objs.add("A");objs.add("B");objs.add("C");// 先得到到这个CopyOnWriteArrayList的迭代器对象Iterator<String> iterator = objs.iterator();// 向这个CopyOnWriteArrayList对象中插入元素objs.add("D");// 利用迭代器对象遍历这个CopyOnWriteArrayListwhile(iterator.hasNext()){System.out.println(iterator.next());}}}


输出结果为:没有“D”,因为我们在获取迭代器对象时,数据结构中没有这个元素:

ABC

上述就是CopyOnWriteArrayList的实现原理,经测试,在读多写少的高并发环境中,CopyOnWriteArrayList的性能会远远好于ArrayList。

【CopyOnWriteArraySet】

CopyOnWriteArraySet 是直接基于CopyOnWriteArrayList实现的,在调用add(E)操作时,会先遍历数组,看这个元素是否存在,不存在才插入成功,否则插入失败!

【BlockingQueue】

BlockingQueue阻塞队列有两种实现方式,ArrayBlockingQueue一个基于数组的、先进先出、线程安全的集合类。其特色是可实现指定时间内的阻塞读写,并且容量是固定的!其内部是通过Lock和Condition来实现线程的互斥与同步!另一种BlockingQueue是LinkedBlockingQueue,其是一个基于链表的,容量大小没有限制的阻塞队列!我们来看看BlockingQueue的一些相关方法:

package cn.test;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;import java.util.concurrent.TimeUnit;public class BlockingQueueTest {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> bq = new ArrayBlockingQueue<String>(10);bq.add("DDD");// 立即返回,如果ArrayBlockingQueue已满,则直接抛异常bq.offer("DDD");// 立即返回,如果ArrayBlockingQueue已满,则返回falsebq.offer("DDD", 1, TimeUnit.SECONDS); // 如果ArrayBlockQueue已满,则阻塞等待1秒,仍然没有空间则抛超时异常bq.put("DDD"); // 如果ArrayBlockingQueue已满,则阻塞,直到有可用空间可以插入元素String v1 = bq.remove(); // 立即返回,如果没有元素,则抛异常String v2 = bq.poll(); // 立即返回,如果没有元素,则返回nullString v3 = bq.poll(1, TimeUnit.SECONDS); // 如果没有元素,则阻塞1秒,仍然没有元素则抛超时异常String v4 = bq.take(); // 如果没有元素,则阻塞,直到有元素可以取才返回}}


【SynchronousQueue】

也是一个实现了接口BlockingQueue的阻塞队列,其有一些特殊特性。SynchronousQueue可以认为只有一个虚拟空间的数据结构,在这个阻塞队列上我们只能使用put和take方法!并且调用put方法一定会阻塞,直到一个take方法调用后,put方法调用才会返回!调用take方法的线程将put方法线程放置的元素得到后,两者继续执行各自其余逻辑:

package cn.test;import java.util.concurrent.SynchronousQueue;public class SynchronousQueueTest {public static void main(String[] args) {final SynchronousQueue<String> sq = new SynchronousQueue<String>();// 放置元素的线程new Thread(new Runnable(){@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " 准备往SynchronousQueue中添加元素!");try {// 此处注意,调用put,即使当前SynchronousQueue是空的,也会阻塞,知道一个线程调用take,方法才会返回!sq.put("ABC");} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 放置元素 ABC 成功!");}}).start();// 读取元素的线程new Thread(new Runnable(){@Overridepublic void run() {// 读取元素的线程先休息1秒钟try {Thread.sleep(1000);} catch (InterruptedException e1) {e1.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 准备从SynchronousQueue中读取元素!");try {String value = sq.take();System.out.println(Thread.currentThread().getName() + " 读取到元素 " + value);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}}

输出结果为:

Thread-0 准备往SynchronousQueue中添加元素!Thread-1 准备从SynchronousQueue中读取元素!Thread-1 读取到元素 ABCThread-0 放置元素 ABC 成功!


实际应用中,先进先出的阻塞队列还是有很多需要的地方,并且BlockingQueue还提供了另外两种实现:PriorityBlockingQueue 和 DelayQueue,此处不再详细介绍,如果需要可查阅JDK文档!

 

这一篇是《Java多线程编程》系列的最后一篇,作为自己写博客一个月(2012-7-20 到 2012-8-20)的纪念吧!

原创粉丝点击