线程并发的工具类

一.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)
线程开始计算
线程中断

发布了30 篇原创文章 · 获赞 4 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_42549122/article/details/94025444
今日推荐