一.fork-join
fork-join体现了算法一种算法上的思想,分而治之。就是把一个大的问题分成(fork)若干个可解决的小问题去解决。这些小问题互相独立,且与原问题形式相同。再将一个个小问题进行计算的结果进行汇总(join)。
工作密取
在这要介绍一下双端队列和工作密取,正如阻塞队列使用与生产者-消费者模式,双端队列同样适用于另一种相关模式,即工作密取。在生产者-消费者设计中,所有消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其它消费者双端队列末尾秘密地获取工作。密取工作模式比传统的生产者-消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,它们都只是访问自己的双端队列,从而极大地减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争程度。当一个工作线程找到新的任务单元时,它会将其放到自己队列的末尾(或者在工作共享模式中,放入其它工作者线程的队列中)。当双端队列为空时,它会在另一个线程的队列末尾查找新的任务,从而确保每个线程都保持忙碌状态。
在fork-join框架中,每个程序都会重写实现compute()方法,这个方法根据实际情况有两种走向,一个是不符合条件(阀值)继续拆分成更小的任务,另一个是符合条件,join()插队到执行的最前面,进行执行,插队的作用在于,先执行最后的小任务(最后执行的任务必然最小),然后集合成一个大任务,以便达到目的。
通过一个例子,表现fork-join线程得效率
首先创建一个工具类,随机生成4000个整数型数据的数组.
public class Util {
public static final int Arraylength=40;
public static int[] arrys=new int[Arraylength];
public static int[] getArrys(){
Random random=new Random();
for (int i=0;i<Arraylength;i++){
arrys[i]=random.nextInt(Arraylength*3);
}
return arrys;
}
}
先用用单线程计算和
public class TestArray {
public static void main(String[] args) throws InterruptedException {
int[] arrays=Util.getArrys();
int count=0;
long start=System.currentTimeMillis();
for (int i=0;i<arrays.length;i++){
//让每次取出数据都休眠1秒
Thread.sleep(1);
count+=arrays[i];
}
long end=System.currentTimeMillis();
System.out.println("总和为:"+count+"所用时间:"+(end-start)+"ms");
}
}
再用fork-join进行计算
public class SumArrays {
private static class SumTask extends RecursiveTask<Integer> {
//设置阀值
private final static int length = Util.Arraylength / 10;
//要统计的数组开始的索引
private int start;
//要统计的数组结束的索引
private int end;
//要统计的数组
private int[] arrys;
public SumTask(int[] arrys, Integer start, Integer end) {
this.arrys = arrys;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int count = 0;
//如果没有超过阀值
if (end - start < length) {
for (int i = start; i <= end; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count =count+arrys[i];
}
return count;
}
//如果超过了需要继续进行拆分
else {
//拆分为2个任务
int middle = (start - end) / 2;
SumTask left = new SumTask(arrys, start, middle);
SumTask right = new SumTask(arrys, middle + 1, end);
invokeAll(left,right);
//最后返回两个小任务线程计算的结果
return left.join() + right.join();
}
}
}
public static void main(String[] args) {
//获取数组
int[]arrys=Util.getArrys();
//建立fork-join线程池
ForkJoinPool forkJoinPool=new ForkJoinPool();
long start=System.currentTimeMillis();
SumTask sumTask=new SumTask(arrys,0,arrys.length-1);
forkJoinPool.invoke(sumTask);
System.out.println("所用时:"+(System.currentTimeMillis()-start));
}
}
本质上fork-join就是多线程处理数据,但是多线程大部分是比单线程快的,多线程主要的费时在于线程轮转和上下文切换,所以当线程数多了后,并非一定比单线程快。
二.CountDownLatch
作用:是一个线程等待其它线程执行完成后再进行执行。
await():用来等待。
countdown()负责计算器-1。
模拟流程,当程序运行时,线程D会被使用await()方法被暂停,当线程A-C执行完一个后会调用countDown()方法-1,线程A-C可以是同时进行的,谁先执行完谁调用countDown就会-1。当值为0时,被await()的线程D会进行执行。本质商countDownLatch()是一个加强版的join()。切记,线程数和扣除点不一定是一致的,一个线程的任务可以有多个扣除点。线程进行countDown()扣减后依旧可以进行处理余下的业务内容。等待的线程也可以不止一个,只要调用了await()就行。
例如:
public class TestCountDown {
//初始化一个countLatchDown类
private static CountDownLatch latch=new CountDownLatch(6);
//自定义线程1
public static class myThread extends Thread{
public void run(){
latch.countDown();
System.out.println("Thread:"+Thread.currentThread().getName()
+"is start");
}
}
//自定义线程2
public static class MyRunable implements Runnable{
@Override
public void run() {
latch.countDown();
System.out.println("Thread:"+Thread.currentThread().getName()
+"is runable");
latch.countDown();
System.out.println("Thread:"+Thread.currentThread().getName()
+"is runable again");
}
}
public static void main(String[] args) throws InterruptedException {
//启动四个Thread线程,里面包含4个countdown
for (int i=0;i<=3;i++){
new myThread().start();
}
System.out.println("MYRunable in run ok!");
//启动Runable实现类,里面包含两次CountDown
new Thread(new MyRunable()).start();
//让主线程在countDownLatch门槛外等待,只有在latch.await()后面的业务代码才会被拦截
latch.await();
System.out.println("Main Thread is run stop");
}
输出结果:
Thread:Thread-0is start
Thread:Thread-3is start
Thread:Thread-2is start
Thread:Thread-1is start
Thread:Thread-4is runable
Thread:Thread-4is runable again
Main Thread is run stop
只有在await()方法后面的业务代码才会被拦截到门槛外。
三.CyclicBarrier
让一组线程进入一个屏障内,当这一组最后一个线程进入屏障时候,屏障开放,所有被阻塞的线程会被开放。
每个线程调用await()方法进行等待,当达到屏障释放临界值的时刻,屏障被打开。
例:
public class TestCyclicBarrier {
//创建一个CyclicBarrier工具类
//public static CyclicBarrier cyclicBarrier=new CyclicBarrier(5);
//CyclicBarrier工具类可以添加一个当屏障打开除这组线程外需要执行的线程
public static CyclicBarrier cyclicBarrier=new CyclicBarrier(5,new Thread(new MyRunable()));
//创建一个线程容器
private static ConcurrentHashMap<String, Long> ResultMap=new ConcurrentHashMap<String, Long>();
//查看集合里面的方法
static class MyRunable implements Runnable {
public void run() {
StringBuilder stringBuilder=new StringBuilder();
for (Map.Entry<String, Long> element : ResultMap.entrySet()) {
stringBuilder.append("["+element.getKey()+":"+element.getValue()+"]");
}
System.out.println(stringBuilder);
}
}
//工作线程
static class MyThread extends Thread{
public void run(){
try {
System.out.println(Thread.currentThread().getName()+"is await()");
ResultMap.put("id"+Thread.currentThread().getId(),Thread.currentThread().getId());
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("MyThread: "+Thread.currentThread().getName()+"is run");
}
}
public static void main(String[] args) {
for (int i=0;i<=4;i++){
new MyThread().start();
}
System.out.println();
}
}
输出结果:
Thread-2is await()
Thread-3is await()
Thread-4is await()
Thread-1is await()
Thread-5is await()
[id17:17][id16:16][id13:13][id15:15][id14:14]
MyThread: Thread-5is run
MyThread: Thread-3is run
MyThread: Thread-4is run
MyThread: Thread-1is run
MyThread: Thread-2is run
CyclicBarrier和CountDownLatch的区别
(1)CyclicBarrier是由一组线程自行决定,而CountDownLatch是由第三方工具决定
(2)放行条件不同 CyclicBarrier是 线程数=放行要求数 CountdownLatch 线程数<=放行条件数
(3)CyclicBarrier遇到线程的await()方法认为排查到线程,而CountdownLatch可以在业务代码任何地方自行设计减1的操作
切记无论哪种方法,并发的线程都是由操作系统决定,你无法控制,你控制的只有门槛的放行条件。而无法控制放行后哪个线程先走,哪个线程后走。
4.Semaphore
控制访问某个特定资源的线程数量,常用于流量控制,也叫信号量。
那么什么是信号量呢?在Semapore存在两种重要的方法,acquire,release。我用一种比较通俗的方式来跟大家解释一下,就是在该类初始化的时候,给定一个数字A,每个线程调用acquire()方法后,首先判断A是否大于0,如果大于0,就将A减去1,然后执行对应的线程,如果不大于0,那么就会阻塞,直到其他线程调用了release()方法,将A加上1,该线程可能有执行的机会。其实本质上Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。
acquire( int permits ) 中的参数是什么意思呢?可以这么理解, new Semaphore(6) 表示初始化了 6个通路, semaphore.acquire(2) 表示每次线程进入将会占用2个通路,semaphore.release(2) 运行时表示归还2个通路。没有通路,则线程就无法进入代码块。
其他一些常有工具方法
availablePermits() 方法在前面用过,表示返回 Semaphore 对象中的当前可用许可数,此方法通常用于调试,因为许可数量(通路)可能是实时在改变的。
getQueueLength() 获取等待许可的线程个数。
hasQueuedThreads() 判断有没有线程在等待这个许可。
getQueueLength() 和 hasQueuedThreads() 都是在判断当前有没有等待许可的线程信息时使用。
在现实种常用于数据库连接池并发控制连接的数量。例如:
public class DbPool {
//初始化信号工作类,一个为可用,一个为用过,两个配合加起来为需要初始化和就可
private static Semaphore useful,useless;
//数据库初始化的数量
private final static int POOL_SIZE=10;
//存放数据连接的集合
private static LinkedList<String> pool=new LinkedList<String>();
public String connection;
public DbPool(){
useful=new Semaphore(POOL_SIZE);
useless=new Semaphore(0);
}
//初始化连接数
static {
for (int i = 0; i < POOL_SIZE; i++) {
//模拟取得连接数
pool.addLast("connection"+i);
}
}
//获得数据库连接的线程方法
public String getConnection() throws InterruptedException {
//获取通道,可调用通道acquire减1
useful.acquire();
synchronized (pool) {
connection = pool.removeFirst();
}
System.out.println(Thread.currentThread().getName()+"获取连接成功!");
//释放通道,用过的通道release加1
useless.release();
return connection;
}
//释放数据库连接
public void returnConnection(String Connection) throws InterruptedException{
if (Connection!=null) {
//获取通道,用过的减1
useless.acquire();
pool.addLast(connection);
System.out.println(Thread.currentThread().getName()+"释放连接成功!");
useful.release();
}
}
}
创建50个线程进行连接测试
public class Test {
//切记尽可能保持一个线程实例化一个对象,否则可能会产生死锁,因为部分数据是不共享的
public static DbPool dbPool=new DbPool();
static class MyThread extends Thread{
public void run(){
try {
String conn=dbPool.getConnection();
Thread.sleep(50);
dbPool.returnConnection(conn);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
for (int i=0;i<50;i++){
new MyThread().start();
}
}
}
Thread-1获取连接成功!
Thread-2获取连接成功!
Thread-3获取连接成功!
Thread-0获取连接成功!
Thread-4获取连接成功!
Thread-5获取连接成功!
Thread-6获取连接成功!
Thread-7获取连接成功!
Thread-9获取连接成功!
Thread-8获取连接成功!
Thread-2释放连接成功!
Thread-3释放连接成功!
Thread-1释放连接成功!
Thread-10获取连接成功!
Thread-6释放连接成功!
Thread-4释放连接成功!
Thread-7释放连接成功!
Thread-14获取连接成功!
Thread-0释放连接成功!
Thread-5释放连接成功!
Thread-11获取连接成功!
Thread-13获取连接成功!
Thread-8释放连接成功!
Thread-9释放连接成功!
Thread-18获取连接成功!
Thread-21获取连接成功!
Thread-10释放连接成功!
Thread-20获取连接成功!
Thread-13释放连接成功!
Thread-11释放连接成功!
Thread-14释放连接成功!
Thread-22获取连接成功!
Thread-19获取连接成功!
Thread-21释放连接成功!
Thread-18释放连接成功!
Thread-24获取连接成功!
Thread-25获取连接成功!
Thread-20释放连接成功!
Thread-26获取连接成功!
Thread-19释放连接成功!
Thread-22释放连接成功!
Thread-29获取连接成功!
Thread-24释放连接成功!
Thread-25释放连接成功!
Thread-27获取连接成功!
Thread-26释放连接成功!
Thread-33获取连接成功!
Thread-29释放连接成功!
Thread-30获取连接成功!
Thread-27释放连接成功!
Thread-31获取连接成功!
Thread-33释放连接成功!
Thread-35获取连接成功!
Thread-30释放连接成功!
Thread-34获取连接成功!
Thread-31释放连接成功!
Thread-36获取连接成功!
Thread-35释放连接成功!
Thread-37获取连接成功!
Thread-34释放连接成功!
Thread-38获取连接成功!
Thread-36释放连接成功!
Thread-40获取连接成功!
Thread-37释放连接成功!
Thread-39获取连接成功!
Thread-38释放连接成功!
Thread-41获取连接成功!
Thread-40释放连接成功!
Thread-42获取连接成功!
Thread-39释放连接成功!
Thread-44获取连接成功!
Thread-41释放连接成功!
Thread-43获取连接成功!
Thread-42释放连接成功!
Thread-45获取连接成功!
Thread-44释放连接成功!
Thread-46获取连接成功!
Thread-43释放连接成功!
Thread-47获取连接成功!
Thread-45释放连接成功!
Thread-48获取连接成功!
Thread-46释放连接成功!
Thread-49获取连接成功!
Thread-47释放连接成功!
Thread-48释放连接成功!
Thread-49释放连接成功!
初始化10个被用完后,其它需求连接的线程会被停止,等有通道被释放后,才可以继续进行连接的获取。
四.Exchange
它的主要功能是线程间数据的交换,但是比较局限,只能是两个线程间的交换。
public class Exchange {
private static final Exchanger<Set<String>> exchange
= new Exchanger<>();
public static class Test implements Runnable{
@Override
public void run() {
Set<String> setA = new HashSet<>();//存放数据的容器
try {
setA.add("one");
for(String value:setA){
System.out.println("Thread_"+Thread.currentThread().getId()+"交换前值为"+value);
}
setA = exchange.exchange(setA);//交换set
/*处理交换后的数据*/
for(String value:setA){
System.out.println("Thread_"+Thread.currentThread().getId()+"交换后值为"+value);
}
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
final Thread one =new Thread(new Test());
//第一个线程
one.start();
//第二个线程
new Thread(new Runnable() {
@Override
public void run() {
Set<String> setB = new HashSet<>();//存放数据的容器
try {
setB.add("two");
for(String value:setB){
System.out.println("Thread_"+Thread.currentThread().getId()+"交换前值为"+value+"\n---------------");
}
setB = exchange.exchange(setB);//交换set
/*处理交换后的数据*/
one.join();
for(String value:setB){
System.out.println("Thread_"+Thread.currentThread().getId()+"交换后值为"+value);
}
} catch (InterruptedException e) {
}
}
}).start();
}
}
输出内容
Thread_12交换前值为one
Thread_13交换前值为two
Thread_12交换后值为two
Thread_13交换后值为one
五.Callable、Future、FutureTask
isdone():无论线程是异常停止还是正常停止,只要是停止了就会返回true,反之false。
isCancelled():任务完成前被取消,返回true。
cancel():有三种情况,任务还没开始,无论参数是true还是false都会返回false,如果说任务已经启动,那么cancel(true)会尝试去中断正在进行的任务。cancel(false)不会去中断已经运行的任务。还有一种是任务已经结束的情况,如果调用cancel方法,会直接返回false。
例如:
public class TestCallable {
public static class myCallable implements Callable{
Integer sum=0;
@Override
public Integer call() throws Exception {
System.out.println("线程开始计算");
Thread.sleep(200);
for (int i=0;i<10;i++){
sum+=i;
}
return sum;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
myCallable callable=new myCallable();
//Callable运行需要用FutureTask进行包装
FutureTask<Integer> futureTask=new FutureTask<Integer>(callable);
//将封装后的结果再次执行的流程,相当于执行runable
new Thread(futureTask).start();
Random random=new Random();
//如果随机为true,则正常执行返回结果,反之则进行中断操作
if (random.nextBoolean()){
System.out.println("结果计算为 : "+futureTask.get());
}else {
//调用线程中断的方法
futureTask.cancel(true);
System.out.println("线程中断");
}
}
}
输出结果
(1)
线程开始计算
结果计算为 : 45
(2)
线程开始计算
线程中断