这是Ted NewardIBM developerWorks5 things系列文章中的一篇,讲述了关于Java并发集合API的一些应用窍门,值得大家学习。(2010.05.24最后更新)

摘要:编写既要性能良好又要防止应用崩溃的多线程代码确实很难--这也正是我们需要java.util.concurrent的原因。Ted Neward向你展示了像CopyOnWriteArrayList,BlockingQueue和ConcurrentMap这样的并发集合类是如何为了并发编程需要而改进标准集合类的。

并发集合API是Java 5的一大新特性,但由于对Annotation和泛型的热捧,许多Java开发者忽视了这些API。另外(可能更真实的是),因为许多开发者猜想并发集合 API肯定很复杂,就像去尝试解决一些问题那样,所以开发者们会回避java.util.concurrent包。
    事实上,java.util.concurrent的很多类并不需要你费很大力就能高效地解决通常的并发问题。继续看下去,你就能学到 java.util.concurrent中的类,如CopyOnWriteArrayList和BlockingQueue,是怎样帮助你解决多线程编程可怕的挑战。

1. TimeUnit
    java.util.concurrent.TimeUnit本身并不是集合框架类,这个枚举使得代码非常易读。使用TimeUnit能够将开发者从与毫秒相关的困苦中解脱出来,转而他们自己的方法或API。
    TimeUnit能与所有的时间单元协作,范围从毫秒和微秒到天和小时,这就意味着它能处理开发者可能用到的几乎所有时间类型。还要感谢这个枚举类型声明的时间转换方法,当时间加快时,它甚至能细致到把小时转换回毫秒。

2. CopyOnWriteArrayList
    制作数组的干净复本是一项成本极高的操作,在时间和内存这两方面均有开销,以至于在通常的应用中不能考虑该方法;开发者常常求助于使用同步的 ArrayList来替代前述方法。但这也是一个比较有代价的选项,因为当每次你遍历访问该集合中的内容时,你不得不同步所有的方法,包括读和写,以确保内存一致性。
    在有大量用户在读取ArrayList而只有很少用户对其进行修改的这一场景中,上述方法将使成本结构变得缓慢。
    CopyOnWriteArrayList就是解决这一问题的一个极好的宝贝工具。它的Javadoc描述到,ArrayList通过创建数组的干净复本来实现可变操作(添加,修改,等等),而CopyOnWriteArrayList则是ArrayList的一个"线程安全"的变体。
    对于任何修改操作,该集合类会在内部将其内容复制到一个新数组中,所以当读用户访问数组的内容时不会招致任何同步开销(因为它们没有对可变数据进行操作)。
    本质上,创建CopyOnWriteArrayList的想法,是出于应对当ArrayList无法满足我们要求时的场景:经常读,而很少写的集合对象,例如针对JavaBean事件的Listener。

3. BlockingQueue
    BlockingQueue接口表明它是一个Queue,这就意味着它的元素是按先进先出(FIFO)的次序进行存储的。以特定次序插入的元素会以相同的次序被取出--但根据插入保证,任何从空队列中取出元素的尝试都会堵塞调用线程直到该元素可被取出时为止。同样地,任何向一个已满队列中插入元素的尝试将会堵塞调用线程直到该队列的存储空间有空余时为止。
    在不需要显式地关注同步问题时,如何将由一个线程聚集的元素"交给"另一个线程进行处理呢,BlockingQueue很灵巧地解决了这个问题。Java Tutorial中Guarded Blocks一节是很好的例子。它使用手工同步和wait()/notifyAll()方法创建了一个单点(single-slot)受限缓冲,当一个新的元素可被消费且当该点已经准备好被一个新的元素填充时,该方法就会在线程之间发出信号。(详情请见Guarded Blocks)
    尽管教程Guarded Blocks中的代码可以正常工作,但它比较长,有些凌乱,而且完全不直观。诚然,在Java平台的早期时代,Java开发者们不得不;但现在已经是 2010年了--问题已经得到改进?
    清单1展示的程序重写了Guarded Blocks中的代码,其中我使用ArrayBlockingQueue替代了手工编写的Drop。

清单1. BlockingQueue

import java.util.*;
import java.util.concurrent.*;

