Java 下多线程的开发我们可以自己启用多线程,线程池,除此之外,Java还为我们提供了Fork-Join、CountDownLatch、CyclicBarrier等并发工具类。掌握并使用它们,有助于我们在进行并发线程开发的过程中,更加得心应手。
Fork-Join
Fork-Join是一个使用多线程的并发工具类,可以让我们不去了解诸如Thread,Runnable 等相关的知识,只要遵循forkjoin的开发模式,就可以写出很好的多线程并发程序,
分而治之
forkjoin 在处理某一类问题时非常的有用,哪一类问题?分而治之的问题。分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n 的问题,若该问题可以容易地解决(比如说规模n 较小)则直接解决,否则将其分解为k 个规模较小的子问题,这些子问题互相独立且与原问题形式相同(子问题相互之间有联系就会变为动态规范算法),递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并,与之对应的还有多路归并。
对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。为了提升性能,有时我们在半子表的个数小于某个数(比如15)的情况下,对半子表的排序采用其他排序算法,比如插入排序。
// 归并排序,递归实现
public static void sortMergeRecursion(int[] nums) {
sortMergeRecursionHelper(nums, 0, nums.length - 1);
}
public static void sortMergeRecursionHelper(int[] nums,int left, int right) {
if(left == right) return; // 当待排序的序列长度为1时,递归开始回溯,进行merge
int middle = left + (right - left) / 2;
sortMergeRecursionHelper(nums, left, middle);
sortMergeRecursionHelper(nums, middle + 1, right);
mergeArr(nums, left, middle, right);
}
public static void mergeArr(int[] nums, int left, int middle, int right) {
int[] tem = new int[right - left + 1];
int i = left, j = middle + 1, k = 0;
while(i <= middle && j <= right) {
tem[k++] = nums[i] < nums[j]? nums[i++] : nums[j++];
}
while(i <= middle) {
tem[k++] = nums[i++];
}
while(j <= right) {
tem[k++] = nums[j++];
}
// 将辅助数组数据写入原数组
int index = 0;
while(left <= right) {
nums[left++] = tem[index++];
}
}
Fork-Join 原理
Fork/Join框架,就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可以再拆时),再将一个个小任务运算的结果进行Join汇总。除了分而治之之外,Fork-Join还有一种重要的思想,那就是工作密取。
工作密取
即当前线程的Task已经全被执行完毕,则自动取到其他线程的Task池中取出Task 继续执行。ForkJoinPool 中维护着多个线程(一般为CPU 核数)在不断地执行Task,每个线程除了执行自己职务内的Task之外,还会根据自己工作线程的闲置情况去获取其他繁忙的工作线程的Task,如此一来就能能够减少线程阻塞或是闲置的时间,提高CPU利用率。
Fork/Join 实战
Fork/Join 使用的标准范式
我们要使用ForkJoin 框架,必须首先创建一个ForkJoin 任务。它提供在任务中执行fork 和join 的操作机制,通常我们不直接继承ForkjoinTask 类,只需要直接继承其子类。
- RecursiveAction,用于没有返回结果的任务
- RecursiveTask,用于有返回值的任务
task 要通过ForkJoinPool 来执行,使用submit()
或invoke()
提交,两者的区别是:invoke()
是同步执行,调用之后需要等待任务完成,才能执行后面的代码;submit()
是异步执行。join()
和get()
方法当任务完成的时候返回计算结果。
在我们自己实现的compute()
方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果不足够小,就必须分割成两个子任务,每个子任务在调用invokeAll()
方法时,又会进入compute()
方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。
同步用法(有返回值)
在这里我们使用Fork/Join的同步用法统计整型数组中所有元素的和。
MakeArray是一个用于生成随机数组的工具类。
先来看单线程计算:
public class SumNormal {
public static void main(String[] args) {
int count = 0;
int[] src = MakeArray.makeArray();
long start = System.currentTimeMillis();
for(int i= 0;i<src.length;i++){
// SleepTools.ms(1);
count = count + src[i];
}
System.out.println("The count is "+count
+" spend time:"+(System.currentTimeMillis()-start)+"ms");
}
}
运行打印结果:The count is 23995323 spend time:0ms。
我们再来看使用Fork/Join来计算:
public class SumArray {
private static class SumTask extends RecursiveTask<Integer> {
//拆分的阈值
private final static int THRESHOLD = MakeArray.ARRAY_LENGTH / 10;
private int[] src;
private int fromIndex;
private int toIndex;
public SumTask(int[] src, int fromIndex, int toIndex) {
this.src = src;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@Override
protected Integer compute() {
//任务的大小是否合适
if (toIndex - fromIndex < THRESHOLD) {
System.out.println(" from index = " + fromIndex + " toIndex=" + toIndex);
int count = 0;
for (int i = fromIndex; i <= toIndex; i++) {
// SleepTools.ms(1);
count = count + src[i];
}
return count;
} else {
//fromIndex....mid.....toIndex
int mid = (fromIndex + toIndex) / 2;
SumTask left = new SumTask(src, fromIndex, mid);
SumTask right = new SumTask(src, mid + 1, toIndex);
invokeAll(left, right);
return left.join() + right.join();
}
}
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
int[] src = MakeArray.makeArray();
SumTask innerFind = new SumTask(src, 0, src.length - 1);
long start = System.currentTimeMillis();
pool.invoke(innerFind);
//System.out.println("Task is Running.....");
System.out.println("The count is " + innerFind.join()
+ " spend time:" + (System.currentTimeMillis() - start) + "ms");
}
}
运行打印结果:The count is 23704295 spend time:2ms。
从结果来看,我们使用Fork/Join,还不如使用单线程。为什么会这样,Fork/Join的Task是继承自RecursiveTask的,Recursive是递归的意思,既然是递归,那么就涉及到方法的出栈和压栈。另一方面由于是多线程任务,那么就会有线程的上下文切换,由于这是CPU计算密集型任务,自然而然就会比单线程的慢。
既然这样,我们在代码中加上一句SleepTools.ms(1);
,再来看结果如何呢。
单线程:The count is 23973773 spend time:6912ms
Fork/Join:The count is 24007429 spend time:920ms
这个时间比单线程快了将近7倍吧,对于单线程来说,每次相加休眠1ms,那么4000个数的累加就休眠将近4000ms。而对于Fork/Join来说,由于分而治之的思想,线程休眠1ms影响微小。当单线程任务耗时变长的情况下,Fork/Join的性能优势就体现出来了。
因此我们在使用多线程开发的时候,要考虑清楚线程任务的性质,再做选择。
异步用法(无返回值)
在这里我们使用Fork/Join的异步用法遍历指定目录,寻找指定类型文件
/**
* 类说明:遍历指定目录(含子目录)找寻指定类型文件
*/
public class FindDirsFiles extends RecursiveAction {
private File path;
public FindDirsFiles(File path) {
this.path = path;
}
@Override
protected void compute() {
List<FindDirsFiles> subTasks = new ArrayList<>();
File[] files = path.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 对每个子目录都新建一个子任务。
subTasks.add(new FindDirsFiles(file));
} else {
// 遇到文件,检查。
if (file.getAbsolutePath().endsWith("txt")) {
System.out.println("文件:" + file.getAbsolutePath());
}
}
}
if (!subTasks.isEmpty()) {
// 在当前的 ForkJoinPool 上调度所有的子任务。
for (FindDirsFiles subTask : invokeAll(subTasks)) {
subTask.join();
}
}
}
}
public static void main(String[] args) {
try {
// 用一个 ForkJoinPool 实例调度总任务
ForkJoinPool pool = new ForkJoinPool();
FindDirsFiles task = new FindDirsFiles(new File("F:/"));
//异步提交
pool.execute(task);
System.out.println("Task is Running......");
Thread.sleep(1);
//主线程做自己的工作
int otherWork = 0;
for (int i = 0; i < 100; i++) {
otherWork = otherWork + i;
}
System.out.println("Main Thread done sth......,otherWork=" + otherWork);
//阻塞方法
task.join();
System.out.println("Task end");
} catch (Exception e) {
e.printStackTrace();
}
}
}
CountDownLatch
CountDownLatch,也称之为闭锁,CountDownLatch 这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行(初始化)。
CountDownLatch 是通过一个计数器来实现的,计数器的初始值为初始任务的数量。每当完成了一个任务后,计数器的值就会减1(CountDownLatch.countDown()
方法)。当计数器值到达0 时,它表示所有的已经完成了任务,然后在闭锁上等待CountDownLatch.await()
方法的线程就可以恢复执行任务。
基本使用
构造方法
- CountDownLatch(int count) 构造一个以给定计数 CountDownLatch CountDownLatch。
方法:
- await() 当前线程等到锁存器计数到零,除非线程是 interrupted 。
- await(long timeout, TimeUnit unit) 使当前线程等待直到锁存器计数到零为止,除非线程为 interrupted或指定的等待时间过去。
- countDown() 减少锁存器的计数,如果计数达到零,释放所有等待的线程。
- getCount() 返回当前计数。
实例演示:
/**
*类说明:演示CountDownLatch用法,
* 共5个初始化子线程,6个闭锁扣除点,扣除完毕后,主线程和业务线程才能继续执行
*/
public class UseCountDownLatch {
static CountDownLatch latch = new CountDownLatch(6);
/**
* 初始化线程
*/
private static class InitThread implements Runnable{
@Override
public void run() {
System.out.println("Thread_"+Thread.currentThread().getId()
+" ready init work......");
latch.countDown();
for(int i =0;i<2;i++) {
System.out.println("Thread_"+Thread.currentThread().getId()
+" ........continue do its work");
}
}
}
/**
* 业务线程等待latch的计数器为0 完成
*/
private static class BusiThread implements Runnable{
@Override
public void run() {
try{
latch.await();
for(int i =0;i<3;i++) {
System.out.println("BusiThread_"+Thread.currentThread().getId()
+" do business-----");
}
}catch (Exception e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
SleepTools.ms(1);
System.out.println("Thread_"+Thread.currentThread().getId()
+" ready init work step 1st......");
latch.countDown();
System.out.println("begin step 2nd.......");
SleepTools.ms(1);
System.out.println("Thread_"+Thread.currentThread().getId()
+" ready init work step 2nd......");
latch.countDown();
}
}).start();
new Thread(new BusiThread()).start();
for(int i=0;i<=3;i++){
Thread thread = new Thread(new InitThread());
thread.start();
}
latch.await();
System.out.println("Main do ites work........");
}
}
从代码来看,等待的线程可以是多个,且可以在主线程和异步线程中使用latch.await();
,而且同一个线程里面,也可以多次调用latch.countDown();
。在使用的时候,我们也要注意,如果闭锁扣除点大于实际扣减数,那么等待线程就会一直等下去,所以一定要计算好闭锁初始化值和扣减次数。
CountDownLatch的不足
CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
CyclicBarrier
CyclicBarrier,也称为循环阻塞,允许一组线程全部等待彼此达到共同屏障点的同步辅助。循环阻塞在涉及固定大小的线程的程序中很有用,这些线程必须偶尔等待彼此。阻塞之所以被称为循环,因为它可以在等待的线程被释放之后重新使用。
基本使用
构造方法
- CyclicBarrier(int parties)
创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待它时,它将跳闸,并且当屏障跳闸时不执行预定义的动作。 - CyclicBarrier(int parties, Runnable barrierAction)
创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。
方法
- int await() 等待所有 parties已经在这个障碍上调用了 await 。
- int await(long timeout, TimeUnit unit) 等待所有 parties已经在此屏障上调用 await ,或指定的等待时间过去。
- int getNumberWaiting() 返回目前正在等待障碍的各方的数量。
- int getParties() 返回旅行这个障碍所需的parties数量。
- boolean isBroken() 查询这个障碍是否处于破碎状态。
- void reset() 将屏障重置为初始状态。
实例演示
/**
* 类说明:演示CyclicBarrier用法,共5个子线程,他们全部完成工作后,交出自己结果,
* 再被统一释放去做自己的事情,而交出的结果被另外的线程拿来拼接字符串
*/
public class UseCyclicBarrier {
static CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new CollectRunnable());
private static ConcurrentHashMap<String, Long> resultMap
= new ConcurrentHashMap<>();//存放子线程工作结果的容器
public static void main(String[] args) {
for (int i = 0; i <= 4; i++) {
Thread thread = new Thread(new SubRunnable());
thread.start();
}
}
private static class CollectRunnable implements Runnable {
@Override
public void run() {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, Long> workResult : resultMap.entrySet()) {
result.append("[" + workResult.getValue() + "]");
}
System.out.println(" the result = " + result);
System.out.println("do other business........");
}
}
private static class SubRunnable implements Runnable {
@Override
public void run() {
long id = Thread.currentThread().getId();
try {
Thread.sleep(1000 + id);
System.out.println("Thread_" + id + " ....do something ");
resultMap.put(Thread.currentThread().getId() + "", id);
cyclicBarrier.await();
Thread.sleep(1000 + id);
System.out.println("Thread_" + id + " ....do its business ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
我们定义了两个Runnable,SubRunnable
和CollectRunnable
,然后定义了CyclicBarrier对象,阻塞计数初始值为5,并设定屏障动作为CollectRunnable
。在SubRunnable
中调用cyclicBarrier.await();
使屏障点加1,在主线程中新启5个线程,分别传入SubRunnable
任务。
运行代码,可以观察到执行SubRunnable
的线程会在调用完await()
后,进入阻塞等待状态,直到所有线程都到达屏障点,即到达屏障点的线程数等于5了,达到初始化设定的值,最后一个到达屏障点的线程就会执行去执行CollectRunnable
中的逻辑,然后各个线程才会去执行各自任务接下来的代码。
Thread_9 ....do something
Thread_10 ....do something
Thread_11 ....do something
Thread_12 ....do something
Thread_13 ....do something
Thread_13 the result = [11][12][13][9][10]
Thread_13 do other business........
Thread_9 ....do its business
Thread_10 ....do its business
Thread_11 ....do its business
Thread_12 ....do its business
Thread_13 ....do its business
Process finished with exit code 0
CountDownLatch和CyclicBarrier的比较
- CountDownLatch是线程组之间的等待,即一个(或多个)线程等待N个线程完成某件事情之后再执行;而CyclicBarrier则是线程组内的等待,即每个线程相互等待,即N个线程都被拦截之后,然后依次执行。
- CountDownLatch是减计数方式,而CyclicBarrier是加计数方式。
- CountDownLatch计数为0无法重置,而CyclicBarrier计数达到初始值,则可以重置。
- CountDownLatch不可以复用,而CyclicBarrier可以复用。
Semaphore
基本使用
Semaphore,也称之为信号量,信号量就相当于一个计数器,通常用来限制线程的数量。它也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。
构造方法
- public Semaphore(int permits)
- public Semaphore(int permits, boolean fair) 可以提供了公平和非公平两种策略
主要方法
- void acquire() 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
- void release() 释放一个许可,将其返回给信号量。
- int availablePermits() 返回此信号量中当前可用的许可数。
- boolean hasQueuedThreads() 查询是否有线程正在等待获取。
实例演示
我们用一个连接池获取连接的例子来演示Semaphore的使用:
DBPoolSemaphore.java
/**
* 类说明:演示Semaphore用法,一个数据库连接池的实现
*/
public class DBPoolSemaphore {
private final static int POOL_SIZE = 10;
private final Semaphore sp;
//存放数据库连接的容器
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//初始化池
static {
for (int i = 0; i < POOL_SIZE; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
public DBPoolSemaphore() {
sp = new Semaphore(10);
}
/*归还连接*/
public void returnConnect(Connection connection) throws InterruptedException {
if (connection != null) {
System.out.println("当前有" + sp.getQueueLength() + "个线程等待数据库连接!!"
+ "可用连接数:" + sp.availablePermits());
synchronized (pool) {
pool.addLast(connection);
}
sp.release();
}
}
/*从池子拿连接*/
public Connection takeConnect() throws InterruptedException {
sp.acquire();
Connection connection;
synchronized (pool) {
connection = pool.removeFirst();
}
return connection;
}
}
SemaphoreTest.java
/**
* 类说明:测试数据库连接池
*/
public class AppTest {
private static DBPoolSemaphore dbPool = new DBPoolSemaphore();
private static class BusinessThread extends Thread {
@Override
public void run() {
Random r = new Random();//让每个线程持有连接的时间不一样
long start = System.currentTimeMillis();
try {
Connection connect = dbPool.takeConnect();
System.out.println("Thread_" + Thread.currentThread().getId()
+ "_获取数据库连接共耗时【" + (System.currentTimeMillis() - start) + "】ms.");
SleepTools.ms(100 + r.nextInt(100));//模拟业务操作,线程持有连接查询数据
System.out.println("查询数据完成,归还连接!");
dbPool.returnConnect(connect);
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
Thread thread = new BusinessThread();
thread.start();
}
}
在这个例子,在DBPoolSemaphore类中,我们初始化了一个有10个连接的连接池,并new了一个Semaphore实例,在主线程中,启动了50个线程去获取数据库连接,获取的过程需要通过Semaphore.acquire()
来获取,如果获取不到,则阻塞,直到分配到信号量许可为止。当使用完连接后,则释放连接,并通过Semaphore.release()
归还信号量许可。
Semaphore注意事项
在使用Semaphore的过程中,有一点非常需要注意,即使创建信号量的时候,指定了信号量的大小,但是不正当使用release()操作释放信号量会使得信号量超过配置的大小,也就有可能同时执行的线程数量比最开始设置的要大。因为没有任何线程获取信号量的时候,依然能够释放并且释放的有效。例如:
private void testRelease(){
Semaphore semaphore = new Semaphore(3);
Runnable runnable = () -> {
try {
System.out.println(Thread.currentThread().getName() + "try acquire");
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "acquired semaphore");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
semaphore.release();
// 4个线程都能够获取到信号量
for (int i = 0; i < 4; i++) {
new Thread(runnable).start();
}
}
因此,推荐的做法是,保证在使用时,一个线程先acquire(),然后release()。如果释放线程和获取线程不是同一个,那么最好保证这种对应关系。不要释放过多的许可证。
Exchange
Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。
简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据。其定义为 Exchanger 泛型类型,其中 V 表示可交换的数据类型,
基本使用
构造方法
- Exchanger():无参构造方法。
方法
- V exchange(V v):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
- V exchange(V v, long timeout, TimeUnit unit):等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间),然后将给定的对象传送给该线程,并接收该线程的对象。
实例演示:
/**
* 类说明:演示CyclicExchange用法
*/
public class UseExchange {
private static final Exchanger<String> exchange = new Exchanger<String>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
long id = Thread.currentThread().getId();
String data = "A";
try {
System.out.println("Thread [" + id + "] 交换前 : " + data);
data = exchange.exchange(data);
System.out.println("Thread [" + id + "] 交换后 : " + data);
} catch (InterruptedException e) {
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
long id = Thread.currentThread().getId();
String data = "B";
try {
System.out.println("Thread [" + id + "] 交换前 : " + data);
Thread.sleep(2000);
data = exchange.exchange(data);
System.out.println("Thread [" + id + "] 交换后 : " + data);
} catch (InterruptedException e) {
}
}
}).start();
}
}
运行结果如下:
Thread [9] 交换前 : A
Thread [10] 交换前 : B
Thread [10] 交换后 : A
Thread [9] 交换后 : B
可以看出,当一个线程到达 exchange 调用点时,如果其他线程此前已经调用了此方法,则其他线程会被调度唤醒并与之进行对象交换,然后各自返回;如果其他线程还没到达交换点,则当前线程会被挂起,直至其他线程到达才会完成交换并正常返回,或者当前线程被中断或超时返回。