并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue用法&&&&SynchronizedMap和ConcurrentHashMap的深入分析

来源:互联网 发布:aso优化 app下载 编辑:程序博客网 时间:2024/06/05 18:57

在Java多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列。Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。

注:什么叫线程安全?这个首先要明确。线程安全就是说多线程访问同一代码,不会产生不确定的结果。

LinkedBlockingQueue

由于LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。

复制代码
package cn.thread;import java.util.concurrent.BlockingQueue;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.LinkedBlockingQueue;/** * 多线程模拟实现生产者/消费者模型 *   * @author 林计钦 * @version 1.0 2013-7-25 下午05:23:11 */public class BlockingQueueTest2 {    /**     *      * 定义装苹果的篮子     *      */    public class Basket {        // 篮子,能够容纳3个苹果        BlockingQueue<String> basket = new LinkedBlockingQueue<String>(3);        // 生产苹果,放入篮子        public void produce() throws InterruptedException {            // put方法放入一个苹果,若basket满了,等到basket有位置            basket.put("An apple");        }        // 消费苹果,从篮子中取走        public String consume() throws InterruptedException {            // take方法取出一个苹果,若basket为空,等到basket有苹果为止(获取并移除此队列的头部)            return basket.take();        }    }    // 定义苹果生产者    class Producer implements Runnable {        private String instance;        private Basket basket;        public Producer(String instance, Basket basket) {            this.instance = instance;            this.basket = basket;        }        public void run() {            try {                while (true) {                    // 生产苹果                    System.out.println("生产者准备生产苹果:" + instance);                    basket.produce();                    System.out.println("!生产者生产苹果完毕:" + instance);                    // 休眠300ms                    Thread.sleep(300);                }            } catch (InterruptedException ex) {                System.out.println("Producer Interrupted");            }        }    }    // 定义苹果消费者    class Consumer implements Runnable {        private String instance;        private Basket basket;        public Consumer(String instance, Basket basket) {            this.instance = instance;            this.basket = basket;        }        public void run() {            try {                while (true) {                    // 消费苹果                    System.out.println("消费者准备消费苹果:" + instance);                    System.out.println(basket.consume());                    System.out.println("!消费者消费苹果完毕:" + instance);                    // 休眠1000ms                    Thread.sleep(1000);                }            } catch (InterruptedException ex) {                System.out.println("Consumer Interrupted");            }        }    }    public static void main(String[] args) {        BlockingQueueTest2 test = new BlockingQueueTest2();        // 建立一个装苹果的篮子        Basket basket = test.new Basket();        ExecutorService service = Executors.newCachedThreadPool();        Producer producer = test.new Producer("生产者001", basket);        Producer producer2 = test.new Producer("生产者002", basket);        Consumer consumer = test.new Consumer("消费者001", basket);        service.submit(producer);        service.submit(producer2);        service.submit(consumer);        // 程序运行5s后,所有任务停止//        try {//            Thread.sleep(1000 * 5);//        } catch (InterruptedException e) {//            e.printStackTrace();//        }//        service.shutdownNow();    }}
复制代码

ConcurrentLinkedQueue

ConcurrentLinkedQueue是Queue的一个安全实现.Queue中元素按FIFO原则进行排序.采用CAS操作,来保证元素的一致性。
LinkedBlockingQueue是一个线程安全的阻塞队列,它实现了BlockingQueue接口,BlockingQueue接口继承自java.util.Queue接口,并在这个接口的基础上增加了take和put方法,这两个方法正是队列操作的阻塞版本。

复制代码
package cn.thread;import java.util.concurrent.ConcurrentLinkedQueue;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ConcurrentLinkedQueueTest {    private static ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<Integer>();    private static int count = 2; // 线程个数    //CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。    private static CountDownLatch latch = new CountDownLatch(count);    public static void main(String[] args) throws InterruptedException {        long timeStart = System.currentTimeMillis();        ExecutorService es = Executors.newFixedThreadPool(4);        ConcurrentLinkedQueueTest.offer();        for (int i = 0; i < count; i++) {            es.submit(new Poll());        }        latch.await(); //使得主线程(main)阻塞直到latch.countDown()为零才继续执行        System.out.println("cost time " + (System.currentTimeMillis() - timeStart) + "ms");        es.shutdown();    }        /**     * 生产     */    public static void offer() {        for (int i = 0; i < 100000; i++) {            queue.offer(i);        }    }    /**     * 消费     *       * @author 林计钦     * @version 1.0 2013-7-25 下午05:32:56     */    static class Poll implements Runnable {        public void run() {            // while (queue.size()>0) {            while (!queue.isEmpty()) {                System.out.println(queue.poll());            }            latch.countDown();        }    }}
复制代码

运行结果:
costtime 2360ms

改用while (queue.size()>0)后
运行结果:
cost time 46422ms

结果居然相差那么大,看了下ConcurrentLinkedQueue的API原来.size()是要遍历一遍集合的,难怪那么慢,所以尽量要避免用size而改用isEmpty().

总结了下, 在单位缺乏性能测试下,对自己的编程要求更加要严格,特别是在生产环境下更是要小心谨慎。

SynchronizedMap和ConcurrentHashMap的深入分析

在开始之前,先介绍下Map是什么?

javadoc中对Map的解释如下:

An objectthat maps keys to values . Amap cannot contain duplicatekeys; each key can map to at most one value.

This interface takes the place of the Dictionary class, which was atotally abstract class rather than an interface.

The Map interface provides three collection views, which allow amap's contents to be viewed as a set of keys, collection of values,or set of key-value mappings.

 从上可知,Map用于存储“key-value”元素对,它将一个key映射到一个而且只能是唯一的一个value。

Map可以使用多种实现方式,HashMap的实现采用的是hash表;而TreeMap采用的是红黑树。

 

1. Hashtable 和 HashMap

这两个类主要有以下几方面的不同:

   Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类。

 

   在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null

 

  这两个类最大的不同在于Hashtable是线程安全的,它的方法是同步了的(方法同步了的意思是 使用 synch了的方法),可以直接用在多线程环境中。而HashMap则不是线程安全的。在多线程环境中,需要手动实现同步机制。因此,在Collections类中提供了一个方法返回一个同步版本的HashMap用于多线程的环境:

 

Java代码
  1. public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {  
  2.     return new SynchronizedMap<K,V>(m);  
  3.  }  
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {        return new SynchronizedMap<K,V>(m); }

该方法返回的是一个SynchronizedMap的实例。SynchronizedMap类是定义在Collections中的一个静态内部类。它实现了Map接口,并对其中的每一个方法实现,通过synchronized关键字进行了同步控制

 

2. 潜在的线程安全问题

上面提到Collections为HashMap提供了一个并发版本SynchronizedMap。这个版本中的方法都进行了同步,但是这并不等于这个类就一定是线程安全的。在某些时候会出现一些意想不到的结果。

如下面这段代码:

Java代码
  1. // shm是SynchronizedMap的一个实例  
  2. if(shm.containsKey('key')){  
  3.         shm.remove(key);  
  4. }  
// shm是SynchronizedMap的一个实例if(shm.containsKey('key')){        shm.remove(key);}

 这段代码用于从map中删除一个元素之前判断是否存在这个元素。这里的containsKey和reomve方法都是同步的,但是整段代码却不是。考虑这么一个使用场景:线程A执行了containsKey方法返回true,准备执行remove操作;这时另一个线程B开始执行,同样执行了containsKey方法返回true,并接着执行了remove操作;然后线程A接着执行remove操作时发现此时已经没有这个元素了。要保证这段代码按我们的意愿工作,一个办法就是对这段代码进行同步控制,但是这么做付出的代价太大。

 

在进行迭代时这个问题更改明显。Map集合共提供了三种方式来分别返回键、值、键值对的集合:

Java代码
  1. Set<K> keySet();  
  2.   
  3. Collection<V> values();  
  4.   
  5. Set<Map.Entry<K,V>> entrySet();  
Set<K> keySet();Collection<V> values();Set<Map.Entry<K,V>> entrySet();

 在这三个方法的基础上,我们一般通过如下方式访问Map的元素:

Java代码
  1. Iterator keys = map.keySet().iterator();  
  2.   
  3. while(keys.hasNext()){  
  4.         map.get(keys.next());  
  5. }  
Iterator keys = map.keySet().iterator();while(keys.hasNext()){        map.get(keys.next());}

 

在这里,有一个地方需要注意的是:得到的keySet和迭代器都是Map中元素的一个“视图”,而不是“副本”

问题也就出现在这里,当一个线程正在迭代Map中的元素时,另一个线程可能正在修改其中的元素。此时,在迭代元素时就可能会抛出ConcurrentModificationException异常。

为了解决这个问题通常有两种方法,是直接返回元素的副本,而不是视图。这个可以通过集合类的 toArray()方法实现,但是创建副本的方式效率比之前有所降低,特别是在元素很多的情况下;

另一种方法就是在迭代的时候锁住整个集合,这样的话效率就更低了。

API上如下解释:

synchronizedMap

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
返回由指定映射支持的同步(线程安全的)映射。为了保证按顺序访问,必须通过返回的映射完成所有对底层实现映射的访问。

在返回映射的任意 collection 视图上进行迭代时,用户必须手工在返回的映射上进行同步:

  Map m = Collections.synchronizedMap(new HashMap());      ...  Set s = m.keySet();  // Needn't be in synchronized block      ...  synchronized(m) {  // Synchronizing on m, not s!      Iterator i = s.iterator(); // Must be in synchronized block      while (i.hasNext())          foo(i.next());  } 
不遵从此建议将导致无法确定的行为。

如果指定映射是可序列化的,则返回的映射也将是可序列化的。

参数:
m - 被“包装”在同步映射中的映射。
返回:
指定映射的同步视图。

3. 更好的选择:ConcurrentHashMap

效率低下的HashTable容器

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

锁分段技术

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

 

java5中新增了ConcurrentMap接口和它的一个实现类ConcurrentHashMap。ConcurrentHashMap提供了和Hashtable以及SynchronizedMap中所不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

 

上面说到的16个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在size等操作时才需要锁住整个hash表。

 

在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。


0 0
原创粉丝点击