此篇博客为个人学习笔记,如有错误欢迎大家指正
本次内容
- 资源的并发访问控制
- 资源的多副本的并发访问控制
- 等待多个并发事件的完成
- 在集合点的同步
- 并发阶段任务的运行
- 并发阶段任务中的阶段切换
- 并发任务间的数据交换
1.资源的并发访问控制
在这一小节,我们会使用Java中提供的Semaphore(信号量)机制来保护一个或多个共享资源的访问。当我们的线程要进入被信号量保护的代码段时,必须要先获得信号量;在执行结束后,必须要记得释放信号量。这一点和之前我们使用锁来对代码进行同步有些类似。信号量内部存在一个计数器,我们可以在创建信号量时通过构造函数为其内部的计数器赋值,当线程试图获取信号量时,如果计数器的值大于0,则线程将获取信号量且计数器减一;反之如果小于等于0则无法获取信号量线程将阻塞,这时只有当其他线程释放信号量使其内置计数器加1后,其他线程才有机会获取信号量。我们调用acquire()方法来获取信号量,调用relase()方法来释放信号量,以上是我们常用的两个方法。当然,我么也可以通过第二个构造函数传入一个布尔类型的决定公平性的变量,我们之前已经介绍过锁的公平性,就不在此赘述了。另外Semaphore类中还有两个方法值得注意:
acquireUninterruptibly()方法在功能上和acquire()方法类似,都是试图获取信号量;不同的是:线程因调用acquire()方法而被阻塞时,可能会被中断因此会抛出InterruptedException异常,但acquireUninterruptibly()方法会忽略中断而且也不会抛出任何异常。tryAcquire()方法和我们之前提到的tryLock()方法类似。如果能获得信号量则返回true,否则返回false,无论是否能够获得信号量,线程都不会阻塞。
范例实现
PrintQueue类:
package day04.code_1;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class PrintQueue {
//信号量对象的声明
private final Semaphore semaphore;
//通过构造函数初始化信号量
public PrintQueue() {
//通过构造函数为信号量的计数器赋值
semaphore = new Semaphore(1);
}
public void printJob() {
try {
//获得信号量,此方法会抛出异常
semaphore.acquire();
//打印相关信息后休眠随机的时间
long duration = (long) (Math.random() * 10);
System.out.printf("%s: PrintQueue1: Printing a Job during " +
"%d seconds\n", Thread.currentThread().getName(), duration);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放信号量
semaphore.release();
}
}
}
Job类:
package day04.code_1;
public class Job implements Runnable {
//打印队列类的声明
private PrintQueue printQueue;
//通过构造函数传入打印队列对象的引用并初始化
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
//打印前输出相关信息
System.out.printf("%s: Going to print a job\n",
Thread.currentThread().getName());
//调用打印队列的打印方法
printQueue.printJob();
//打印后输出相关信息
System.out.printf("%s: The document has been printed\n",
Thread.currentThread().getName());
}
}
main函数:
package day04.code_1;
public class Main {
public static void main(String[] args) {
//创建打印队列
PrintQueue printQueue = new PrintQueue();
//创建工作类对象,并传入打印队列对象引用
Job job = new Job(printQueue);
//以工作类对象为参数创建十个线程并启动
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(job);
thread.start();
}
}
}
2.资源的多副本的并发访问控制
我们可以使用信号量来保护一个资源的多个副本,只需要改变调用构造函数时传入的参数即可。另外,acquire()、acquireUninterruptibly()、tryAcquire()、release()方法均存在另一种实现方式,我们可以将一个int型作为参数传入,用来表示在信号量计数器上增加或减少的数目。
范例实现
在这个范例中,我们对第一小节的打印队列进行了修改,使其可以最多同时被三个打印机使用
修改后的打印队列:
package day04.code_2;
import day04.code_1.PrintQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PrintQueue1 extends PrintQueue {
/*
* 布尔类型的数组
* 用来存放打印机的状态
* true为空闲,false为忙碌
* */
private boolean freePrinters[];
//用来同步操作上面数组的代码的锁
private Lock lockPrinters;
//信号量
private Semaphore semaphore;
public PrintQueue1() {
//这里设置最大同时可访问线程数为3
semaphore = new Semaphore(3);
//假设我们有三台打印机
freePrinters = new boolean[3];
//还没有开始工作,所以三台打印机均空闲
for (int i = 0; i < 3; i++) {
freePrinters[i] = true;
}
//创建一个锁对象
lockPrinters = new ReentrantLock();
}
public void printJob() {
try {
//获得信号量,此方法会抛出异常
semaphore.acquire();
//获取选中的打印机编号
int assignedPrinter = getPrinter();
//打印相关信息后休眠随机的时间
long duration = (long) (Math.random() * 10);
System.out.printf("%s: PrintQueue1: Printing a Job in %d Printer" +
"during " + "%d seconds\n",
Thread.currentThread().getName(), assignedPrinter, duration);
TimeUnit.SECONDS.sleep(duration);
//打印完成后将打印机状态重新置为空闲
freePrinters[assignedPrinter] = true;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放信号量
semaphore.release();
}
}
//查找空闲打印机的方法
private int getPrinter() {
//初始化查询结果变量
int ret = -1;
try {
//获取锁
lockPrinters.lock();
//遍历打印机状态数组
for (int i = 0; i < freePrinters.length; i++) {
//找到第一个状态为空闲的打印机
if (freePrinters[i]) {
ret = i;
freePrinters[i] = false;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lockPrinters.unlock();
}
//返回空闲的打印机编号
return ret;
}
}
main函数:
package day04.code_2;
import day04.code_1.Job;
import day04.code_1.PrintQueue;
public class Main {
public static void main(String[] args) {
//创建改进版打印队列
PrintQueue1 printQueue1 = new PrintQueue1();
//创建工作类对象,并传入打印队列对象引用
Job job = new Job(printQueue1);
//以工作类对象为参数创建十个线程并启动
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(job);
thread.start();
}
}
}
Job类和第一小节中的完全一致,就不再单独给出了
3.等待多个并发事件的完成
CountDownLatch类是Java为我们提供的一个同步辅助类,这个类并不是用来保护共享资源的,而是用来在某一个点同步多个线程的。它像一道一次性闸门,只有当我们预定的所有线程均到达这个闸门后,闸门才会打开放行所有线程并且再也不会关闭。在我们创建CountDownLatch类对象时,我们需要传入一个int型的参数用来表示我们需要等待线程的数目。CountDownLatch类中有两个方法比较常用:
await()方法被调用后,调用线程会进入阻塞状态,直到相应的CountDownLatch类对象开启countDown()方法被调用后,相应的CountDownLatch类对象中的计数器会减一,当计数器为0时,所有因相应对象进入阻塞的线程将会开启(闸门打开,放行所有阻塞的线程)
范例实现
在这个范例中,我们将创建一个会议线程和十个与会者线程,只有当十个与会者全部到场后,会议才可以开始。
会议类:
package day04.code_3;
import java.util.concurrent.CountDownLatch;
public class Videoconference implements Runnable {
private final CountDownLatch controller;
//通过构造函数创建CountDownLatch对象
public Videoconference(int number) {
//number为需要等待的线程数量
controller = new CountDownLatch(number);
}
@Override
public void run() {
//在会议类刚运行时打印需要到会的总人数
System.out.printf("VideoConference: Initialization: %d participants\n",
controller.getCount());
try {
//等待与会人员到达
controller.await();
//所有线程均到达,会议开始
System.out.printf("VideoConference: All the participants have come\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//与会者到达方法,name为与会者姓名
public void arrive(String name) {
//打印与会者到达的信息
System.out.printf("%s has arrived.\n", name);
//调用countDown方法使内置计数器减1
controller.countDown();
//打印未到会人数
System.out.printf("Waiting for %d participants\n",
controller.getCount());
}
}
与会者类:
package day04.code_3;
import java.util.concurrent.TimeUnit;
public class Participant implements Runnable {
//会议类
private Videoconference conference;
//与会者姓名
private String name;
//通过构造函数为两个成员变量赋值
public Participant(Videoconference conference, String name) {
this.conference = conference;
this.name = name;
}
@Override
public void run() {
//休眠随机时间表示前来参加会议所话费的时间
long duration = (long) (Math.random() * 10);
try {
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用会议对象的arrive方法使计数器减一
conference.arrive(name);
}
}
main函数:
package day04.code_3;
import java.util.concurrent.TimeUnit;
public class Participant implements Runnable {
//会议类
private Videoconference conference;
//与会者姓名
private String name;
//通过构造函数为两个成员变量赋值
public Participant(Videoconference conference, String name) {
this.conference = conference;
this.name = name;
}
@Override
public void run() {
//休眠随机时间表示前来参加会议所话费的时间
long duration = (long) (Math.random() * 10);
try {
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用会议对象的arrive方法使计数器减一
conference.arrive(name);
}
}
4.在集合点的同步
Java为我们提供了CyclicBarrier类,和CountDownLatch类相似,这个类也可以对多个线程在某个点上进行同步并且功能更加强大。如果将CountDownLatch类比作一次性的闸门,那么CyclicBarrier类就是一个可以重复利用的闸门,我们可以使用reset()方法对CyclicBarrier进行初始化,类似于重新关闭闸门的方法,调用此方法后因调用await()方法而阻塞的线程将会抛出BrokenBarrierException异常。CyclicBarrier类没有countDown()方法,所以我们不必也不能直接对计数器进行操作,当最后一个线程调用了await()方法后,CyclicBarrier对象会自动放行。CyclicBarrier类还有另一个构造函数,我们在传入计数器数值的同时还可以传入一个Runnable对象,闸门打开后CyclicBarrier对象将优先把这个Runnable对象作为一个线程来执行。另外还有三个方法需要我们熟悉:
getNumberWaiting():返回在await()方法上阻塞的线程数量getParties():返回在CyclicBarrier对象上同步的线程数量isBroken():返回CyclicBarrier对象是否已损坏。当有许多线程在await()方法上阻塞时,如果其中一个线程被中断抛出了InterruptedException异常,其他阻塞的线程此时会抛出BrokenBarrierException异常,这样我们的CyclicBarrier对象就处于损坏状态了
范例实现
在这个范例中,有一个很大的随机数矩阵,我们需要统计在矩阵中包含了多少个我们指定的数据。由于数据量较大,我们使用分治编程技术,将这个大矩阵分成五个子集,并创建五个线程分别对这五个子集进行搜索。使用CyclicBarrier对象对这五个线程进行同步,并在创建CyclicBarrier对象时传入的Runnable对象中统计五个子集的总个数最后打印结果。
大矩阵类:
package day04.code_4;
import java.util.Random;
public class MatrixMock {
//私有的二维数组data
private int[][] data;
//size为矩阵行数,length为列数,number为待查询的数据
public MatrixMock(int size, int length, int number) {
//counter用来记录待查询数据的个数
int counter = 0;
//根据参数创建一个二维数组
data = new int[size][length];
//创建随机数产生器
Random random = new Random();
//生成随机数填充数组
for (int i = 0; i < size; i++) {
for (int j = 0; j < length; j++) {
data[i][j] = random.nextInt(10);
//如果生成的随机数等于待查询数据,计数器加一
if (data[i][j] == number) {
counter++;
}
}
}
//打印待查询数据的数量,方便检验程序最终的结果
System.out.printf("Mock: There are %d ocurrences of number in" +
"generated data\n", counter);
}
//根据行号返回一行数据
public int[] getRow(int row) {
//判断行号是否存在
if ((row >= 0) && (row < data.length)) {
return data[row];
}
return null;
}
}
结果类:
package day04.code_4;
public class Results {
//一维数组用来储存每一行包含指定数据的个数
private int[] data;
//根据传入参数创建一维数组
public Results(int size) {
data = new int[size];
}
//position为行号,value为一行中存在指定数据的个数
public void setData(int position, int value) {
data[position] = value;
}
//返回结果数组
public int[] getData() {
return data;
}
}
搜索类:
package day04.code_4;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Searcher implements Runnable {
//搜索起始行(包括)
private int firstRow;
//搜索截止行(不包括)
private int lastRow;
//大矩阵
private MatrixMock mock;
//结果类对象
private Results results;
//待查询数据
private int number;
//用于同步的CyclicBarrier对象
private CyclicBarrier barrier;
public Searcher(int firstRow, int lastRow, MatrixMock mock,
Results results, int number, CyclicBarrier barrier) {
this.firstRow = firstRow;
this.lastRow = lastRow;
this.mock = mock;
this.results = results;
this.number = number;
this.barrier = barrier;
}
@Override
public void run() {
//声明一个计数器
int counter;
//打印搜索的起始、截止行
System.out.printf("%s: Processing lines from %d to %d\n",
Thread.currentThread().getName(), firstRow, lastRow);
for (int i = firstRow; i < lastRow; i++) {
//循环得到子集中每一行的数据
int[] row = mock.getRow(i);
//每一行都重新统计待查询数据的个数
counter = 0;
for (int j = 0; j < row.length; j++) {
if (row[j] == number)
counter++;
}
//查询一行后,写入结果
results.setData(i, counter);
}
//打印当前行查询结束信息
System.out.printf("%s: Lines processed\n",
Thread.currentThread().getName());
try {
//等待其他行查询结束
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
汇总类:
package day04.code_4;
public class Grouper implements Runnable {
//结果类
private Results results;
//构造方法
public Grouper(Results results) {
this.results = results;
}
@Override
public void run() {
//整个矩阵包含待查询数据的个数
int finalResult = 0;
//打印提示语句
System.out.println("Grouper: Processing results...");
//遍历一维结果数组,求和
int[] data = results.getData();
for (int d : data) {
finalResult += d;
}
//打印结果
System.out.printf("Grouper: Total result: %d\n",
finalResult);
}
}
main方法:
package day04.code_4;
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) {
//行数为10000行
final int ROWS = 10000;
//列数为1000列
final int LENGTH = 1000;
//待查询数据为5
final int SEARCH = 5;
//五个查询线程
final int PARTICIPANTS = 5;
//每个线程查询2000行
final int LINES_PARTICIPANT = 2000;
//创建大矩阵对象
MatrixMock mock = new MatrixMock(ROWS, LENGTH, SEARCH);
//创建结果对象
Results results = new Results(ROWS);
//创建汇总结果对象
Grouper grouper = new Grouper(results);
//创建CyclicBarrier对象并将结果对象作为第二个参数传入
CyclicBarrier barrier = new CyclicBarrier(PARTICIPANTS, grouper);
//创建五个线程开始搜索
for (int i = 0; i < 5; i++) {
Searcher searcher = new Searcher(i * LINES_PARTICIPANT,
(i + 1) * LINES_PARTICIPANT, mock, results, SEARCH, barrier);
Thread thread = new Thread(searcher);
thread.start();
}
//打印主线程结束提示语
System.out.println("Main: The main thread has finished");
}
}
5.并发阶段任务的运行
Phaser是一个更强大的同步辅助类,它可以对多阶段的并发任务进行同步。并且在程序运行过程中动态的对参与同步的线程数目进行改变。创建Phaser对象时,我们依然可以通过构造方法设置当前阶段需要被同步的线程数目。以下几个方法我们也需要了解:
arriveAndAwaitAdvance():线程调用此方法后,会通知Phaser对象此线程已完成当前阶段并进入阻塞状态,直到其他线程也完成当前阶段arriveAndDeregister():线程调用此方法后,会通知Phaser对象此线程以完成当前阶段且不再参与后续阶段的操作。因此,调用此方法的同时也减少了需要参与同步的线程数量isTerminated():返回一个布尔类型的值表示Phaser对象是否处于终止状态。当所有参与同步的线程均取消注册后,Phaser对象就处于终止状态了getPhase():获取当前阶段,Phaser类的阶段默认是从0开始的arrive():通知Phaser对象此线程已完成当前阶段但不会进入阻塞状态awaitAdvance(int phase):如果传入的阶段参数与当前所处阶段一致则线程阻塞直到其他线程均完成此阶段,否则将立刻返回。awaitAdvanceInterruptibly(int phase):此方法和awaitAdvance(int phase)一样,区别在于awaitAdvanceInterruptibly(int phase)会抛出InterruptedException异常,也就是会响应中断register():使Phaser对象上同步的线程数加一,新增的将默认没有执行完当前阶段的任务bulkRegister(int parties):和register()方法的区别在于此方法可以使Phaser对象增加指定数量的需同步线程数forceTermination():强制终止Phaser对象,此时awaitAdvance(int phase)和awaitAdvanceInterruptibly(int phase)会立刻返回一个负数,我们可以根据返回值判断Phaser对象是否被终止了。
范例实现
我们将创建三个线程去寻找指定文件夹中拓展名为exe且在一天内被修改过的文件。在整个搜索、整理过程中,我们会使用Phaser对象在不同阶段对线程进行同步。 文件搜索类:
package day04.code_5;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class FileSearch implements Runnable {
//查找的文件夹
private String initPath;
//待查找文件的拓展名
private String end;
//用来存储查找到的文件的完成路径
private List<String> results;
private Phaser phaser;
public FileSearch(String initPath, String end, Phaser phaser) {
this.initPath = initPath;
this.end = end;
this.phaser = phaser;
results = new ArrayList<>();
}
private void directoryProcess(File file) {
//找到当前文件夹下的所有文件和文件夹
File[] files = file.listFiles();
//如果文件夹为空则直接返回
if (files != null) {
//遍历文件数组
for (int i = 0; i < files.length; i++) {
//如果是文件夹则递归调用directoryProcess继续深入
if (files[i].isDirectory()) {
directoryProcess(files[i]);
} else {
//如果是文件则调用fileProcess判断是否为要找的文件
fileProcess(files[i]);
}
}
}
}
private void fileProcess(File file) {
//判断文件的拓展名是否符合查询要求
if (file.getName().endsWith(end)) {
//如果符合就填入集合中
results.add(file.getAbsolutePath());
}
}
private void filterResults() {
//创建一个新的集合对象
ArrayList<String> newResults = new ArrayList<>();
//得到当前时间
long time = new Date().getTime();
//遍历集合
for (int i = 0; i < results.size(); i++) {
//根据全路径得到文件
File file = new File(results.get(i));
//得到文件最后更改的时间
long fileData = file.lastModified();
//判断时间差是否小于一天
if (time - fileData < TimeUnit.MILLISECONDS.
convert(1, TimeUnit.DAYS)) {
//如果小于就添加到新的集合中
newResults.add(results.get(i));
}
}
//将新的集合赋值给结果
results = newResults;
}
private boolean checkResults() {
//如果结果为空
if (results.isEmpty()) {
//打印相关信息
System.out.printf("%s: Phase %d: 0 results\n",
Thread.currentThread().getName(),
phaser.getPhase());
System.out.printf("%s: Phase %d: End\n",
Thread.currentThread().getName(),
phaser.getPhase());
//通知phaser对象当前线程已完成当前阶段且不再参与接下来的操作
phaser.arriveAndDeregister();
return false;
} else {
//打印相关信息
System.out.printf("%s: Phase %d: %d results\n",
Thread.currentThread().getName(),
phaser.getPhase(), results.size());
//通知phaser对象当前线程已完成当前阶段并进入等待状态
phaser.arriveAndAwaitAdvance();
return true;
}
}
private void showInfo() {
//打印结果集合中的所有文件
for (String file : results) {
System.out.printf("%s: %s\n",
Thread.currentThread().getName(),
file);
}
//通知phaser对象当前线程已完成当前阶段并进入等待状态
phaser.arriveAndAwaitAdvance();
}
@Override
public void run() {
//在run方法第一句调用此方法保证所有线程同时开始
phaser.arriveAndAwaitAdvance();
//打印线程信息
System.out.printf("%s: Starting\n",
Thread.currentThread().getName());
//根据路径进行搜索
File file = new File(initPath);
if (file.isDirectory()) {
directoryProcess(file);
}
//检查结果是否为空,如果为空直接结束
if (!checkResults()) {
return;
}
//过滤第一次搜索出来的结果
filterResults();
//检查结果是否为空,如果为空直接结束
if (!checkResults()) {
return;
}
//打印结果
showInfo();
//通知phaser对象当前线程已完成当前阶段且不再参与接下来的操作
phaser.arriveAndDeregister();
//打印线程结束信息
System.out.printf("%s: Work completed\n",
Thread.currentThread().getName());
}
}
main方法:
package day04.code_5;
import java.util.concurrent.Phaser;
public class Main {
public static void main(String[] args) {
//创建phaser对象,并设置参与阶段同步的线程数为3
Phaser phaser = new Phaser(3);
//创建三个FileSearch对象并将其作为参数来创建线程并运行
FileSearch system = new FileSearch("C:\\", "exe", phaser);
FileSearch apps = new FileSearch("D:\\", "exe", phaser);
FileSearch files = new FileSearch("F:\\", "exe", phaser);
Thread systemThreaad = new Thread(system, "System");
systemThreaad.start();
Thread appsThreaad = new Thread(apps, "Apps");
appsThreaad.start();
Thread filesThreaad = new Thread(files, "Files");
filesThreaad.start();
//主线程等待三个线程运行结束
try {
systemThreaad.join();
appsThreaad.join();
filesThreaad.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印Phaser对象是否已经终止
System.out.println("Terminated: " + phaser.isTerminated());
}
}
6.并发阶段任务中的阶段切换
Phaser类中提供了onAdvance()方法,这个方法会在phaser阶段改变、休眠线程被唤醒前自动执行。方法会被传入两个int型的参数,第一个参数是当前阶段序号,第二个参数为注册的参与者数量。方法执行结束后会返回布尔类型的值来表示phaser对象是否已经终止,true表示终止,此时phaser对象仍然会唤醒休眠中的线程但状态已经变为终止。线程如果继续调用phaser的方法将会立即返回。我们可以建造我们自己的Phaser类并重写onAdvance()方法。
范例实现
在这个范例中,我们模拟了一场考试。当所有学生均到场后,考试开始;所有学生均答完一道题后,才可以继续向下作答;最后只有所有学生均答完试题才宣布考试结束。我们会使用自己编写的MyPhaser类在每一个阶段开始时打印提示信息。
学生类:
package day04.code_6;
import java.util.Date;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class Student implements Runnable {
//我们自己拓展的Phaser类
private MyPhaser myPhaser;
public Student(MyPhaser myPhaser) {
this.myPhaser = myPhaser;
}
@Override
public void run() {
//打印学生到达考场的信息
System.out.printf("%s: Has arrived to do the exam.%s\n",
Thread.currentThread().getName(), new Date());
//通知MyPhaser对象此线程已完成第0阶段任务并进入等待状态
myPhaser.arriveAndAwaitAdvance();
//被唤醒,进入第1阶段
//打印学生开始作答第一题的信息
System.out.printf("%s: Is going to do the first exercise.%s\n",
Thread.currentThread().getName(), new Date());
//休眠随机时间
doExercise();
//打印第一题作答完毕的信息
System.out.printf("%s: Has done the first exercise.%s\n",
Thread.currentThread().getName(), new Date());
//通知MyPhaser对象此线程已完成第1阶段任务并进入等待状态
myPhaser.arriveAndAwaitAdvance();
//被唤醒,进入第2阶段
//打印学生开始作答第二题的信息
System.out.printf("%s: Is going to do the second exercise.%s\n",
Thread.currentThread().getName(), new Date());
//休眠
doExercise();
//打印第二题作答完毕的信息
System.out.printf("%s: Has done the second exercise.%s\n",
Thread.currentThread().getName(), new Date());
//通知MyPhaser对象此线程已完成第2阶段任务并进入等待状态
myPhaser.arriveAndAwaitAdvance();
//被唤醒,进入第3阶段
//打印学生开始作答第三题的信息
System.out.printf("%s: Is going to do the third exercise.%s\n",
Thread.currentThread().getName(), new Date());
//休眠
doExercise();
//三题均答完,考试结束
System.out.printf("%s: Has done the third exercise.%s\n",
Thread.currentThread().getName(), new Date());
//等待其他线程完成当前阶段任务
myPhaser.arriveAndAwaitAdvance();
}
//休眠方法
private void doExercise() {
try {
long duration = (long) (Math.random() * 10);
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
MyPhaser类:
package day04.code_6;
import java.util.concurrent.Phaser;
public class MyPhaser extends Phaser {
@Override
//phase为当前阶段,registeredParties为注册数
protected boolean onAdvance(int phase, int registeredParties) {
//根据不同的阶段调用不同的方法
switch (phase) {
case 0:
return studentsArrived();
case 1:
return finishFirstExercise();
case 2:
return finishSecondExercise();
case 3:
return finishExam();
default:
return true;
}
}
private boolean studentsArrived() {
//打印学生均到达考场的信息
System.out.println("Phaser: The exam are going to start. " +
"The students are ready.");
System.out.printf("Phaser: We have %d students.\n",
this.getRegisteredParties());
return false;
}
private boolean finishFirstExercise() {
//打印所有学生均完成第一题的信息
System.out.println("Phaser: All the students have finished " +
"the first exercise.");
System.out.println("Phaser: It's time for the second one.");
return false;
}
private boolean finishSecondExercise() {
//打印所有学生均完成第二题的信息
System.out.println("Phaser: All the students have finished " +
"the second exercise.");
System.out.println("Phaser: It's time for the third one.");
return false;
}
private boolean finishExam() {
//打印所有学生均完成考试的信息
System.out.println("Phaser: All the students have finished " +
"the exam.");
System.out.println("Phaser: Thank for your time.");
return true;
}
}
main方法:
package day04.code_6;
public class Main {
public static void main(String[] args) {
//创建我们自己的Phaser对象
MyPhaser myPhaser = new MyPhaser();
//创建五个Student对象并装入数组
Student[] students = new Student[5];
for (int i = 0; i < students.length; i++) {
students[i] = new Student(myPhaser);
/*
* 我们在创建Phaser对象时并没有传入参数
* 因此调用register方法来增加注册数量
* */
myPhaser.register();
}
//创建五个线程并开启
Thread[] threads = new Thread[5];
for (int i = 0; i < students.length; i++) {
threads[i] = new Thread(students[i], "Student" + i);
threads[i].start();
}
//等待五个线程执行结束
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//打印MyPhaser对象的状态
System.out.printf("Main: The phaser has finished: %s.\n",
myPhaser.isTerminated());
}
}
7.并发任务间的数据交换
Exchanger也是一个同步辅助类,它可以帮助我们在两个线程之间交换数据。在使用此类交换数据时,我们需要在线程中定义同步点,在同步点两个线程可以交换数据。Exchanger类只能使两个线程之间交换数据,但这在一对一的消费者——生产者模式中是非常有用的。exchange(V data)方法会返回交换得到的数据,exchange(V data,long time,TimeUnit unit)方法也可以交换数据,不同之处在于此方法可以设置超时时间,一旦超过了指定的时间线程将不再等待。
范例实现
在此范例中,我们实现了一对一的生产者——消费者模式
生产者:
package day04.code_7;
import java.util.List;
import java.util.concurrent.Exchanger;
public class Producer implements Runnable {
//存储数据的容器
private List<String> buffer;
//交换器
private final Exchanger<List<String>> exchanger;
public Producer(List<String> buffer, Exchanger<List<String>> exchanger) {
this.buffer = buffer;
this.exchanger = exchanger;
}
@Override
public void run() {
//记录循环次数
int cycle = 1;
//总共需要交换十次数据
for (int i = 0; i < 10; i++) {
//打印这是第几次循环
System.out.printf("Producer: Cycle %d\n", cycle);
//将数组装入容器中
for (int j = 0; j < 10; j++) {
String message = "Event " + (i * 10 + j);
System.out.printf("Producer: %s\n", message);
buffer.add(message);
}
try {
//交换并得接收返回的数据
buffer = exchanger.exchange(buffer);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印交换容器后容器中的数据量
System.out.printf("Producer: %d\n", buffer.size());
cycle++;
}
}
}
消费者:
package day04.code_7;
import java.util.List;
import java.util.concurrent.Exchanger;
public class Consumer implements Runnable {
//存储数据的容器
private List<String> buffer;
//交换器
private final Exchanger<List<String>> exchanger;
public Consumer(List<String> buffer, Exchanger<List<String>> exchanger) {
this.buffer = buffer;
this.exchanger = exchanger;
}
@Override
public void run() {
//记录循环次数
int cycle = 1;
//总共需要交换十次数据
for (int i = 0; i < 10; i++) {
//打印这是第几次循环
System.out.printf("Consumer: Cycle %d\n", cycle);
try {
//交换并得接收返回的数据
buffer = exchanger.exchange(buffer);
} catch (InterruptedException e) {
e.printStackTrace();
}
//消费掉所有数据
for (int j = 0; j < 10; j++) {
String message = buffer.get(0);
System.out.printf("Consumer: %s\n", message);
buffer.remove(0);
}
cycle++;
}
}
}
main方法:
package day04.code_7;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Exchanger;
public class Main {
public static void main(String[] args) {
//为生产者、消费者各创建一个容器
ArrayList<String> buffer1 = new ArrayList<>();
ArrayList<String> buffer2 = new ArrayList<>();
//创建一个交换器
Exchanger<List<String>> exchanger = new Exchanger<>();
//创建生产者并作为参数传入线程构造函数,启动线程
Producer producer = new Producer(buffer1, exchanger);
Thread thread1 = new Thread(producer);
thread1.start();
//创建消费者并作为参数传入线程构造函数,启动线程
Consumer consumer = new Consumer(buffer2, exchanger);
Thread thread2 = new Thread(consumer);
thread2.start();
}
}