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

摘要:除了便于编写并发应用的集合API外,java.util.concurrent还引入了其它的预置程序组件,这些组件能辅助你在多线程应用中控制和执行线程。Ted Neward再介绍了五个来自于java.util.concurrent的Java编程必备窍门。

通过提供线程安全,性能良好的数据结构,并发集合框架使并发编程变得更容易。然而在有些情况下,开发者需要多走一步,并要考虑控制和/或调节线程的执行。提供java.util.concurrent包的全部原因就是为了简化多线程编程--事实上正是如此。
    接着第一部分,本文介绍了多个同步数据结构,这些数据结构比核心语言基本结构(监视器)的层次要高,但不会高到将它们圈囿在一个集合类中。一旦知道了这些锁与栓的用途,就能径直去用了。

1. 信号量

    在有些企业级系统中,常需要开发者去控制针对特定资源的请求(线程或动作)的数量。虽然完全可能试着手工编写这样的调节程序,但使用Semaphore类会更容易些,该类会为你处理对线程的控制,如清单1所示:

清单1. 使用信号量调节线程

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

public class SemApp
{
    public static void main(String[] args)
    {
        Runnable limitedCall = new Runnable() {
            final Random rand = new Random();
            final Semaphore available = new Semaphore(3);
            int count = 0;
            public void run()
            {
                int time = rand.nextInt(15);
                int num = count++;

                try
                {
                    available.acquire();

                    System.out.println("Executing " +
                        "long-running action for " +
                        time + " seconds #" + num);

                    Thread.sleep(time * 1000);

                    System.out.println("Done with #" +
                        num + "!");

                    available.release();
                }
                catch (InterruptedException intEx)
                {
                    intEx.printStackTrace();
                }
            }
        };

        for (int i=0; i<10; i++)
            new Thread(limitedCall).start();
    }
}

虽然上例有10个线程在运行(针对运行SemApp的Java进程执行jstack程序可以验证这一点),但只有3个是活动的。另外7个线程会被保存起来,直到其中一个信号量计数器被释放出来。(准确地说,Semaphore类支持一次获取和释放一个以上的被许可线程,但在此处的场景中这么做没有意义。)

2. CountDownLatch
    如果并发类Semaphore是被设计为在同一时刻允许"其中"一个线程执行的话,那么CountDownLatch就是赛马比赛中的起跑门。该类持有所有的线程,当遇到某个特定条件,那时CountDownLatch就会一次性释放全部的线程。

清单2. CountDownLatch:让我们比赛!

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

class Race
{
    private Random rand = new Random();

    private int distance = rand.nextInt(250);
    private CountDownLatch start;
    private CountDownLatch finish;

    private List<String> horses = new ArrayList<String>();

    public Race(String names)
    {
        this.horses.addAll(Arrays.asList(names));
    }

    public void run()
        throws InterruptedException
    {
        System.out.println("And the horses are stepping up to the gate");
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch finish = new CountDownLatch(horses.size());
        final List<String> places =
            Collections.synchronizedList(new ArrayList<String>());

        for (final String h : horses)
        {
            new Thread(new Runnable() {
                public void run() {
                    try
                    {
                        System.out.println(h +
                            " stepping up to the gate");
                        start.await();

                        int traveled = 0;
                        while (traveled < distance)
                        {
                            // In a 0-2 second period of time.
                            Thread.sleep(rand.nextInt(3) * 1000);

                            //  a horse travels 0-14 lengths
                            traveled += rand.nextInt(15);
                            System.out.println(h +
                                " advanced to " + traveled + "!");
                        }
                        finish.countDown();
                        System.out.println(h +
                            " crossed the finish!");
                        places.add(h);
                    }
                    catch (InterruptedException intEx)
                    {
                        System.out.println("ABORTING RACE!!!");
                        intEx.printStackTrace();
                    }
                }
            }).start();
        }

        System.out.println("And they're off!");
        start.countDown();        

        finish.await();
        System.out.println("And we have our winners!");
        System.out.println(places.get(0) + " took the gold");
        System.out.println(places.get(1) + " got the silver");
        System.out.println("and " + places.get(2) + " took home the bronze.");
    }
}

