并发集合通过提供线程安全的,经过良好调整的数据结构,使并发编程更加容易。 但是,在某些情况下,开发人员需要更进一步,考虑调整和/或限制线程执行。 鉴于java.util.concurrent
的全部要点是简化多线程编程,您可能希望该程序包包含同步实用程序—确实如此。
本文是第1部分的后续文章,介绍了几种同步结构,这些结构比核心语言原语(监视器)的级别更高,但程度不高,以至于它们被埋藏在Collection类中。 一旦知道它们的用途,使用这些锁和大门非常简单。
在某些企业系统中,开发人员需要针对特定资源限制打开请求(线程/操作)的数量并不少见-实际上,限制有时可以通过减少针对特定资源的争用量来提高系统的吞吐量。资源。 虽然当然可以尝试手动编写节流代码,但是使用semaphore类会更容易,它可以为您节流,如清单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
进行验证),但只有三个SemApp
处于活动状态。 其他七个被搁置,直到其中一个信号量计数被释放。 (实际上, Semaphore
类支持一次获取和释放多个许可证 ,但这在这种情况下是没有意义的。)
如果Semaphore
是旨在允许线程一次“进入”的并发类(也许唤起了流行夜总会里蹦蹦跳跳的记忆),则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 horses = new ArrayList();
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 places =
Collections.synchronizedList(new ArrayList());
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
es,因为马越过了距离的四分之一,一半和四分之三。
清单1和清单2中的示例都具有相当令人沮丧的缺陷,因为它们迫使您直接创建Thread
对象。 这是麻烦的秘诀,因为在某些JVM中,创建Thread
是一项重量级的操作,与重用现有Thread
相比创建新Thread
要好得多。 但是,在其他JVM中,情况恰恰相反: Thread
非常轻巧,最好在需要时使用new
Thread
。 当然,如果墨菲按照他的方式行事(他通常会这样做),那么对于您最终在其上进行部署的平台而言,使用的任何一种方法都是完全错误的。
JSR-166专家组(请参阅参考资料 )在某种程度上预见了这种情况。 他们没有让Java开发人员直接创建Thread
,而是引入了Executor
接口,该接口是用于创建新线程的抽象。 如清单3所示, Executor
允许您创建线程,而不必自己new
Thread
对象:
Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() { ... });
使用Executor
的主要缺点与我们在所有工厂遇到的缺点相同:工厂必须来自某个地方。 不幸的是,与CLR不同,JVM没有附带标准的VM级线程池。
Executor
类确实是获得Executor
实例的常用场所,但是它只有new
方法(例如,创建新的线程池)。 它没有预先创建的实例。 因此,如果您想在整个代码中创建和使用Executor
实例,则需要您一个人做。 (或者,在某些情况下,您将能够使用所选容器/平台提供的实例。)
Executor
接口虽然不必担心Thread
的来源而有用,但它缺少Java开发人员可能期望的某些功能,例如能够启动旨在产生结果的线程并在非线程中等待的功能。阻止时尚,直到结果可用。 (这是台式机应用程序中的常见需求,在该应用程序中,用户将执行需要访问数据库的UI操作,但如果操作时间过长,则可能希望在操作完成之前取消该操作。)
为此,JSR-166专家创建了一个更为有用的抽象,即ExecutorService
接口,该接口将线程启动工厂建模为可以集体控制的服务。 例如,与为每个任务调用一次execute()
相比, ExecutorService
可以采用任务集合并返回表示每个任务的未来结果的期货列表 。
与ExecutorService
接口一样,某些任务需要以计划的方式完成,例如以确定的间隔或在特定的时间执行给定的任务。 这是ScheduledExecutorService
的省,该省扩展了ExecutorService
。
如果您的目标是创建一个每五秒钟“ ping”一次的“心跳”命令,那么ScheduledExecutorService
将使它像清单4一样简单:
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
方法来关闭已调度的操作。
在阻塞操作周围设置具体超时(从而避免死锁)的能力是java.util.concurrent
库相对于其较早的并发表亲(例如,用于监视的监视器)的一大进步。
这些方法几乎总是以int
/ TimeUnit
对重载,表明对方法进行搁置并将控制权返回给程序之前应等待多长时间。 开发人员需要做更多的工作-如果不获得锁,您将如何恢复? -但是结果几乎总是更正确:更少的死锁和更多的安全生产代码。 (欲了解更多有关编写产品代码,看到迈克尔·尼加德的发布吧!在相关主题 。)
java.util.concurrent
软件包包含许多漂亮的实用程序,它们远远超出了Collections的范围,尤其是在.locks
和.atomic
软件包中。 深入研究,您还将发现有用的控件结构,例如CyclicBarrier
等。
像Java平台的许多方面一样,您无需费劲查找可以非常有用的基础结构代码。 每当您编写多线程代码时,请记住本文和上一篇文章中讨论的实用程序。
翻译自: https://www.ibm.com/developerworks/java/library/j-5things5/index.html