class Producer
    implements Runnable
{
    private BlockingQueue<String> drop;
    List<String> messages = Arrays.asList(
        "Mares eat oats",
        "Does eat oats",
        "Little lambs eat ivy",
        "Wouldn't you eat ivy too?");

    public Producer(BlockingQueue<String> d) { this.drop = d; }

    public void run()
    {
        try
        {
            for (String s : messages)
                drop.put(s);
            drop.put("DONE");
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " +
                "Last one out, turn out the lights!");
        }
    }
}

class Consumer
    implements Runnable
{
    private BlockingQueue<String> drop;
    public Consumer(BlockingQueue<String> d) { this.drop = d; }

    public void run()
    {
        try
        {
            String msg = null;
            while (!((msg = drop.take()).equals("DONE")))
                System.out.println(msg);
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " +
                "Last one out, turn out the lights!");
        }
    }
}

public class ABQApp
{
    public static void main(String[] args)
    {
        BlockingQueue<String> drop = new ArrayBlockingQueue(1, true);
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

ArrayBlockingQueue也崇尚"公平"--即意味着,它能给予读和写线程先进先出的访问次序。该方法可能是一种更高效的策略,但它也加大了造成线程饥饿的风险。(就是说,当其它读线程持有锁时,该策略可更高效地允许读线程进行执行,但这也就会产生读线程的常量流使写线程总是无法执行的风险)
    BlockingQueue也支持在方法中使用时间参数,当插入或取出元素出了问题时,方法需要返回以发出操作失败的信号,而该时间参数指定了在返回前应该阻塞多长时间。

4. ConcurrentMap
    Map有一些细微的并发Bug,会使许多粗心的Java开发者误入歧途。ConcurrentMap则是一个简单的决定方案。
    当有多个线程在访问一个Map时,通常在储存一个键/值对之前通常会使用方法containsKey()或get()去确定给出的键是否存在。即使用同步的Map,某个线程仍可在处理的过程中潜入其中,然后获得对Map的控制权。问题在于,在get()方法的开始处获得了锁,然后在调用方法put()去重新获得该锁之前会先释放它。这就导致了竞争条件:两个线程之间的竞争,根据哪个线程先执行,其结果将不尽相同。
    如果两个线程在同一时刻调用一个方法,一个测试键是否存在,另一个则置入新的键/值对,那么在此过程中,第一个线程的值将会丢失。幸运地是,ConcurrentMap接口支持一组额外的方法,设计这些方法是为了在一个锁中做两件事情:例如,putIfAbsent()首先进行测试,之后只有当该键还未存储到Map中时,才执行置入操作。

5. SynchronousQueues
    根据Javadoc的描述,SynchronousQueue是一个很有趣的创造物:
    一个阻塞队列在每次的插入操作中必须等等另一线程执行对应的删除线程,反之亦然。同步队列并没有任何内部的存储空间,一个都没有。
    本质上,SynchronousQueue是之前提及的BlockingQueue的另一种实现。使用ArrayBlockingQueue利用的阻塞语义,SynchronousQueue给予我们一种极轻量级的途径在两个线程之间交换单个元素。在清单2中,我用SynchronousQueue替代 ArrayBlockingQueue重写了清单1的代码:

清单2 SynchronousQueue

import java.util.*;
import java.util.concurrent.*;

class Producer
    implements Runnable
{
    private BlockingQueue<String> drop;
    List<String> messages = Arrays.asList(
        "Mares eat oats",
        "Does eat oats",
        "Little lambs eat ivy",
        "Wouldn't you eat ivy too?");

    public Producer(BlockingQueue<String> d) { this.drop = d; }

    public void run()
    {
        try
        {
            for (String s : messages)
                drop.put(s);
            drop.put("DONE");
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " +
                "Last one out, turn out the lights!");
        }
    }
}

class Consumer
    implements Runnable
{
    private BlockingQueue<String> drop;
    public Consumer(BlockingQueue<String> d) { this.drop = d; }

    public void run()
    {
        try
        {
            String msg = null;
            while (!((msg = drop.take()).equals("DONE")))
                System.out.println(msg);
        }
        catch (InterruptedException intEx)
        {
            System.out.println("Interrupted! " +
                "Last one out, turn out the lights!");
        }
    }
}

public class SynQApp
{
    public static void main(String[] args)
    {
        BlockingQueue<String> drop = new SynchronousQueue<String>();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

上述实现看起来几乎相同,但该应用程序已新加了一个好处,在这个实现中,只有当有线程正在等待消费某个元素时,SynchronousQueue才会允许将该元素插入到队列中。
就实践方式来看,SynchronousQueue类似于Ada或CSP等语言中的"交会通道(Rendezvous Channel)"。在其它环境中,有时候被称为"连接"。

结论
    当Java运行时类库预先已经提供了方便使用的等价物时,为什么还要费力地向集合框架中引入并发呢?本系列的下一篇文章将探索 java.util.concurrent命名空间的更多内容。

你所不知道的五件事情--java.util.concurrent(第一部分)的更多相关文章

  1. 你所不知道的五件事情--java.util.concurrent(第二部分)

    这是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,仍然讲述了关于Java并发集合API的一些应用窍门,值得大家学习.(2010.06.17最后更新) 摘 ...

  2. 聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore

    前几篇分析了一下AQS的原理和实现.这篇拿Semaphore信号量做样例看看AQS实际是怎样使用的. Semaphore表示了一种能够同一时候有多个线程进入临界区的同步器,它维护了一个状态表示可用的票 ...

  3. 【译】Surface中你也许不知道的五件事

    Bring up the Quick Link Menu - Select the Windows Key + X or right click the Start Button to bring u ...

  4. 关于 java.util.concurrent 您不知道的 5 件事--转

    第 1 部分 http://www.ibm.com/developerworks/cn/java/j-5things4.html Concurrent Collections 是 Java™ 5 的巨 ...

  5. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  6. (转)关于 Java 对象序列化您不知道的 5 件事

    关于 Java 对象序列化您不知道的 5 件事 转自:http://developer.51cto.com/art/201506/479979.htm 数年前,当和一个软件团队一起用 Java 语言编 ...

  7. 关于 Java Collections API 您不知道的 5 件事,第 1 部分

    定制和扩展 Java Collections Java™ Collections API 远不止是数组的替代品,虽然一开始这样用也不错.Ted Neward 提供了关于用 Collections 做更 ...

  8. 关于 Java Collections API 您不知道的 5 件事--转

    第 1 部分 http://www.ibm.com/developerworks/cn/java/j-5things2.html 对于很多 Java 开发人员来说,Java Collections A ...

  9. 你所不知道的html5与html中的那些事(五)——web图像

    文章简介:       现在的页面,一般都离不开图像,而怎么做才能让我们的页面中的图像加载的又快又好呢?在优化页面速度的时候还有什么事是你所不知道的呢?     下面看看今天我为大家带来了哪些关于we ...

随机推荐

  1. POJ2348+博弈

    /* 博弈 关键态:较大数是较小数的2倍以上. */ #include<stdio.h> #include<string.h> #include<stdlib.h> ...

  2. 【疯狂Java学习笔记】【理解面向对象】

    [学习笔记]1.Java语言是纯粹的面向对象语言,这体现在Java完全支持面向对象的三大基本特征:封装.继承.多态.抽象也是面向对象的重要组成部分,不过它不是面向对象的特征之一,因为所有的编程语言都需 ...

  3. Android ExpandableListView 带有Checkbox的简单应用

    expandablelistview2_groups.xml <?xml version="1.0" encoding="utf-8"?> < ...

  4. Get请求携带数据量的各种限制及解决办法、Post请求说明

    1.   Get请求携带数据量的各种限制及解决办法 Http Get方法提交的数据大小长度并没有限制,HTTP协议规范没有对URL长度进行限制.这个限制是特定的浏览器及服务器对它的限制. 到新公司处理 ...

  5. Android ActionBar隐藏修改图标和标题

    有时候在一些子页面或者内容页面,不需要显示ActionBar的标题栏图标.可用如下方式进行设置. 首先获取到ActionBar对象 ActionBar actionBar=getActionBar() ...

  6. python学习笔记七--数据操作符

    一.Python表达式操作符及程序:

  7. 在Ubuntu 12.04安装和设置Samba实现网上邻居共享

    转载:http://www.startos.com/ubuntu/tips/2012031333097.html          有微小改动. Samba 是一款功能强大的共享工具,可以实现与win ...

  8. 摄像头(3)调用系统拍照activity来拍照

    import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager ...

  9. hdu 4502 吉哥系列故事——临时工计划(dp)

    题目:http://acm.hdu.edu.cn/showproblem.php?pid=4502 思路:一个简单的dp ,比赛时没做出来呢..... d[i]代表 到第i天时的最大值 #includ ...

  10. js 动态计算折扣后总价格

    <script type="text/javascript"> <!---计算折扣后的总价格---> function outtotalprice(i) { ...