public class CDLApp
{
    public static void main(String[] args)
        throws InterruptedException, java.io.IOException
    {
        System.out.println("Prepping");

        Race r = new Race(
            "Beverly Takes a Bath",
            "RockerHorse",
            "Phineas",
            "Ferb",
            "Tin Cup",
            "I'm Faster Than a Monkey",
            "Glue Factory Reject"
            );

        System.out.println("It's a race of " + r.getDistance() + " lengths");

        System.out.println("Press Enter to run the race.");
        System.in.read();

        r.run();
    }
}

注意在清单2中,CountDownLatch服务于两个目的:首先,它同时释放所有的线程,模拟比赛的开始;但之后,另一个CountDownLatch模拟了比赛的结束。一场比赛会有更多的评论,你可以在比赛的"转弯"和"半程"点添加CountDownLatch,当马匹跑过1/4程,半程和3/4程时。

3. Executor
    清单1和清单2中的例子都遭遇了一个令人非常沮丧的错误,你被迫要直接地创建Thread对象。这是一个造成麻烦的方式,因为在有些JVM中,创建Thread对象是一件重量级的工作,所以重用而非创建新的线程要好得多。然而在另一些JVM中,情况就恰恰相反:Thread是非常轻量级的,若你需要一个线程,直接创建它则会好得多。当然,如果Murphy有他自己的方法(他经常就是这么做的),无论你使用哪种方法,对于你最终所依赖的某种Java平台都会是错误的。
    JSR-166专家组在一定程度上预见到了这种情况。与让Java开发者直接创建Thread实例不同,他们推荐Executor接口,这是一个创建新线程的抽象。如果清单3所示,Executor允许你自己不必使用new操作符去创建Thread对象:

清单3. Executor

Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() {  });

使用Excutor的主要缺点与我们使用所有对象工厂所遇到的缺点一样:工厂必须来源于某处。不幸地是,不同于CLR,JVM并不带有一个标准的VM范围内的线程池。
    Executor类只是作为获取Executor实现实例的常用地方,但它只有new方法(例如,为了创建新的线程池);它没有预创建的实例。所以,如果你想创建并使用一个能贯穿于整个程序的Executor实现,你就可以创建一个你自己的Executor实例。(或者,在有些情况下,你可以使用你所选容器/平台所提供的Executor实例。)

ExecutorService,为你服务

    ExecutorService的用处在于使你不必关心Thread来自于何处,Executor接口缺乏Java开发者可能期望的一些功能,比如启动一个线程,该线程用于产生结果,它会以非阻塞方式一直等待,直到结果出现为止。(在桌面应用中这是很普通的需求,在这种应用中用户会执行一个需要访问数据库的UI操作,如果它耗时太长的话,就可能想要在它完成之前就取消这一操作。)
    为此,JSR-166的专家们创造一个更为有用的抽象,ExecutorService接口,该接口将启动线程的工厂模型化为一个服务,这样就能对该服务进行集合化控制了。例如,不对每个任务调用一次execute()方法,ExecutorService能创建一个任务的集合,并可返回代表这些任务未来结果的Future集合。

4. ScheduledExecutorServices
    与ExecutorService接口同样优秀,特定的任务需要以计划的形式进行执行,例如在特定的时间间隔或在特定的时刻执行给定的任务。这就是继承自ExecutorService的ScheduledExecutorService的职责范畴。
    如果你的目的是创建一个"心跳"命令,该命令每5秒钟就去"ping"一次。ScheduledExecutorService会帮你做到这一点,正如你在清单4中所见的那般简单:

清单4. ScheduledExecutorService按计划去"Ping"

import java.util.concurrent.*;

public class Ping
{
    public static void main(String[] args)
    {
        ScheduledExecutorService ses =
            Executors.newScheduledThreadPool(1);
        Runnable pinger = new Runnable() {
            public void run() {
                System.out.println("PING!");
            }
        };
        ses.scheduleAtFixedRate(pinger, 5, 5, TimeUnit.SECONDS);
    }
}

