CountDownLatch
概念
- countDownLatch这个类使用一个线程等待其他线程各自执行完毕后再执行
- 是通过一个计数器实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程*就可以恢复工作了。
源码
1.countDownLatch类中只提供了一个构造器
//参数count为计数值
public CountDownLatch(int count) { };
-
类中有三个方法是非常重要的:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 public void await(){ sync.acquireShared(1);} throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //将count值减1 public void countDown() {sync.releaseShared(1); }; //通过继承AQS重新获取共享锁和释放共享锁来实现 class Sync extends AbstractQueuedSynchronizer{ public Sync(int count){ setState(count); } @Override protected int tryAcquireShared(int arg) { //只有当state变为0时,加锁成功 return getState()==0 ? 1: -1; } /* countdown的方法 */ @Override protected boolean tryReleaseShared(int arg) { for (;;){ int c = getState(); if (c == 0) return false; int nextc = c -1; //用CAS操作,讲count减一 if (compareAndSetState(c , nextc)){ //当state=0时,释放锁成功,返回true return nextc ==0; } } } }
原理
本质由一把共享锁实现有,线程调用await方法抢共享锁,因为state不为0所以抢不到共享锁,只有各自执行完countdown方法,state值变为0才能抢到共享锁,如下图所示:
使用示例
/****
* 10、9、8、7、6、5、4、3、2、1
* 相当于一个计数器 每个线程减一完成前,主线程阻塞在await处;相当于main线程希望11个线程执行完操作后,才执行某个操作
* @throws InterruptedException
*/
public static void test01()throws InterruptedException{
CountDownLatch ct1=new CountDownLatch(11);
for(int i=10;i>=0;i--){
int num=i;
new Thread(){
@Override
public void run() {
System.out.println(">>>>>>>"+num);
ct1.countDown();
}
}.start();
Thread.sleep(1000L);//sleep可以保证10 到0的顺序
}
ct1.await();//主线程阻塞等到计数器为0 才往下执行
System.out.println("点火.......");
}
Semaphore
概念
Semaphore是一个计数信号量,常用于限制可以访问某些资源(物理或逻辑的)线程数目,就是限流作用。简单来说,是一种用来控制并发量的共享锁。
如下图(左半部分)所示,访问数据库同时最多只有五个线程进行访问,这样可以减少对数据库的压力。
原理
实质是一个带有限制数量的共享锁,线程获取锁成功则state加一,state等于设置的最大数量再来获取进入阻塞,其他线程执行完释放信号量后其他线程才可以抢到锁。
使用示例
static JamesSemaphore sp=new JamesSemaphore(6);
public static void main(String[] args) {
for(int i=0;i<1000;i++){
new Thread(){
@Override
public void run() {
try {
sp.acquire();//抢信号量、就是在获取共享锁
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
queryDB("localhost:3306");
sp.release();//执行完毕释放锁、释放信号量
}
}.start();
}
}
public static void queryDB(String url){
System.out.println("query"+url);
}
CyclicBarrier
循环栅栏,可以循环利用屏障。举例:排队上摩天轮时,每到齐四个人,就可以上同一车厢,不够四个人就等到够四个人。
示例
CyclicBarrier barrier=new CyclicBarrier(4);
//传入一个Runnable,打印栅栏
for(int i=0;i<100;i++){
new Thread(){
@Override
public void run() {
try {
barrier.await();//不够一批次就阻塞在这里,四个线程为一个批次
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("上到摩天轮....");
}
}.start();
LockSupport.parkNanos(1000*1000*1000L);
}
FutrueTask
**
* 一个带有返回值的线程类
*/
public class FutureTaskTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTask cTask=new CallableTask();
//todo FutrueTask继承Runnable,所以CallableTask可以通过FutrueTask包装 当做Thread的参数
FutureTask futureTask=new FutureTask(cTask);
//执行任务
new Thread(futureTask).start();
System.out.println("begin to get result");
System.out.println(futureTask.get());
System.out.println("got result...........");
//todo futureTask只能执行一次,上面执行了一次下面再次调用就不执行了,,因为futureTask内部标记了该任务已经执行过
// new Thread(futureTask).start();
}
}
class CallableTask implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("》》》执行任务...");
//模拟耗时
LockSupport.parkNanos(1000*1000*1000*5L);
return "success";
}
}
ForkJoinPool
ForkJoinPool是ExecutorService接口的实现,它专门可以递归分解成小块的工作而设计。fork/join框架将任务分配给线程池中的工作线程,充分利用处理器的优势,提高程序性能。
使用fork/join框架的第一步是编写执行一部分工作的代码。类似的伪代码如下:
如果(当前工作部分足够小)
直接做这项工作
其他
把当前工作分成两部分
调用这两个部分并等待结果
将此代码包装在ForkJoinTask子类中,通常是RecursiveTask(可以返回结果)或RecursiveAction。
关键点:分解任务fork出新任务,汇集join任务执行结果。
代码示例
static ArrayList<String> urls = new ArrayList<String>(){
{
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
add("http://www.baidu.com");
add("http://www.sina.com");
}
};
static ForkJoinPool firkJoinPool = new ForkJoinPool(3,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null,
true);
public static void main(String args[]) throws ExecutionException, InterruptedException {
Job job = new Job(urls, 0, urls.size());
ForkJoinTask<String> forkJoinTask = firkJoinPool.submit(job);
String result = forkJoinTask.get();
System.out.println(result);
}
public static String doRequest(String url){
//模拟网络请求
return "Kody ... test ... " + url + "\n";
}
static class Job extends RecursiveTask<String>{
List<String> urls;
int start;
int end;
public Job(List<String> urls,int start, int end){
this.urls = urls;
this.start = start;
this.end = end;
}
@Override
protected String compute() {
//计算任务的大小
int count = end - start;
if (count <=10){
//直接执行
String result = "";
for (int i=start; i<end; i++){
String response = doRequest(urls.get(i));
result +=response;
}
return result;
}else{
//继续拆分任务
int x = (start + end) / 2;
Job job1 = new Job(urls, start, x);
job1.fork();
Job job2 = new Job(urls, x, end);
job2.fork();
//固定写法
String result = "";
result += job1.join();
result += job2.join();
return result;
}
}
}
使用场景
二分查找,阶乘计算,归并排序,堆排序、快速排序、傅里叶变换都用了分治法的思想。
ForkJoin并行处理框架
在JDK1.7中推出的ForkJoinPool线程池,主要用于ForkJoinTask任务的执行,ForkJoinTask是一个类似线程的实体,但是比普通线程更轻量。
我们来使用ForkJoin框架完成以下1-10亿求和的代码。
public class ForkJoinMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> rootTask = forkJoinPool.submit(new SumForkJoinTask(1L, 10_0000_0000L));
System.out.println("计算结果:" + rootTask.get());
}
}
class SumForkJoinTask extends RecursiveTask<Long> {
private final Long min;
private final Long max;
private Long threshold = 1000L;
public SumForkJoinTask(Long min, Long max) {
this.min = min;
this.max = max;
}
@Override
protected Long compute() {
// 小于阈值时直接计算
if ((max - min) <= threshold) {
long sum = 0;
for (long i = min; i < max; i++) {
sum = sum + i;
}
return sum;
}
// 拆分成小任务
long middle = (max + min) >>> 1;
SumForkJoinTask leftTask = new SumForkJoinTask(min, middle);
leftTask.fork();
SumForkJoinTask rightTask = new SumForkJoinTask(middle, max);
rightTask.fork();
// 汇总结果
return leftTask.join() + rightTask.join();
}
}
上述代码逻辑可通过下图更加直观地理解。
ForkJoin框架实现
在ForkJoin框架中重要的一些接口和类如下图所示。
ForkJoinPool
ForkJoinPool是用于运行ForkJoinTasks的线程池,实现了Executor接口。可以通过new ForkJoinPool()直接创建ForkJoinPool对象。
public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode){
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
通过查看构造方法源码我们可以发现,在创建ForkJoinPool时,有以下4个参数:
- parallelism:期望并发数。默认会使用Runtime.getRuntime().availableProcessors()的值
- factory:创建ForkJoin工作线程的工厂,默认为defaultForkJoinWorkerThreadFactory
- handler:执行任务时遇到不可恢复的错误时的处理程序,默认为null
- asyncMode:工作线程获取任务使用FIFO模式还是LIFO模式,默认为LIFO
ForkJoinTask
ForkJoinTask是一个对于在ForkJoinPool中运行任务的抽象类定义。
可以通过少量的线程处理大量任务和子任务,ForkJoinTask实现了Future接口。主要通过fork()方法安排异步任务执行,通过join()方法等待任务执行的结果。
想要使用ForkJoinTask通过少量的线程处理大量任务,需要接受一些限制。
- 拆分的任务中避免同步方法或同步代码块;
- 在细分的任务中避免执行阻塞I/O操作,理想情况下基于完全独立于其他正在运行的任务访问的变量;
- 不允许在细分任务中抛出受检异常。
因为ForkJoinTask是抽象类不能被实例化,所以在使用时JDK为我们提供了三种特定类型的ForkJoinTask父类供我们自定义时继承使用。
- RecursiveAction:子任务不返回结果
- RecursiveTask:子任务返回结果
- CountedCompleter:在任务完成执行后会触发执行
ForkJoinWorkerThread
ForkJoinPool中用于执行ForkJoinTask的线程。
ForkJoinPool既然实现了Executor接口,那么它和我们常用的ThreadPoolExecutor之前又有什么差异呢?
如果们使用ThreadPoolExecutor来完成分治法的逻辑,那么每个子任务都需要创建一个线程,当子任务的数量很大的情况下,可能会达到上万个,那么使用ThreadPoolExecutor创建出上万个线程,这显然是不可行、不合理的;
而ForkJoinPool在处理任务时,并不会按照任务开启线程,只会按照指定的期望并行数量创建线程。在每个线程工作时,如果需要继续拆分子任务,则会将当前任务放入ForkJoinWorkerThread的任务队列中,递归处理直到最外层的任务。
工作窃取算法
ForkJoinPool的各个工作线程都会维护一个各自的任务队列,减少线程之间对于任务的竞争;
每个线程都会先保证将自己队列中的任务执行完,当自己的任务执行完之后,会去看其他线程的任务队列中是否有未处理完的任务,如果有则会帮助其他线程执行;
为了减少在帮助其他线程执行任务时发生竞争,会使用双端队列来存放任务,被窃取的任务只会从队列的头部获取任务,而正常处理的线程每次都是从队列的尾部获取任务。
优点
充分利用了线程资源,避免资源的浪费,并且减少了线程间的竞争。
缺点
需要给每个线程开辟一个队列空间;在工作队列中只有一个任务时同样会存在线程竞争
并行数量创建线程。在每个线程工作时,如果需要继续拆分子任务,则会将当前任务放入ForkJoinWorkerThread的任务队列中,递归处理直到最外层的任务。
工作窃取算法
ForkJoinPool的各个工作线程都会维护一个各自的任务队列,减少线程之间对于任务的竞争;
每个线程都会先保证将自己队列中的任务执行完,当自己的任务执行完之后,会去看其他线程的任务队列中是否有未处理完的任务,如果有则会帮助其他线程执行;
为了减少在帮助其他线程执行任务时发生竞争,会使用双端队列来存放任务,被窃取的任务只会从队列的头部获取任务,而正常处理的线程每次都是从队列的尾部获取任务。
[外链图片转存中…(img-PJZd018s-1645582263961)]
优点
充分利用了线程资源,避免资源的浪费,并且减少了线程间的竞争。
缺点
需要给每个线程开辟一个队列空间;在工作队列中只有一个任务时同样会存在线程竞争