java并发线程工具类

CountDownLatch

概念

  1. countDownLatch这个类使用一个线程等待其他线程各自执行完毕后再执行
  2. 是通过一个计数器实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程*就可以恢复工作了。

源码

1.countDownLatch类中只提供了一个构造器

//参数count为计数值
public CountDownLatch(int count) {  };  
  1. 类中有三个方法是非常重要的:

    //调用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();
    }
}

上述代码逻辑可通过下图更加直观地理解。

img

ForkJoin框架实现

在ForkJoin框架中重要的一些接口和类如下图所示。

img

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的各个工作线程都会维护一个各自的任务队列,减少线程之间对于任务的竞争;

每个线程都会先保证将自己队列中的任务执行完,当自己的任务执行完之后,会去看其他线程的任务队列中是否有未处理完的任务,如果有则会帮助其他线程执行;

为了减少在帮助其他线程执行任务时发生竞争,会使用双端队列来存放任务,被窃取的任务只会从队列的头部获取任务,而正常处理的线程每次都是从队列的尾部获取任务。

img

优点

充分利用了线程资源,避免资源的浪费,并且减少了线程间的竞争。

缺点

需要给每个线程开辟一个队列空间;在工作队列中只有一个任务时同样会存在线程竞争

并行数量创建线程。在每个线程工作时,如果需要继续拆分子任务,则会将当前任务放入ForkJoinWorkerThread的任务队列中,递归处理直到最外层的任务。

工作窃取算法

ForkJoinPool的各个工作线程都会维护一个各自的任务队列,减少线程之间对于任务的竞争;

每个线程都会先保证将自己队列中的任务执行完,当自己的任务执行完之后,会去看其他线程的任务队列中是否有未处理完的任务,如果有则会帮助其他线程执行;

为了减少在帮助其他线程执行任务时发生竞争,会使用双端队列来存放任务,被窃取的任务只会从队列的头部获取任务,而正常处理的线程每次都是从队列的尾部获取任务。

[外链图片转存中…(img-PJZd018s-1645582263961)]

优点

充分利用了线程资源,避免资源的浪费,并且减少了线程间的竞争。

缺点

需要给每个线程开辟一个队列空间;在工作队列中只有一个任务时同样会存在线程竞争

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值