Java7 并发秘籍(四)
原文:
zh.annas-archive.org/md5/F8E5EF0E7E4290BD7C1CC58C96A57EB0译者:飞龙
第七章:自定义并发类
在本章中,我们将涵盖:
-
自定义
ThreadPoolExecutor类 -
实现基于优先级的
Executor类 -
实现
ThreadFactory接口以生成自定义线程 -
在
Executor对象中使用我们的ThreadFactory -
自定义在计划线程池中运行的任务
-
实现
ThreadFactory接口以为 Fork/Join 框架生成自定义线程 -
自定义在 Fork/Join 框架中运行的任务
-
实现自定义
Lock类 -
基于优先级实现传输队列
-
实现自己的原子对象
介绍
Java 并发 API 提供了许多接口和类来实现并发应用程序。它们提供低级机制,如Thread类、Runnable或Callable接口或synchronized关键字,以及高级机制,如 Executor 框架和 Java 7 版本中添加的 Fork/Join 框架。尽管如此,您可能会发现自己正在开发一个程序,其中没有任何 java 类满足您的需求。
在这种情况下,您可能需要基于 Java 提供的工具来实现自己的自定义并发工具。基本上,您可以:
-
实现一个接口以提供该接口定义的功能。例如,
ThreadFactory接口。 -
重写类的一些方法以使其行为适应您的需求。例如,重写
Thread类的run()方法,默认情况下不执行任何有用的操作,应该重写以提供一些功能。
通过本章的示例,您将学习如何更改一些 Java 并发 API 类的行为,而无需从头设计并发框架。您可以将这些示例作为实现自定义的初始点。
自定义 ThreadPoolExecutor 类
Executor 框架是一种允许您将线程创建与其执行分离的机制。它基于Executor和ExecutorService接口,使用实现了这两个接口的ThreadPoolExecutor类。它具有内部线程池,并提供方法,允许您发送两种类型的任务以在池化线程中执行。这些任务是:
-
Runnable接口以实现不返回结果的任务 -
Callable接口以实现返回结果的任务
在这两种情况下,您只需将任务发送到执行器。执行器使用其池化线程之一或创建一个新线程来执行这些任务。执行器还决定任务执行的时机。
在本示例中,您将学习如何重写ThreadPoolExecutor类的一些方法,以计算在执行器中执行的任务的执行时间,并在执行器完成执行时在控制台中写入有关执行器的统计信息。
准备工作
本示例的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做…
按照下面描述的步骤实现示例:
- 创建一个名为
MyExecutor的类,它扩展了ThreadPoolExecutor类。
public class MyExecutor extends ThreadPoolExecutor {
- 声明一个私有的
ConcurrentHashMap属性,参数化为String和Date类,命名为startTimes。
private ConcurrentHashMap<String, Date> startTimes;
- 实现该类的构造函数。使用
super关键字调用父类的构造函数并初始化startTime属性。
public MyExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
startTimes=new ConcurrentHashMap<>();
}
- 重写
shutdown()方法。在控制台中写入有关已执行任务、正在运行任务和待处理任务的信息。然后,使用super关键字调用父类的shutdown()方法。
@Override
public void shutdown() {
System.out.printf("MyExecutor: Going to shutdown.\n");
System.out.printf("MyExecutor: Executed tasks: %d\n",getCompletedTaskCount());
System.out.printf("MyExecutor: Running tasks: %d\n",getActiveCount());
System.out.printf("MyExecutor: Pending tasks: %d\n",getQueue().size());
super.shutdown();
}
- 重写
shutdownNow()方法。在控制台中写入有关已执行任务、正在运行任务和待处理任务的信息。然后,使用super关键字调用父类的shutdownNow()方法。
@Override
public List<Runnable> shutdownNow() {
System.out.printf("MyExecutor: Going to immediately shutdown.\n");
System.out.printf("MyExecutor: Executed tasks: %d\n",getCompletedTaskCount());
System.out.printf("MyExecutor: Running tasks: %d\n",getActiveCount());
System.out.printf("MyExecutor: Pending tasks: %d\n",getQueue().size());
return super.shutdownNow();
}
- 覆盖
beforeExecute()方法。在控制台中写入将执行任务的线程的名称和任务的哈希码的消息。使用任务的哈希码作为键,将开始日期存储在HashMap中。
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.printf("MyExecutor: A task is beginning: %s : %s\n",t.getName(),r.hashCode());
startTimes.put(String.valueOf(r.hashCode()), new Date());
}
- 覆盖
afterExecute()方法。在控制台中写入任务的结果,并计算任务的运行时间,减去存储在HashMap中的任务的开始日期。
@Override
protected void afterExecute(Runnable r, Throwable t) {
Future<?> result=(Future<?>)r;
try {
System.out.printf("*********************************\n");
System.out.printf("MyExecutor: A task is finishing.\n");
System.out.printf("MyExecutor: Result: %s\n",result.get());
Date startDate=startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate=new Date();
long diff=finishDate.getTime()-startDate.getTime();
System.out.printf("MyExecutor: Duration: %d\n",diff);
System.out.printf("*********************************\n");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
- 创建一个名为
SleepTwoSecondsTask的类,实现带有String类参数的Callable接口。实现call()方法。将当前线程休眠 2 秒,并返回转换为String类型的当前日期。
public class SleepTwoSecondsTask implements Callable<String> {
public String call() throws Exception {
TimeUnit.SECONDS.sleep(2);
return new Date().toString();
}
}
- 通过创建一个名为
Main的主类来实现示例的主要类,其中包含一个main()方法。
public class Main {
public static void main(String[] args) {
- 创建一个名为
myExecutor的MyExecutor对象。
MyExecutor myExecutor=new MyExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>());
- 创建一个带有
String类参数的Future对象列表,用于存储您要发送到执行器的任务的结果对象。
List<Future<String>> results=new ArrayList<>();¡;
- 提交 10 个
Task对象。
for (int i=0; i<10; i++) {
SleepTwoSecondsTask task=new SleepTwoSecondsTask();
Future<String> result=myExecutor.submit(task);
results.add(result);
}
- 使用
get()方法获取前五个任务的执行结果。在控制台中写入它们。
for (int i=0; i<5; i++){
try {
String result=results.get(i).get();
System.out.printf("Main: Result for Task %d : %s\n",i,result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
- 使用
shutdown()方法结束执行器的执行。
myExecutor.shutdown();
- 使用
get()方法获取最后五个任务的执行结果。在控制台中写入它们。
for (int i=5; i<10; i++){
try {
String result=results.get(i).get();
System.out.printf("Main: Result for Task %d : %s\n",i,result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
- 使用
awaitTermination()方法等待执行器的完成。
try {
myExecutor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 写一条消息指示程序执行结束。
System.out.printf("Main: End of the program.\n");
它是如何工作的...
在这个配方中,我们扩展了ThreadPoolExecutor类来实现我们的自定义执行器,并覆盖了它的四个方法。beforeExecute()和afterExecute()方法用于计算任务的执行时间。beforeExecute()方法在任务执行之前执行。在这种情况下,我们使用HashMap来存储任务的开始日期。afterExecute()方法在任务执行后执行。您可以从HashMap中获取已完成任务的startTime,然后计算实际日期与该日期之间的差异,以获取任务的执行时间。您还覆盖了shutdown()和shutdownNow()方法,以将执行器中执行的任务的统计信息写入控制台:
-
使用
getCompletedTaskCount()方法执行的任务 -
使用
getActiveCount()方法获取当前正在运行的任务
使用阻塞队列的size()方法来存储待处理任务的执行器。实现Callable接口的SleepTwoSecondsTask类将其执行线程休眠 2 秒,Main类中,您向执行器发送 10 个任务,使用它和其他类来演示它们的特性。
执行程序,您将看到程序显示每个正在运行的任务的时间跨度以及在调用shutdown()方法时执行器的统计信息。
另请参阅
-
第四章中的创建线程执行器配方,线程执行器
-
第七章中的在执行器中使用我们的 ThreadFactory对象配方,自定义并发类
实现基于优先级的执行器类
在 Java 并发 API 的早期版本中,您必须创建和运行应用程序的所有线程。在 Java 版本 5 中,随着执行器框架的出现,引入了一种新的机制来执行并发任务。
使用执行器框架,您只需实现您的任务并将其发送到执行器。执行器负责创建和执行执行您的任务的线程。
在内部,执行程序使用阻塞队列来存储待处理任务。这些任务按照它们到达执行程序的顺序进行存储。一种可能的替代方案是使用优先级队列来存储新任务。这样,如果具有高优先级的新任务到达执行程序,它将在已经等待线程执行的其他线程之前执行,但具有较低优先级。
在本示例中,您将学习如何实现一个执行程序,该执行程序将使用优先级队列来存储您发送的任务以供执行。
准备就绪
本示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyPriorityTask的类,该类实现了Runnable和Comparable接口,参数化为MyPriorityTask类接口。
public class MyPriorityTask implements Runnable, Comparable<MyPriorityTask> {
- 声明一个名为
priority的私有int属性。
private int priority;
- 声明一个名为
name的私有String属性。
private String name;
- 通过实现类的构造函数来初始化其属性。
public MyPriorityTask(String name, int priority) {
this.name=name;
this.priority=priority;
}
- 实现一个方法来返回优先级属性的值。
public int getPriority(){
return priority;
}
- 实现
Comparable接口中声明的compareTo()方法。它接收一个MyPriorityTask对象作为参数,并比较两个对象的优先级,当前对象和参数对象。您让具有更高优先级的任务在具有较低优先级的任务之前执行。
@Override
public int compareTo(MyPriorityTask o) {
if (this.getPriority() < o.getPriority()) {
return 1;
}
if (this.getPriority() > o.getPriority()) {
return -1;
}
return 0;
}
- 实现
run()方法。将当前线程休眠 2 秒。
@Override
public void run() {
System.out.printf("MyPriorityTask: %s Priority : %d\n",name,priority);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建名为
executor的ThreadPoolExecutor对象。使用PriorityBlockingQueue参数化为Runnable接口作为此执行程序将用于存储其待处理任务的队列。
ThreadPoolExecutor executor=new ThreadPoolExecutor(2,2,1,TimeUnit.SECONDS,new PriorityBlockingQueue<Runnable>());
- 使用循环的计数器作为任务的优先级,向执行程序发送四个任务。使用
execute()方法将任务发送到执行程序。
for (int i=0; i<4; i++){
MyPriorityTask task=new MyPriorityTask ("Task "+i,i);
executor.execute(task);
}
- 将当前线程休眠 1 秒。
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 使用循环的计数器作为任务的优先级,向执行程序发送四个额外的任务。使用
execute()方法将任务发送到执行程序。
for (int i=4; i<8; i++) {
MyPriorityTask task=new MyPriorityTask ("Task "+i,i);
executor.execute(task);
}
- 使用
shutdown()方法关闭执行程序。
executor.shutdown();
- 使用
awaitTermination()方法等待执行程序的完成。
try {
executor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
- 在控制台中写入一条消息,指示程序的完成。
System.out.printf("Main: End of the program.\n");
它是如何工作的...
将执行程序转换为基于优先级的执行程序很简单。您只需传递一个使用Runnable接口参数化的PriorityBlockingQueue对象作为参数。但是对于执行程序,您应该知道存储在优先级队列中的所有对象都必须实现Comparable接口。
您已经实现了MyPriorityTask类,该类实现了Runnable接口,用作任务,并实现了Comparable接口,用于存储在优先级队列中。该类具有一个Priority属性,用于存储任务的优先级。如果任务的这个属性具有更高的值,它将更早执行。compareTo()方法确定了优先级队列中任务的顺序。在Main类中,您向执行程序发送了八个具有不同优先级的任务。您发送给执行程序的第一个任务是最先执行的任务。当执行程序空闲等待要执行的任务时,随着第一个任务到达执行程序,它立即执行它们。您使用两个执行线程创建了执行程序,因此前两个任务将是最先执行的任务。然后,其余的任务将根据它们的优先级执行。
以下屏幕截图显示了此示例的一个执行:
还有更多...
您可以配置Executor以使用BlockingQueue接口的任何实现。一个有趣的实现是DelayQueue。这个类用于存储延迟激活的元素。它提供了只返回活动对象的方法。您可以使用这个类来实现自己版本的ScheduledThreadPoolExecutor类。
另请参阅
-
第四章中的创建线程执行器配方,线程执行器
-
第七章中的自定义 ThreadPoolExecutor 类配方,自定义并发类
-
第六章中的使用按优先级排序的阻塞线程安全列表配方,并发集合
实现 ThreadFactory 接口以生成自定义线程
工厂模式是面向对象编程世界中广泛使用的设计模式。它是一个创建模式,其目标是开发一个类,其任务是创建一个或多个类的对象。然后,当我们想要创建这些类中的一个对象时,我们使用工厂而不是使用new运算符。
- 使用这个工厂,我们集中了对象的创建,从而方便地改变创建的对象的类或创建这些对象的方式,从而轻松限制了有限资源的对象创建。例如,我们可以只有N个对象,这些对象很容易生成有关对象创建的统计数据。
Java 提供了ThreadFactory接口来实现Thread对象工厂。Java 并发 API 的一些高级工具,如 Executor 框架或 Fork/Join 框架,使用线程工厂来创建线程。
Java 并发 API 中工厂模式的另一个例子是Executors类。它提供了许多方法来创建不同类型的Executor对象。
在这个配方中,您将通过添加新功能来扩展Thread类,并实现一个线程工厂类来生成该新类的线程。
准备工作
这个配方的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyThread的类,它继承Thread类。
public class MyThread extends Thread {
- 声明三个私有的
Date属性,分别命名为creationDate、startDate和finishDate。
private Date creationDate;
private Date startDate;
private Date finishDate;
- 实现类的构造函数。它接收名称和
Runnable对象作为参数。存储线程的创建日期。
public MyThread(Runnable target, String name ){
super(target,name);
setCreationDate();
}
- 实现
run()方法。存储线程的开始日期,调用父类的run()方法,并存储执行的完成日期。
@Override
public void run() {
setStartDate();
super.run();
setFinishDate();
}
- 实现一个方法来建立
creationDate属性的值。
public void setCreationDate() {
creationDate=new Date();
}
- 实现一个方法来建立
startDate属性的值。
public void setStartDate() {
startDate=new Date();
}
- 实现一个方法来建立
finishDate属性的值。
public void setFinishDate() {
finishDate=new Date();
}
- 实现一个名为
getExecutionTime()的方法,它计算线程的执行时间,即开始日期和完成日期之间的差异。
public long getExecutionTime() {
return finishDate.getTime()-startDate.getTime();
}
- 重写
toString()方法以返回线程的创建日期和执行时间。
@Override
public String toString(){
StringBuilder buffer=new StringBuilder();
buffer.append(getName());
buffer.append(": ");
buffer.append(" Creation Date: ");
buffer.append(creationDate);
buffer.append(" : Running time: ");
buffer.append(getExecutionTime());
buffer.append(" Milliseconds.");
return buffer.toString();
}
- 创建一个名为
MyThreadFactory的类,它实现ThreadFactory接口。
public class MyThreadFactory implements ThreadFactory {
- 声明一个私有的
int属性,命名为counter。
private int counter;
- 声明一个私有的
String属性,命名为prefix。
private String prefix;
- 实现类的构造函数以初始化其属性。
public MyThreadFactory (String prefix) {
this.prefix=prefix;
counter=1;
}
- 实现
newThread()方法。创建一个MyThread对象并增加counter属性。
@Override
public Thread newThread(Runnable r) {
MyThread myThread=new MyThread(r,prefix+"-"+counter);
counter++;
return myThread;
}
- 创建一个名为
MyTask的类,它实现Runnable接口。实现run()方法。让当前线程休眠 2 秒。
public class MyTask implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 通过创建一个名为
Main的类并添加一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个
MyThreadFactory对象。
MyThreadFactory myFactory=new MyThreadFactory("MyThreadFactory");
- 创建一个
Task对象。
MyTask task=new MyTask();
- 创建一个
MyThread对象,使用工厂的newThread()方法来执行任务。
Thread thread=myFactory.newThread(task);
- 启动线程并等待其完成。
thread.start();
thread.join();
- 使用
toString()方法编写有关线程的信息。
System.out.printf("Main: Thread information.\n");
System.out.printf("%s\n",thread);
System.out.printf("Main: End of the example.\n");
它是如何工作的...
在本篇文章中,您已经实现了一个自定义的MyThread类,该类扩展了Thread类。该类有三个属性,用于存储创建日期、执行开始日期和执行结束日期。使用开始日期和结束日期属性,您已经实现了getExecutionTime()方法,该方法返回线程执行任务的时间。最后,您已重写了toString()方法以生成有关线程的信息。
一旦您拥有自己的线程类,您就实现了一个工厂来创建实现ThreadFactory接口的该类的对象。如果您要将工厂用作独立对象,则不一定要使用接口,但是如果您想要将此工厂与 Java 并发 API 的其他类一起使用,则必须通过实现该接口来构建您的工厂。ThreadFactory接口只有一个方法,即newThread()方法,该方法接收一个Runnable对象作为参数,并返回一个Thread对象来执行该Runnable对象。在您的情况下,您返回一个MyThread对象。
要检查这两个类,您已经实现了实现Runnable对象的MyTask类。这是由MyThread对象管理的线程要执行的任务。MyTask实例将其执行线程休眠 2 秒。
在示例的主方法中,您使用MyThreadFactory工厂创建了一个MyThread对象来执行一个Task对象。执行程序,您将看到一个带有线程开始日期和执行时间的消息。
以下屏幕截图显示了此示例生成的输出:
还有更多...
Java 并发 API 提供了Executors类来生成线程执行器,通常是ThreadPoolExecutor类的对象。您还可以使用此类来获取ThreadFactory接口的最基本实现,使用defaultThreadFactory()方法。此方法生成的工厂生成基本的Thread对象,它们都属于同一个ThreadGroup对象。
您可以在程序中使用ThreadFactory接口进行任何目的,不一定与 Executor 框架相关。
在 Executor 对象中使用我们的 ThreadFactory
在前一篇文章中,实现 ThreadFactory 接口以生成自定义线程,我们介绍了工厂模式,并提供了如何实现实现ThreadFactory接口的线程工厂的示例。
Executor 框架是一种允许您分离线程创建和执行的机制。它基于Executor和ExecutorService接口以及实现这两个接口的ThreadPoolExecutor类。它具有内部线程池,并提供方法,允许您将两种类型的任务发送到池化线程中进行执行。这两种类型的任务是:
-
实现
Runnable接口的类,以实现不返回结果的任务 -
实现
Callable接口的类,以实现返回结果的任务
在内部,Executor 框架使用ThreadFactory接口来创建它用于生成新线程的线程。在本篇文章中,您将学习如何实现自己的线程类、线程工厂来创建该类的线程,以及如何在执行器中使用该工厂,以便执行器将执行您的线程。
准备就绪...
阅读前一篇文章,实现 ThreadFactory 接口以生成自定义线程,并实现其示例。
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤来实现示例:
-
将在实现 ThreadFactory 接口以生成自定义线程中实现的
MyThread、MyThreadFactory和MyTask类复制到项目中,以便在此示例中使用它们。 -
通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
threadFactory的新MyThreadFactory对象。
MyThreadFactory threadFactory=new MyThreadFactory("MyThreadFactory");
- 使用
Executors类的newCachedThreadPool()方法创建一个新的Executor对象。将之前创建的工厂对象作为参数传递。新的Executor对象将使用该工厂来创建必要的线程,因此它将执行MyThread线程。
ExecutorService executor=Executors.newCachedThreadPool(threadFactory);
- 创建一个新的
Task对象,并使用submit()方法将其发送到执行器。
MyTask task=new MyTask();
executor.submit(task);
- 使用
shutdown()方法关闭执行器。
executor.shutdown();
- 使用
awaitTermination()方法等待执行器的完成。
executor.awaitTermination(1, TimeUnit.DAYS);
- 写一条消息来指示程序的结束。
System.out.printf("Main: End of the program.\n");
它是如何工作的...
在前一个示例的*How it works...*部分,实现 ThreadFactory 接口以生成自定义线程中,您可以阅读有关MyThread、MyThreadFactory和MyTask类如何工作的详细解释。
在示例的main()方法中,使用Executors类的newCachedThreadPool()方法创建了一个Executor对象。您已将之前创建的工厂对象作为参数传递,因此创建的Executor对象将使用该工厂来创建所需的线程,并执行MyThread类的线程。
执行程序,您将看到一个关于线程启动日期和执行时间的信息。以下截图显示了此示例生成的输出:
另请参阅
- 在第七章的自定义并发类中的实现 ThreadFactory 接口以生成自定义线程食谱中
自定义在定时线程池中运行的任务
定时线程池是 Executor 框架的基本线程池的扩展,允许您安排任务在一段时间后执行。它由ScheduledThreadPoolExecutor类实现,并允许执行以下两种类型的任务:
-
延迟任务:这种类型的任务在一段时间后只执行一次
-
周期性任务:这种类型的任务在延迟后定期执行
延迟任务可以执行Callable和Runnable对象,但周期性任务只能执行Runnable对象。定时池执行的所有任务都是RunnableScheduledFuture接口的实现。在此示例中,您将学习如何实现自己的RunnableScheduledFuture接口的实现来执行延迟和周期性任务。
准备工作
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE,如 NetBeans,请打开它并创建一个新的 Java 项目。
如何做...
按照下面描述的步骤来实现示例:
- 创建一个名为
MyScheduledTask的类,参数化为一个名为V的泛型类型。它扩展了FutureTask类并实现了RunnableScheduledFuture接口。
public class MyScheduledTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {
- 声明一个名为
task的私有RunnableScheduledFuture属性。
private RunnableScheduledFuture<V> task;
- 声明一个名为
executor的私有ScheduledThreadPoolExecutor。
private ScheduledThreadPoolExecutor executor;
- 声明一个名为
period的私有long属性。
private long period;
- 声明一个名为
startDate的私有long属性。
private long startDate;
- 实现一个类的构造函数。它接收一个将由任务执行的
Runnable对象,将由此任务返回的结果,将用于创建MyScheduledTask对象的RunnableScheduledFuture任务,以及将执行任务的ScheduledThreadPoolExecutor对象。调用其父类的构造函数并存储任务和executor属性。
public MyScheduledTask(Runnable runnable, V result, RunnableScheduledFuture<V> task, ScheduledThreadPoolExecutor executor) {
super(runnable, result);
this.task=task;
this.executor=executor;
}
- 实现
getDelay()方法。如果任务是一个周期性任务,并且startDate属性的值不为零,则计算返回值为startDate属性和实际日期之间的差值。否则,返回存储在task属性中的原始任务的延迟。不要忘记以参数传递的时间单位返回结果。
@Override
public long getDelay(TimeUnit unit) {
if (!isPeriodic()) {
return task.getDelay(unit);
} else {
if (startDate==0){
return task.getDelay(unit);
} else {
Date now=new Date();
long delay=startDate-now.getTime();
return unit.convert(delay, TimeUnit.MILLISECONDS);
}
}
}
- 实现
compareTo()方法。调用原始任务的compareTo()方法。
@Override
public int compareTo(Delayed o) {
return task.compareTo(o);
}
- 实现
isPeriodic()方法。调用原始任务的isPeriodic()方法。
@Override
public boolean isPeriodic() {
return task.isPeriodic();
}
- 实现
run()方法。如果是一个周期性任务,你必须更新它的startDate属性,以便将来执行任务的开始日期。计算方法是将实际日期和周期相加。然后,再次将任务添加到ScheduledThreadPoolExecutor对象的队列中。
@Override
public void run() {
if (isPeriodic() && (!executor.isShutdown())) {
Date now=new Date();
startDate=now.getTime()+period;
executor.getQueue().add(this);
}
- 在控制台中打印一条带有实际日期的消息,调用
runAndReset()方法执行任务,然后再次在控制台中打印一条带有实际日期的消息。
System.out.printf("Pre-MyScheduledTask: %s\n",new Date());
System.out.printf("MyScheduledTask: Is Periodic: %s\n",isPeriodic());
super.runAndReset();
System.out.printf("Post-MyScheduledTask: %s\n",new Date());
}
- 实现
setPeriod()方法来设定该任务的周期。
public void setPeriod(long period) {
this.period=period;
}
- 创建一个名为
MyScheduledThreadPoolExecutor的类,以实现执行MyScheduledTask任务的ScheduledThreadPoolExecutor对象。指定该类扩展ScheduledThreadPoolExecutor类。
public class MyScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
- 实现一个类的构造函数,它仅调用其父类的构造函数。
public MyScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize);
}
- 实现
decorateTask()方法。它接收一个将要执行的Runnable对象和将执行该Runnable对象的RunnableScheduledFuture任务作为参数。使用这些对象创建并返回一个MyScheduledTask任务。
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(Runnable runnable,
RunnableScheduledFuture<V> task) {
MyScheduledTask<V> myTask=new MyScheduledTask<V>(runnable, null, task,this);
return myTask;
}
- 重写
scheduledAtFixedRate()方法。调用其父类的方法,将返回的对象转换为MyScheduledTask对象,并使用setPeriod()方法设定该任务的周期。
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
ScheduledFuture<?> task= super.scheduleAtFixedRate(command, initialDelay, period, unit);
MyScheduledTask<?> myTask=(MyScheduledTask<?>)task;
myTask.setPeriod(TimeUnit.MILLISECONDS.convert(period,unit));
return task;
}
- 创建一个名为
Task的类,实现Runnable接口。
public class Task implements Runnable {
- 实现
run()方法。在任务开始时打印一条消息,让当前线程休眠 2 秒,然后在任务结束时再打印一条消息。
@Override
public void run() {
System.out.printf("Task: Begin.\n");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Task: End.\n");
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception{
- 创建一个名为
executor的MyScheduledThreadPoolExecutor对象。使用2作为参数,在池中有两个线程。
MyScheduledThreadPoolExecutor executor=new MyScheduledThreadPoolExecutor(2);
- 创建一个名为
task的Task对象。在控制台中写下实际日期。
Task task=new Task();
System.out.printf("Main: %s\n",new Date());
- 使用
schedule()方法向执行器发送一个延迟任务。该任务将在 1 秒延迟后执行。
executor.schedule(task, 1, TimeUnit.SECONDS);
- 让主线程休眠 3 秒。
TimeUnit.SECONDS.sleep(3);
- 创建另一个
Task对象。再次在控制台中打印实际日期。
task=new Task();
System.out.printf("Main: %s\n",new Date());
- 使用
scheduleAtFixedRate()方法向执行器发送一个周期性任务。该任务将在 1 秒延迟后执行,然后每 3 秒执行一次。
executor.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);
- 让主线程休眠 10 秒。
TimeUnit.SECONDS.sleep(10);
- 使用
shutdown()方法关闭执行器。使用awaitTermination()方法等待执行器的完成。
executor.shutdown();
executor.awaitTermination(1, TimeUnit.DAYS);
- 在控制台中写一条消息,指示程序结束。
System.out.printf("Main: End of the program.\n");
工作原理...
在这个示例中,您已经实现了MyScheduledTask类,以实现可以在ScheduledThreadPoolExecutor执行器上执行的自定义任务。该类扩展了FutureTask类并实现了RunnableScheduledFuture接口。它实现了RunnableScheduledFuture接口,因为在计划的执行器中执行的所有任务都必须实现该接口并扩展FutureTask类,因为该类提供了在RunnableScheduledFuture接口中声明的方法的有效实现。之前提到的所有接口和类都是参数化类,具有将由任务返回的数据类型。
要在计划的执行器中使用MyScheduledTask任务,您已经覆盖了MyScheduledThreadPoolExecutor类中的decorateTask()方法。该类扩展了ScheduledThreadPoolExecutor执行器,该方法提供了一种将ScheduledThreadPoolExecutor执行器实现的默认计划任务转换为MyScheduledTask任务的机制。因此,当您实现自己的计划任务版本时,必须实现自己的计划执行器的版本。
decorateTask()方法只是使用参数创建一个新的MyScheduledTask对象;一个将在任务中执行的Runnable对象。该任务将返回一个结果。在这种情况下,任务不会返回结果,因此使用了null值。原始任务用于执行Runnable对象。这是新对象将在池中替换的任务;将执行任务的执行器。在这种情况下,您使用this关键字引用创建任务的执行器。
MyScheduledTask类可以执行延迟和周期性任务。您已经实现了两种任务的所有必要逻辑的两种方法,它们是getDelay()和run()方法。
scheduled executor调用getDelay()方法来确定是否执行任务。此方法在延迟和周期性任务中的行为不同。正如我们之前提到的,MyScheduledClass类的构造函数接收原始的ScheduledRunnableFuture对象,该对象将执行Runnable对象,并将其存储为类的属性,以便访问其方法和数据。当要执行延迟任务时,getDelay()方法返回原始任务的延迟,但是对于周期性任务,getDelay()方法返回startDate属性与实际日期之间的差异。
run()方法是执行任务的方法。周期性任务的一个特点是,如果要再次执行任务,您必须将任务的下一次执行放入执行器的队列中作为新任务。因此,如果要执行周期性任务,您要确定startDate属性的值,将其添加到实际日期和任务执行的周期,并将任务再次存储在执行器的队列中。startDate属性存储了任务的下一次执行将开始的日期。然后,您使用FutureTask类提供的runAndReset()方法执行任务。对于延迟任务,您不必将它们放入执行器的队列中,因为它们只执行一次。
注意
您还必须考虑执行器是否已关闭。在这种情况下,您不必再将周期性任务存储到执行器的队列中。
最后,您已经覆盖了MyScheduledThreadPoolExecutor类中的scheduleAtFixedRate()方法。我们之前提到,对于周期性任务,您要使用任务的周期来确定startDate属性的值,但是您还没有初始化该周期。您必须覆盖此方法,该方法接收该周期作为参数,然后将其传递给MyScheduledTask类,以便它可以使用它。
该示例包括实现了Runnable接口的Task类,并且是在计划执行程序中执行的任务。示例的主类创建了一个MyScheduledThreadPoolExecutor执行程序,并将以下两个任务发送给它们:
-
一个延迟任务,1 秒后执行。
-
一个周期性任务,首次在实际日期后 1 秒执行,然后每 3 秒执行一次
以下屏幕截图显示了此示例的部分执行。您可以检查两种类型的任务是否被正确执行:
还有更多...
ScheduledThreadPoolExecutor类提供了decorateTask()方法的另一个版本,该方法接收Callable对象作为参数,而不是Runnable对象。
另请参阅
-
在延迟后在执行者中运行任务配方第四章, 线程执行者
-
在执行者中定期运行任务配方第四章, 线程执行者
实现 ThreadFactory 接口以为 Fork/Join 框架生成自定义线程
Java 7 最有趣的特性之一是 Fork/Join 框架。它是Executor和ExecutorService接口的实现,允许您执行Callable和Runnable任务,而无需管理执行它们的线程。
这个执行器旨在执行可以分成更小部分的任务。其主要组件如下:
-
ForkJoinTask类实现的一种特殊类型的任务。 -
将任务分成子任务的两个操作(
fork操作)和等待这些子任务完成(join操作)。 -
一种算法,称为工作窃取算法,它优化了线程池中线程的使用。当一个任务正在等待其子任务时,执行它的线程被用来执行另一个线程。
Fork/Join 框架的主类是ForkJoinPool类。在内部,它有以下两个元素:
-
一个等待执行的任务队列
-
执行任务的线程池
在本示例中,您将学习如何实现一个自定义的工作线程,用于ForkJoinPool类,并使用工厂来使用它。
准备工作
这个示例的实现是使用 Eclipse IDE 完成的。如果您使用 Eclipse 或其他 IDE,如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyWorkerThread的类,它扩展了ForkJoinWorkerThread类。
public class MyWorkerThread extends ForkJoinWorkerThread {
- 声明并创建一个私有的
ThreadLocal属性,参数为Integer类,命名为taskCounter。
private static ThreadLocal<Integer> taskCounter=new ThreadLocal<Integer>();
- 实现一个类的构造函数。
protected MyWorkerThread(ForkJoinPool pool) {
super(pool);
}
- 重写
onStart()方法。调用其父类的方法,在控制台中打印一条消息,并将此线程的taskCounter属性的值设置为零。
@Override
protected void onStart() {
super.onStart();
System.out.printf("MyWorkerThread %d: Initializing task counter.\n",getId());
taskCounter.set(0);
}
- 重写
onTermination()方法。在控制台中写入此线程的taskCounter属性的值。
@Override
protected void onTermination(Throwable exception) {
System.out.printf("MyWorkerThread %d: %d\n",getId(),taskCounter.get());
super.onTermination(exception);
}
- 实现
addTask()方法。增加taskCounter属性的值。
public void addTask(){
int counter=taskCounter.get().intValue();
counter++;
taskCounter.set(counter);
}
- 创建一个名为
MyWorkerThreadFactory的类,它实现了ForkJoinWorkerThreadFactory接口。实现newThread()方法。创建并返回一个MyWorkerThread对象。
public class MyWorkerThreadFactory implements ForkJoinWorkerThreadFactory {
@Override
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
return new MyWorkerThread(pool);
}
}
- 创建一个名为
MyRecursiveTask的类,它扩展了Integer类参数化的RecursiveTask类。
public class MyRecursiveTask extends RecursiveTask<Integer> {
- 声明一个名为
array的私有int数组。
private int array[];
- 声明两个私有的
int属性,命名为start和end。
private int start, end;
- 实现初始化其属性的类的构造函数。
public Task(int array[],int start, int end) {
this.array=array;
this.start=start;
this.end=end;
}
- 实现
compute()方法,对数组在开始和结束位置之间的所有元素求和。首先,将执行任务的线程转换为MyWorkerThread对象,并使用addTask()方法增加该线程的任务计数器。
@Override
protected Integer compute() {
Integer ret;
MyWorkerThread thread=(MyWorkerThread)Thread.currentThread();
thread.addTask();
}
- 实现
addResults()方法。计算并返回作为参数接收的两个任务结果的总和。
private Integer addResults(Task task1, Task task2) {
int value;
try {
value = task1.get().intValue()+task2.get().intValue();
} catch (InterruptedException e) {
e.printStackTrace();
value=0;
} catch (ExecutionException e) {
e.printStackTrace();
value=0;
}
- 让线程休眠 10 毫秒,并返回任务的结果。
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return value;
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
factory的MyWorkerThreadFactory对象。
MyWorkerThreadFactory factory=new MyWorkerThreadFactory();
- 创建一个名为
pool的ForkJoinPool对象。将之前创建的工厂对象传递给构造函数。
ForkJoinPool pool=new ForkJoinPool(4, factory, null, false);
- 创建一个包含 100,000 个整数的数组。将所有元素初始化为
1。
int array[]=new int[100000];
for (int i=0; i<array.length; i++){
array[i]=1;
}
- 创建一个新的
Task对象来对数组的所有元素求和。
MyRecursiveTask task=new MyRecursiveTask(array,0,array.length);
- 使用
execute()方法将任务发送到池中。
pool.execute(task);
- 使用
join()方法等待任务结束。
task.join();
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 使用
awaitTermination()方法等待执行器的完成。
pool.awaitTermination(1, TimeUnit.DAYS);
- 使用
get()方法在控制台中写入任务的结果。
System.out.printf("Main: Result: %d\n",task.get());
- 在控制台中写入一条消息,指示示例的结束。
System.out.printf("Main: End of the program\n");
它是如何工作的...
Fork/Join 框架使用的线程称为工作线程。Java 包括ForkJoinWorkerThread类,该类扩展了Thread类并实现了 Fork/Join 框架使用的工作线程。
在这个配方中,您已经实现了MyWorkerThread类,该类扩展了ForkJoinWorkerThread类并重写了该类的两个方法。您的目标是在每个工作线程中实现一个任务计数器,以便您知道每个工作线程执行了多少任务。您使用ThreadLocal属性实现了计数器。这样,每个线程都将以对程序员透明的方式拥有自己的计数器。
您已经重写了ForkJoinWorkerThread类的onStart()方法,以初始化任务计数器。当工作线程开始执行时,将调用此方法。您还重写了onTermination()方法,以将任务计数器的值打印到控制台。当工作线程完成执行时,将调用此方法。您还在MyWorkerThread类中实现了一个方法。addTask()方法增加每个线程的任务计数器。
ForkJoinPool类,与 Java 并发 API 中的所有执行程序一样,使用工厂创建其线程,因此如果要在ForkJoinPool类中使用MyWorkerThread线程,必须实现自己的线程工厂。对于 Fork/Join 框架,此工厂必须实现ForkJoinPool.ForkJoinWorkerThreadFactory类。您已经为此目的实现了MyWorkerThreadFactory类。这个类只有一个方法,用于创建一个新的MyWorkerThread对象。
最后,您只需使用您创建的工厂初始化一个ForkJoinPool类。您已经在Main类中使用ForkJoinPool类的构造函数完成了这一点。
以下屏幕截图显示了程序输出的一部分:
您可以看到ForkJoinPool对象已经执行了四个工作线程,以及每个工作线程执行了多少任务。
还有更多...
请注意,ForkJoinWorkerThread类提供的onTermination()方法在线程正常完成或抛出Exception异常时调用。该方法接收一个Throwable对象作为参数。如果参数取null值,则工作线程正常完成,但如果参数取值,则线程抛出异常。您必须包含必要的代码来处理这种情况。
另请参阅
-
在第五章的创建 Fork/Join 池配方中,Fork/Join 框架
-
在第一章的通过工厂创建线程配方中,线程管理
自定义在 Fork/Join 框架中运行的任务
执行器框架将任务的创建和执行分开。您只需实现Runnable对象并使用Executor对象。将Runnable任务发送到执行器,它将创建、管理和完成执行这些任务所需的线程。
Java 7 提供了 Fork/Join 框架中的一种特殊的执行器。该框架旨在使用分而治之的技术解决可以分解为较小任务的问题。在任务内部,您必须检查要解决的问题的大小,如果大于设定的大小,则将问题分成两个或更多任务,并使用框架执行这些任务。如果问题的大小小于设定的大小,则直接在任务中解决问题,然后可选择地返回结果。Fork/Join 框架实现了改进这类问题整体性能的工作窃取算法。
Fork/Join 框架的主要类是ForkJoinPool类。在内部,它具有以下两个元素:
-
等待执行的任务队列
-
执行任务的线程池
默认情况下,由ForkJoinPool类执行的任务是ForkJoinTask类的对象。您还可以将Runnable和Callable对象发送到ForkJoinPool类,但它们无法充分利用 Fork/Join 框架的所有优势。通常,您将向ForkJoinPool对象发送ForkJoinTask类的两个子类之一的对象:
-
RecursiveAction:如果您的任务不返回结果 -
RecursiveTask:如果您的任务返回结果
在本示例中,您将学习如何为 Fork/Join 框架实现自己的任务,实现一个扩展了ForkJoinTask类的任务,该任务测量并在控制台中写入其执行时间,以便您可以控制其演变。您还可以实现自己的 Fork/Join 任务来写入日志信息,获取任务中使用的资源,或者对任务的结果进行后处理。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyWorkerTask的类,并指定它扩展了参数为Void类型的ForkJoinTask类。
public abstract class MyWorkerTask extends ForkJoinTask<Void> {
- 声明一个名为
name的私有String属性,用于存储任务的名称。
private String name;
- 实现类的构造函数以初始化其属性。
public MyWorkerTask(String name) {
this.name=name;
}
- 实现
getRawResult()方法。这是ForkJoinTask类的抽象方法之一。由于MyWorkerTask任务不会返回任何结果,因此此方法必须返回null值。
@Override
public Void getRawResult() {
return null;
}
- 实现
setRawResult()方法。这是ForkJoinTask类的另一个抽象方法。由于MyWorkerTask任务不会返回任何结果,因此将此方法的主体留空。
@Override
protected void setRawResult(Void value) {
}
- 实现
exec()方法。这是任务的主要方法。在这种情况下,将任务的逻辑委托给compute()方法。计算该方法的执行时间并将其写入控制台。
@Override
protected boolean exec() {
Date startDate=new Date();
compute();
Date finishDate=new Date();
long diff=finishDate.getTime()-startDate.getTime();
System.out.printf("MyWorkerTask: %s : %d Milliseconds to complete.\n",name,diff);
return true;
}
- 实现
getName()方法以返回任务的名称。
public String getName(){
return name;
}
- 声明抽象方法
compute()。如前所述,此方法将实现任务的逻辑,并且必须由MyWorkerTask类的子类实现。
protected abstract void compute();
- 创建一个名为
Task的类,该类扩展了MyWorkerTask类。
public class Task extends MyWorkerTask {
- 声明一个名为
array的私有int值数组。
private int array[];
- 实现一个初始化其属性的类的构造函数。
public Task(String name, int array[], int start, int end){
super(name);
this.array=array;
this.start=start;
this.end=end;
}
- 实现
compute()方法。此方法增加由开始和结束属性确定的数组元素块。如果此元素块的元素超过 100 个,则将该块分成两部分,并创建两个Task对象来处理每个部分。使用invokeAll()方法将这些任务发送到池中。
protected void compute() {
if (end-start>100){
int mid=(end+start)/2;
Task task1=new Task(this.getName()+"1",array,start,mid);
Task task2=new Task(this.getName()+"2",array,mid,end);
invokeAll(task1,task2);
- 如果元素块少于 100 个,则使用
for循环增加所有元素。
} else {
for (int i=start; i<end; i++) {
array[i]++;
}
- 最后,让执行任务的线程休眠 50 毫秒。
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个包含 10,000 个元素的
int数组。
int array[]=new int[10000];
- 创建一个名为
pool的ForkJoinPool对象。
ForkJoinPool pool=new ForkJoinPool();
- 创建一个
Task对象来增加数组的所有元素。构造函数的参数是Task作为任务的名称,数组对象和值0和10000,以指示该任务必须处理整个数组。
Task task=new Task("Task",array,0,array.length);
- 使用
execute()方法将任务发送到池中。
pool.invoke(task);
- 使用
shutdown()方法关闭池。
pool.shutdown();
- 在控制台中写入一条消息,指示程序的结束。
System.out.printf("Main: End of the program.\n");
工作原理...
在此配方中,您已实现了MyWorkerTask类,该类扩展了ForkJoinTask类。这是您自己的基类,用于实现可以在ForkJoinPool执行程序中执行并且可以利用该执行程序的所有优势的任务,如工作窃取算法。该类相当于RecursiveAction和RecursiveTask类。
当您扩展ForkJoinTask类时,您必须实现以下三种方法:
-
setRawResult(): 此方法用于设置任务的结果。由于您的任务不返回任何结果,因此您将此方法留空。 -
getRawResult(): 此方法用于返回任务的结果。由于您的任务不返回任何结果,因此此方法返回null值。 -
exec(): 此方法实现任务的逻辑。在您的情况下,您已将逻辑委托给抽象方法compute()(如RecursiveAction和RecursiveTask类),并且在exec()方法中测量该方法的执行时间,并将其写入控制台。
最后,在示例的主类中,您已创建了一个包含 10,000 个元素的数组,一个ForkJoinPool执行程序和一个Task对象来处理整个数组。执行程序,您将看到执行的不同任务如何在控制台中写入它们的执行时间。
另请参阅
-
第五章中的创建 Fork/Join 池配方,Fork/Join Framework
-
第七章中的实现 ThreadFactory 接口以为 Fork/Join 框架生成自定义线程配方,自定义并发类
实现自定义锁类
锁是 Java 并发 API 提供的基本同步机制之一。它允许程序员保护代码的临界区,因此只有一个线程可以一次执行该代码块。它提供以下两个操作:
-
lock(): 当您想要访问临界区时,调用此操作。如果有另一个线程正在运行该临界区,则其他线程将被阻塞,直到它们被锁唤醒以访问临界区。 -
unlock(): 在临界区的末尾调用此操作,以允许其他线程访问临界区。
在 Java 并发 API 中,锁在Lock接口中声明,并在一些类中实现,例如ReentrantLock类。
在此配方中,您将学习如何实现自己的Lock对象,该对象实现了实现Lock接口的类,可用于保护临界区。
准备工作
此示例已使用 Eclipse IDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyQueuedSynchronizer的类,它继承AbstractQueuedSynchronizer类。
public class MyAbstractQueuedSynchronizer extends AbstractQueuedSynchronizer {
- 声明一个名为
state的私有AtomicInteger属性。
private AtomicInteger state;
- 实现类的构造函数以初始化其属性。
public MyAbstractQueuedSynchronizer() {
state=new AtomicInteger(0);
}
- 实现
tryAcquire()方法。此方法尝试将状态变量的值从零更改为一。如果可以,它返回true值,否则返回false。
@Override
protected boolean tryAcquire(int arg) {
return state.compareAndSet(0, 1);
}
- 实现
tryRelease()方法。此方法尝试将状态变量的值从一更改为零。如果可以,则返回true值,否则返回false值。
@Override
protected boolean tryRelease(int arg) {
return state.compareAndSet(1, 0);
}
- 创建一个名为
MyLock的类,并指定它实现Lock接口。
public class MyLock implements Lock{
- 声明一个名为
sync的私有AbstractQueuedSynchronizer属性。
private AbstractQueuedSynchronizer sync;
- 通过使用新的
MyAbstractQueueSynchronizer对象初始化sync属性来实现类的构造函数。
public MyLock() {
sync=new MyAbstractQueuedSynchronizer();
}
- 实现
lock()方法。调用sync对象的acquire()方法。
@Override
public void lock() {
sync.acquire(1);
}
- 实现
lockInterruptibly()方法。调用sync对象的acquireInterruptibly()方法。
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
- 实现
tryLock()方法。调用sync对象的tryAcquireNanos()方法。
@Override
public boolean tryLock() {
try {
return sync.tryAcquireNanos(1, 1000);
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
}
- 使用两个参数实现
tryLock()方法的另一个版本。一个名为time的长参数和一个名为unit的TimeUnit参数。调用sync对象的tryAcquireNanos()方法。
@Override
public boolean tryLock(long time, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, TimeUnit.NANOSECONDS.convert(time, unit));
}
- 实现
unlock()方法。调用sync对象的release()方法。
@Override
public void unlock() {
sync.release(1);
}
- 实现
newCondition()方法。创建sync对象的内部类ConditionObject的新对象。
@Override
public Condition newCondition() {
return sync.new ConditionObject();
}
- 创建一个名为
Task的类,并指定它实现Runnable接口。
public class Task implements Runnable {
- 声明一个名为
lock的私有MyLock属性。
private MyLock lock;
- 声明一个名为
name的私有String属性。
private String name;
- 实现类的构造函数以初始化其属性。
public Task(String name, MyLock lock){
this.lock=lock;
this.name=name;
}
- 实现类的
run()方法。获取锁,使线程休眠 2 秒,然后释放lock对象。
@Override
public void run() {
lock.lock();
System.out.printf("Task: %s: Take the lock\n",name);
try {
TimeUnit.SECONDS.sleep(2);
System.out.printf("Task: %s: Free the lock\n",name);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) {
- 创建一个名为
lock的MyLock对象。
MyLock lock=new MyLock();
- 创建并执行 10 个
Task任务。
for (int i=0; i<10; i++){
Task task=new Task("Task-"+i,lock);
Thread thread=new Thread(task);
thread.start();
}
- 尝试使用
tryLock()方法获取锁。等待一秒钟,如果没有获得锁,则写一条消息并重试。
boolean value;
do {
try {
value=lock.tryLock(1,TimeUnit.SECONDS);
if (!value) {
System.out.printf("Main: Trying to get the Lock\n");
}
} catch (InterruptedException e) {
e.printStackTrace();
value=false;
}
} while (!value);
- 写一条消息指示您已获得锁并释放它。
System.out.printf("Main: Got the lock\n");
lock.unlock();
- 写一条消息指示程序结束。
System.out.printf("Main: End of the program\n");
它是如何工作的...
Java 并发 API 提供了一个类,可用于实现具有锁或信号量特性的同步机制。它是AbstractQueuedSynchronizer,正如其名称所示,它是一个抽象类。它提供了控制对临界区的访问以及管理被阻塞等待对临界区的访问的线程队列的操作。这些操作基于两个抽象方法:
-
tryAcquire():调用此方法尝试访问临界区。如果调用它的线程可以访问临界区,则该方法返回true值。否则,该方法返回false值。 -
tryRelease():调用此方法尝试释放对临界区的访问。如果调用它的线程可以释放访问,则该方法返回true值。否则,该方法返回false值。
在这些方法中,您必须实现用于控制对临界区的访问的机制。在您的情况下,您已经实现了MyQueuedSynchonizer类,该类扩展了AbstractQueuedSyncrhonizer类,并使用AtomicInteger变量实现了抽象方法,以控制对临界区的访问。如果锁是空闲的,该变量将存储0值,因此线程可以访问临界区,如果锁被阻塞,该变量将存储1值,因此线程无法访问临界区。
您已经使用了AtomicInteger类提供的compareAndSet()方法,该方法尝试将您指定为第一个参数的值更改为您指定为第二个参数的值。要实现tryAcquire()方法,您尝试将原子变量的值从零更改为一。同样,要实现tryRelease()方法,您尝试将原子变量的值从一更改为零。
你必须实现这个类,因为AbstractQueuedSynchronizer类的其他实现(例如ReentrantLock类使用的实现)是作为私有类在使用它的类内部实现的,所以你无法访问它。
然后,你已经实现了MyLock类。这个类实现了Lock接口,并有一个MyQueuedSynchronizer对象作为属性。为了实现Lock接口的所有方法,你使用了MyQueuedSynchronizer对象的方法。
最后,你已经实现了Task类,它实现了Runnable接口,并使用MyLock对象来访问临界区。该临界区使线程休眠 2 秒。主类创建了一个MyLock对象,并运行了 10 个共享该锁的Task对象。主类还尝试使用tryLock()方法来访问锁。
当你执行示例时,你会看到只有一个线程可以访问临界区,当该线程完成时,另一个线程将获得访问权限。
你可以使用自己的Lock来编写关于其使用情况的日志消息,控制锁定的时间,或实现高级的同步机制,例如控制对资源的访问,使其只在特定时间可用。
还有更多...
AbstractQueuedSynchronizer类提供了两个方法来管理锁的状态。它们是getState()和setState()方法。这些方法接收并返回一个整数值,表示锁的状态。你可以使用这些方法来代替AtomicInteger属性来存储锁的状态。
Java 并发 API 提供了另一个类来实现同步机制。它是AbstractQueuedLongSynchronizer类,它相当于AbstractQueuedSynchronizer类,但使用long属性来存储线程的状态。
另请参阅
- 在第二章的Synchronizing a block of code with locks食谱中,基本线程同步
基于优先级实现传输队列
Java 7 API 提供了几种数据结构来处理并发应用程序。其中,我们想要强调以下两种数据结构:
-
LinkedTransferQueue:这种数据结构应该在具有生产者/消费者结构的程序中使用。在这些应用程序中,你有一个或多个数据的生产者和一个或多个数据的消费者,一个数据结构被所有人共享。生产者将数据放入数据结构,消费者从数据结构中取数据。如果数据结构为空,消费者将被阻塞,直到有数据可供消费。如果数据结构已满,生产者将被阻塞,直到有空间放置他们的数据。 -
PriorityBlockingQueue:在这种数据结构中,元素以有序方式存储。元素必须实现带有compareTo()方法的Comparable接口。当你将一个元素插入结构中时,它会与结构中的元素进行比较,直到找到它的位置。
LinkedTransferQueue的元素按照它们到达的顺序存储,所以先到达的元素先被消耗。当你想要开发一个生产者/消费者程序时,数据根据某种优先级而不是到达时间进行消耗时,可能会出现这种情况。在这个示例中,你将学习如何实现一个数据结构,用于解决生产者/消费者问题,其元素将按照它们的优先级进行排序。具有更高优先级的元素将首先被消耗。
准备工作
这个示例已经在 Eclipse IDE 中实现。如果你使用 Eclipse 或其他 IDE,比如 NetBeans,打开它并创建一个新的 Java 项目。
如何做...
按照以下步骤实现示例:
- 创建一个名为
MyPriorityTransferQueue的类,该类扩展了PriorityBlockingQueue类并实现了TransferQueue接口。
public class MyPriorityTransferQueue<E> extends PriorityBlockingQueue<E> implements
TransferQueue<E> {
- 声明一个私有的
AtomicInteger属性,名为counter,用于存储等待消费元素的消费者数量。
private AtomicInteger counter;
- 声明一个私有的
LinkedBlockingQueue属性,名为transferred。
private LinkedBlockingQueue<E> transfered;
- 声明一个私有的
ReentrantLock属性,名为lock。
private ReentrantLock lock;
- 实现类的构造函数以初始化其属性。
public MyPriorityTransferQueue() {
counter=new AtomicInteger(0);
lock=new ReentrantLock();
transfered=new LinkedBlockingQueue<E>();
}
- 实现
tryTransfer()方法。该方法尝试立即将元素发送给等待的消费者,如果可能的话。如果没有等待的消费者,该方法返回false值。
@Override
public boolean tryTransfer(E e) {
lock.lock();
boolean value;
if (counter.get()==0) {
value=false;
} else {
put(e);
value=true;
}
lock.unlock();
return value;
}
- 实现
transfer()方法。该方法尝试立即将元素发送给等待的消费者,如果可能的话。如果没有等待的消费者,该方法将元素存储在一个特殊的队列中,以便发送给尝试获取元素并阻塞线程直到元素被消费的第一个消费者。
@Override
public void transfer(E e) throws InterruptedException {
lock.lock();
if (counter.get()!=0) {
put(e);
lock.unlock();
} else {
transfered.add(e);
lock.unlock();
synchronized (e) {
e.wait();
}
}
}
- 实现
tryTransfer()方法,该方法接收三个参数:元素、等待消费者的时间(如果没有)和用于指定时间的时间单位。如果有等待的消费者,它立即发送元素。否则,将指定的时间转换为毫秒,并使用wait()方法使线程进入休眠状态。当消费者取走元素时,如果线程正在wait()方法中休眠,你将使用notify()方法唤醒它,稍后会看到。
@Override
public boolean tryTransfer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
lock.lock();
if (counter.get()!=0) {
put(e);
lock.unlock();
return true;
} else {
transfered.add(e);
long newTimeout= TimeUnit.MILLISECONDS.convert(timeout, unit);
lock.unlock();
e.wait(newTimeout);
lock.lock();
if (transfered.contains(e)) {
transfered.remove(e);
lock.unlock();
return false;
} else {
lock.unlock();
return true;
}
}
}
- 实现
hasWaitingConsumer()方法。使用计数属性的值来计算该方法的返回值。如果计数大于零,则返回true。否则,返回false。
@Override
public boolean hasWaitingConsumer() {
return (counter.get()!=0);
}
- 实现
getWaitingConsumerCount()方法。返回counter属性的值。
@Override
public int getWaitingConsumerCount() {
return counter.get();
}
- 实现
take()方法。消费者想要消费元素时调用此方法。首先获取之前定义的锁,并增加等待消费者的数量。
@Override
public E take() throws InterruptedException {
lock.lock();
counter.incrementAndGet();
- 如果在转移队列中没有任何元素,则释放锁并尝试从队列中获取元素,使用
take()方法,并再次获取锁。如果队列中没有任何元素,该方法将使线程进入休眠状态,直到有元素可供消费。
E value=transfered.poll();
if (value==null) {
lock.unlock();
value=super.take();
lock.lock();
- 否则,从转移队列中取出元素,并唤醒正在等待消费该元素的线程(如果有的话)。
} else {
synchronized (value) {
value.notify();
}
}
- 最后,减少等待消费者的计数并释放锁。
counter.decrementAndGet();
lock.unlock();
return value;
}
- 实现一个名为
Event的类,该类实现了参数化为Event类的Comparable接口。
public class Event implements Comparable<Event> {
- 声明一个私有的
String属性,名为thread,用于存储创建事件的线程的名称。
private String thread;
- 声明一个私有的
int属性,名为priority,用于存储事件的优先级。
private int priority;
- 实现类的构造函数以初始化其属性。
public Event(String thread, int priority){
this.thread=thread;
this.priority=priority;
}
- 实现一个方法来返回
thread属性的值。
public String getThread() {
return thread;
}
- 实现一个方法来返回
priority属性的值。
public int getPriority() {
return priority;
}
- 实现
compareTo()方法。该方法将实际事件与作为参数接收的事件进行比较。如果实际事件的优先级高于参数,则返回-1,如果实际事件的优先级低于参数,则返回1,如果两个事件具有相同的优先级,则返回0。你将按优先级降序获得列表。优先级较高的事件将首先存储在队列中。
public int compareTo(Event e) {
if (this.priority>e.getPriority()) {
return -1;
} else if (this.priority<e.getPriority()) {
return 1;
} else {
return 0;
}
}
- 实现一个名为
Producer的类,该类实现了Runnable接口。
public class Producer implements Runnable {
- 声明一个私有的
MyPriorityTransferQueue属性,参数化为Event类,名为buffer,用于存储生产者生成的事件。
private MyPriorityTransferQueue<Event> buffer;
- 实现类的构造函数以初始化其属性。
public Producer(MyPriorityTransferQueue<Event> buffer) {
this.buffer=buffer;
}
- 实现类的
run()方法。使用创建顺序作为优先级创建 100 个Event对象(最新的事件将具有最高优先级),并使用put()方法将它们插入队列。
@Override
public void run() {
for (int i=0; i<100; i++) {
Event event=new Event(Thread.currentThread().getName(),i);
buffer.put(event);
}
}
- 实现一个名为
Consumer的类,该类实现了Runnable接口。
public class Consumer implements Runnable {
- 声明一个私有的
MyPriorityTransferQueue属性,参数化为Event类,命名为 buffer,以获取此类消耗的事件。
private MyPriorityTransferQueue<Event> buffer;
- 实现类的构造函数以初始化其属性。
public Consumer(MyPriorityTransferQueue<Event> buffer) {
this.buffer=buffer;
}
- 实现
run()方法。它使用take()方法消耗 1002 个Events(在示例中生成的所有事件),并在控制台中写入生成事件的线程编号和其优先级。
@Override
public void run() {
for (int i=0; i<1002; i++) {
try {
Event value=buffer.take();
System.out.printf("Consumer: %s: %d\n",value.getThread(),value.getPriority());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 通过创建一个名为
Main的类和一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建一个名为
buffer的MyPriorityTransferQueue对象。
MyPriorityTransferQueue<Event> buffer=new MyPriorityTransferQueue<Event>();
- 创建一个
Producer任务并启动 10 个线程来执行该任务。
Producer producer=new Producer(buffer);
Thread producerThreads[]=new Thread[10];
for (int i=0; i<producerThreads.length; i++) {
producerThreads[i]=new Thread(producer);
producerThreads[i].start();
}
- 创建并启动一个
Consumer任务。
Consumer consumer=new Consumer(buffer);
Thread consumerThread=new Thread(consumer);
consumerThread.start();
- 在控制台中写入实际消费者计数。
System.out.printf("Main: Buffer: Consumer count: %d\n",buffer.getWaitingConsumerCount());
- 使用
transfer()方法向消费者传输事件。
Event myEvent=new Event("Core Event",0);
buffer.transfer(myEvent);
System.out.printf("Main: My Event has ben transfered.\n");
- 使用
join()方法等待生产者的完成。
for (int i=0; i<producerThreads.length; i++) {
try {
producerThreads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 使线程休眠 1 秒。
TimeUnit.SECONDS.sleep(1);
- 写入实际消费者计数。
System.out.printf("Main: Buffer: Consumer count: %d\n",buffer.getWaitingConsumerCount());
- 使用
transfer()方法传输另一个事件。
myEvent=new Event("Core Event 2",0);
buffer.transfer(myEvent);
- 使用
join()方法等待消费者的完成。
consumerThread.join();
- 写一条消息指示程序的结束。
System.out.printf("Main: End of the program\n");
工作原理...
在这个示例中,您已经实现了MyPriorityTransferQueue数据结构。这是一个用于生产者/消费者问题的数据结构,但其元素按优先级而不是到达顺序排序。由于 Java 不允许多重继承,您的第一个决定是MyPriorityTransferQueue类的基类。您扩展了PriorityBlockingQueue类,以实现按优先级将元素插入结构的操作。您还实现了TransferQueue接口以添加与生产者/消费者相关的方法。
MyPriortyTransferQueue类具有以下三个属性:
-
一个名为
counter的AtomicInteger属性:此属性存储等待从数据结构中获取元素的消费者数量。当消费者调用take()操作从数据结构中获取元素时,计数器会递增。当消费者完成take()操作的执行时,计数器再次递减。此计数器用于实现hasWaitingConsumer()和getWaitingConsumerCount()方法。 -
名为
lock的ReentrantLock属性:此属性用于控制对实现的操作的访问。只有一个线程可以使用数据结构。 -
最后,创建一个
LinkedBlockingQueue列表来存储传输的元素。
您已经在MyPriorityTransferQueue中实现了一些方法。所有这些方法都在TransferQueue接口中声明,并且take()方法在PriorityBlockingQueue接口中实现。其中两个已在前面描述过。以下是其余方法的描述:
-
tryTransfer(E``e):此方法尝试直接将一个元素发送给消费者。如果有消费者在等待,该方法将元素存储在优先级队列中,以便立即被消费者消费,然后返回true值。如果没有消费者在等待,该方法返回false值。 -
transfer(E``e):此方法直接将一个元素传输给消费者。如果有消费者在等待,该方法将元素存储在优先级队列中,以便立即被消费者消费。否则,该元素将存储在传输元素的列表中,并且线程将被阻塞,直到元素被消费。当线程处于休眠状态时,您必须释放锁,否则会阻塞队列。 -
tryTransfer(E``e,``long``timeout,``TimeUnit``unit):此方法类似于transfer()方法,但线程会阻塞由其参数确定的时间段。当线程处于休眠状态时,您必须释放锁,否则会阻塞队列。 -
take(): 该方法返回要消耗的下一个元素。如果传输元素列表中有元素,则要消耗的元素将从该列表中取出。否则,它将从优先级队列中取出。
实现数据结构后,您已经实现了Event类。这是您在数据结构中存储的元素的类。Event类有两个属性,用于存储生产者的 ID 和事件的优先级,并实现了Comparable接口,因为这是数据结构的要求。
然后,您已经实现了Producer和Consumer类。在示例中,您有 10 个生产者和一个消费者,它们共享相同的缓冲区。每个生产者生成 100 个具有增量优先级的事件,因此具有更高优先级的事件是最后生成的。
示例的主类创建了一个MyPriorityTransferQueue对象,10 个生产者和一个消费者,并使用MyPriorityTransferQueue缓冲区的transfer()方法将两个事件传输到缓冲区。
以下屏幕截图显示了程序执行的部分输出:
您可以看到,具有更高优先级的事件首先被消耗,并且消费者消耗了传输的事件。
另请参阅
-
第六章中的使用按优先级排序的阻塞线程安全列表配方,并发集合
-
第六章中的使用阻塞线程安全列表配方,并发集合
实现自己的原子对象
原子变量是在 Java 版本 5 中引入的,它们对单个变量提供原子操作。当线程对原子变量进行操作时,类的实现包括一个机制来检查操作是否在一步中完成。基本上,该操作获取变量的值,将值更改为本地变量,然后尝试将旧值更改为新值。如果旧值仍然相同,则进行更改。如果不是,则该方法重新开始操作。
在本配方中,您将学习如何扩展原子对象以及如何实现遵循原子对象机制的两个操作,以确保所有操作都在一步中完成。
准备就绪
本配方的示例是使用 Eclipse IDE 实现的。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
操作步骤...
按照以下步骤实现示例:
- 创建一个名为
ParkingCounter的类,并指定它扩展AtomicInteger类。
public class ParkingCounter extends AtomicInteger {
- 声明一个名为
maxNumber的私有int属性,以存储停车场中允许的最大汽车数量。
private int maxNumber;
- 实现类的构造函数以初始化其属性。
public ParkingCounter(int maxNumber){
set(0);
this.maxNumber=maxNumber;
}
- 实现
carIn()方法。如果计数器的值小于设定的最大值,则该方法递增汽车的计数器。构建一个无限循环,并使用get()方法获取内部计数器的值。
public boolean carIn() {
for (;;) {
int value=get();
- 如果该值等于
maxNumber属性,则无法递增计数器(停车场已满,汽车无法进入)。该方法返回false值。
if (value==maxNumber) {
System.out.printf("ParkingCounter: The parking lot is full.\n");
return false;
- 否则,增加该值并使用
compareAndSet()方法将旧值更改为新值。该方法返回false值;计数器未递增,因此必须重新开始循环。如果返回true值,则表示已进行更改,然后返回true值。
} else {
int newValue=value+1;
boolean changed=compareAndSet(value,newValue);
if (changed) {
System.out.printf("ParkingCounter: A car has entered.\n");
return true;
}
}
}
}
- 实现
carOut()方法。如果计数器的值大于0,则该方法递减汽车的计数器。构建一个无限循环,并使用get()方法获取内部计数器的值。
public boolean carOut() {
for (;;) {
int value=get();
if (value==0) {
System.out.printf("ParkingCounter: The parking lot is empty.\n");
return false;
} else {
int newValue=value-1;
boolean changed=compareAndSet(value,newValue);
if (changed) {
System.out.printf("ParkingCounter: A car has gone out.\n");
return true;
}
}
}
}
- 创建一个名为
Sensor1的类,该类实现Runnable接口。
public class Sensor1 implements Runnable {
- 声明一个名为
counter的私有ParkingCounter属性。
private ParkingCounter counter;
- 实现类的构造函数以初始化其属性。
public Sensor1(ParkingCounter counter) {
this.counter=counter;
}
- 实现
run()方法。多次调用carIn()和carOut()操作。
@Override
public void run() {
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carOut();
counter.carOut();
counter.carOut();
counter.carIn();
counter.carIn();
counter.carIn();
}
- 创建一个名为
Sensor2的类,实现Runnable接口。
public class Sensor2 implements Runnable {
- 声明一个名为
counter的私有ParkingCounter属性。
private ParkingCounter counter;
- 实现类的构造函数以初始化其属性。
public Sensor2(ParkingCounter counter) {
this.counter=counter;
}
- 实现
run()方法。多次调用carIn()和carOut()操作。
@Override
public void run() {
counter.carIn();
counter.carOut();
counter.carOut();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
counter.carIn();
}
- 通过创建一个名为
Main的类并实现一个main()方法来实现示例的主类。
public class Main {
public static void main(String[] args) throws Exception {
- 创建名为
counter的ParkingCounter对象。
ParkingCounter counter=new ParkingCounter(5);
- 创建并启动一个
Sensor1任务和一个Sensor2任务。
Sensor1 sensor1=new Sensor1(counter);
Sensor2 sensor2=new Sensor2(counter);
Thread thread1=new Thread(sensor1);
Thread thread2=new Thread(sensor2);
thread1.start();
thread2.start();
- 等待两个任务的最终确定。
thread1.join();
thread2.join();
- 在控制台中写入计数器的实际值。
System.out.printf("Main: Number of cars: %d\n",counter.get());
- 在控制台中写入指示程序结束的消息。
System.out.printf("Main: End of the program.\n");
工作原理...
ParkingCounter类扩展了AtomicInteger类,具有两个原子操作carIn()和carOut()。该示例模拟了一个控制停车场内汽车数量的系统。停车场可以容纳一定数量的汽车,由maxNumber属性表示。
carIn()操作将停车场内的实际汽车数量与最大值进行比较。如果它们相等,则汽车无法进入停车场,该方法返回false值。否则,它使用原子操作的以下结构:
-
将原子对象的值存储在本地变量中。
-
将新值存储在不同的变量中。
-
使用
compareAndSet()方法尝试用新值替换旧值。如果此方法返回true值,则您发送的旧值作为参数是变量的值,因此它会更改值。该操作以原子方式执行,因为carIn()方法返回true值。如果compareAndSet()方法返回false值,则您发送的旧值不是变量的值(其他线程对其进行了修改),因此该操作无法以原子方式执行。操作将重新开始,直到可以以原子方式执行为止。
carOut()方法类似于carIn()方法。您还实现了两个使用carIn()和carOut()方法来模拟停车活动的Runnable对象。当您执行程序时,您会发现停车场从未超过停车场内汽车的最大值。
另请参阅
- 在第六章中使用原子变量中的配方,并发集合