怎么样?没有操作线程的烦恼,如果用户想取消心跳,也不必操心如何去做,前台或后台都没有显示的标记线程;所有的调度细节都留给了ScheduledExecutorService。
    顺便提一下,如果用户想要取消心跳,从scheduleAtFixedRate()方法返回的会是一个ScheduledFuture实例,它不仅含有执行结果(如果有的话),也有一个cancel()方法去停止该计划任务。

5. 超时方法
    拥有为阻塞操作置一个确定的超时控制的能力(这样就可以避免死锁)是java.util.concurrent类库相比于旧有并发API,如针对锁的监视器,的最大优点之一。
    这些方法几乎总是按int/TimeUnit对的方式进行重载,该int/TimeUnit对用于指示方法在跳出执行并将控制返回给平台之前需要等待多长时间。这要求开发者对此做更多的工作--如果没有获得锁,将如何进行恢复?--但结果却几乎总是正确的:更少的死锁,以及更加生产安全的代码。(更多关于生产就绪的代码,请见Michael Nygard的Release It!)

结论
    java.util.concurrent包含有许多更优雅的工具,它们出于集合框架,但更胜之,特别是.locks和.atomic包中的类。深入挖掘之,你将发现像CyclicBarrier这样的十分有用的控制结构,甚至于更多。
    下一次,我们将步入一个新的主题:你所不知道的五件关于Jar的事情。

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

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

                                                                这是Ted Neward在IBM developerWorks中5 things ...

  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. Win32 DLL和MFC DLL 中封装对话框

    现在最常看见的关于DLL的问题就是如何在DLL中使用对话框,这是一个很普遍的关于如何在DLL中使用资源的问题.这里我们从Win32   DLL和MFC   DLL两个方面来分析并解决这个问题.     ...

  2. IText PdfPTable表格 单元的居中显示

    昨晚寻找了网上很多关于IText表格居中问题,他们其中的有些代码我即使复制上去生成的doc表格的文字都是不居中的,后来我自己找出了一种居中方式: 为PdfPCell对象添加paragraph对象,并将 ...

  3. http://jingyan.baidu.com/article/e4511cf33479812b855eaf67.html

    http://jingyan.baidu.com/article/e4511cf33479812b855eaf67.html

  4. C++中 模板Template的使用

    1.在c++Template中很多地方都用到了typename与class这两个关键字,而且好像可以替换,是不是这两个关键字完全一样呢?答:class用于定义类,在模板引入c++后,最初定义模板的方法 ...

  5. WinAPI你知道多少?!(上千个,好多都没见过)

    http://www.cnblogs.com/vanver/archive/2013/06/13/NO-2013_06_13pm.html 播客开篇,讲讲废话:本篇播客只是推荐给热与钻研的同学们... ...

  6. (转)Android系统自带样式(@android:style/)

    在AndroidManifest.xml文件的activity中配置 1.android:theme="@android:style/Theme" 默认状态,即如果theme这里不 ...

  7. Vim常用命令手册

    这两年工作基本都是用vim,用习惯发现到哪都离不开这玩意. 退出编辑器 :w 将缓冲区写入文件,即保存修改:wq 保存修改并退出:x 保存修改并退出:q 退出,如果对缓冲区进行过修改,则会提示:q! ...

  8. 彻底搞懂javascript中的match, exec的区别

    在工作中经常发现一些同学把这两个方法搞混,以致把自己弄的很郁闷.所以我和大家一起来探讨一下这两个方法的奥妙之处吧. 我们分以下几点来讲解: 相同点: 1.两个方法都是查找符合条件的匹配项,并以数组形式 ...

  9. Codeforces Round #221 (Div. 2) Lever I.O.U.

    这场cf 做的很差,,第一题犯了一个很低级的错误..把i写成了J.... 第二题 想的太复杂了...其实我们只需要 考虑每个人自己的负债情况就行了,就是假设每个人把别人 欠他的钱,拿过来还给别人..这 ...

  10. poj 2109 Power of Cryptography (double 精度)

    题目:http://poj.org/problem?id=2109 题意:求一个整数k,使得k满足kn=p. 思路:exp()用来计算以e为底的x次方值,即ex值,然后将结果返回.log是自然对数,就 ...