并发编程(二):并发工具类和线程池

1.并发工具类

1.CountDownLatch:可以实现线程计数,阻塞后续线程

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行。

CountDownLatch的方法

  • countDown()实现计数器-1
  • await()等待拦截方法,等待计数器为0时再放行,否则则一直阻塞
  • getCount()获取当前计数器中计数数量

例如下面的案例

 public void countDownLatchTest() throws InterruptedException {
        System.out.println("======主线程开始运行=====");
        //第一个子线程
        new Thread(()->{
            System.out.println("第一个子线程"+Thread.currentThread().getName()+"开始运行~");
        }).start();

        //第二个子线程
        new Thread(()->{
            System.out.println("第二个子线程"+Thread.currentThread().getName()+"开始运行~");
        }).start();
        
        System.out.println("子线程执行完毕,主线程继续执行");

    }

  

按我们正常的编码思路来看,上面的这代码应该是图1这样的。

但实际上缺少图2这样的这就是因为是多个线程,它们的运行时机是不定,是谁先抢到资源谁先执行

如何将这段代码的执行效果和我们期望的图1一样呢?

我们可以使用 CountDownLatch这个工具类来实现

代码

使用new一个新的CountDownLatch因为有2个线程就传一个数量为2

然后在使用await()方法和countDown()方法来实现

private static CountDownLatch countDownLatch=new CountDownLatch(2);


public void countDownLatchTest() throws InterruptedException {
        System.out.println("======主线程开始运行=====");
        //第一个子线程
        new Thread(()->{
            System.out.println("第一个子线程"+Thread.currentThread().getName()+"开始运行~");
            //线程数量-1操作,通知该线程运行完毕
            countDownLatch.countDown();
        }).start();

        //第二个子线程
        new Thread(()->{
            System.out.println("第二个子线程"+Thread.currentThread().getName()+"开始运行~");
            //线程数量-1操作,通知该线程运行完毕
            countDownLatch.countDown();
        }).start();

        //等待,等待计数器中线程计数为0时才继续向下执行
        countDownLatch.await();
        System.out.println("子线程执行完毕,主线程继续执行");

效果

2.CyclicBarrier:类似于栅栏,进行拦截,等待所有线程都准备,然后统一放行,阻塞当前线程

CyclicBarrier是一个线程同步的工具类,它能够使一组线程互相等待。

每个线程在完成自己的目标任务后调用CyclicBarrier.await()方法进入等待状态,

当所有线程都调用了await()方法,则所有等待的线程释放,继续执行。所以叫同步屏障。

CyclicBarrier的方法

  • int await() 当前线程进入等待状态
  • int await(long timeout, TimeUnit unit)带超时时间的等待
  • int getNumberWaiting() 到达屏障处的线程数
  • int getParties() 屏障大小
  • boolean isBroken() 屏障损坏
  • void reset() 重置屏障

例如:十个选手进行比赛

代码

  public static void main(String[] args) {
        //创建10个线程
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                try {
                    Thread.sleep(100);
                    System.out.println("选手"+Thread.currentThread().getName()+"准备就绪");
                } catch (InterruptedException  e) {
                    e.printStackTrace();
                }
                System.out.println("选手"+Thread.currentThread().getName()+"开始比赛~");
            }).start();
        }
    }

  

结果

按理来说比赛应该在他们都准备好的情况下开始但是,因为有的人准备的慢,有的人准备的快,结果准备的快的就先开始比赛了,那么有什么办法可以让他们都准备完了在一同比赛呢?

我们可以使用CyclicBarrier工具类的await()让他们都进入准备状态,等他们都准备完毕之后在开始比赛

实现代码

  //设置等待线程数量,当线程数量到达指定数量时,统一向下运行
    private static CyclicBarrier cyclicBarrier=new CyclicBarrier(10);
    public static void main(String[] args) {
        //创建10个线程
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                try {
                    Thread.sleep(100);
                    System.out.println("选手"+Thread.currentThread().getName()+"准备就绪");
                    //等待
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println("选手"+Thread.currentThread().getName()+"开始比赛~");
            }).start();
        }
    }

  

结果:如图这样就实现了他们同时准备好了

3.Semaphore:可以做资源控制,容器中有几个资源,那么线程执行时先申请资源,资源如果可用则继续执行,如果资源不可用则阻塞等待,当资源占用完毕之后将该资源释放,其他线程排队占用

Semaphore是一种基于计数的信号量。

它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的事情后释放许可信号,超过阈值后,线程申请许可信号将会被阻塞。

Semaphore可以用来构建一些对象池,资源池之类的,比如数据库连接池,我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

  • acquire()  申请资源
  • release()  释放资源

例如,我们在网吧上网一样,必须有空位置了,我们才能去开心的玩耍

实现代码

 //假设这个网吧有三台电脑
    private static Semaphore semaphore=new Semaphore(3);
    
    public static void main(String[] args) {
        for (int i = 1; i <=8 ; i++) {
            new Thread(()->{
                try {
                    //申请资源,发生阻塞
                    System.out.println(Thread.currentThread().getName()+"来了");
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"发现有空位置了,做在位置上,打游戏");
                    //模拟打游戏的时间
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"打的头晕眼花了,下机回家吃饭");
                    //释放资源
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

  

结果,可以看到0,3,1先来了,他们在使用电脑的途中4,2,5,但是他们没有电脑可以使用就进行等待,这时候,0,3打完了,4,2就立马使用了电脑

4.Exchanger:可以执行线程的资源交换,线程数量必须为偶数,因为是两两相互交换资源,如果不是偶数默认情况下导致阻塞,可以设置交换资源超时时间

 Exchanger是一种线程间安全交换数据的机制。

当线程A调用Exchange对象的exchange()方法后,他会进入阻塞状态,直到线程B也调用了exchange()方法,然后以线程安全的方式交换数据,之后线程A和B继续运行。

实现代码

   private static String str1="资源1";
    private static String str2="资源2";
    //构建资源交换对象
    private static Exchanger<String> stringExchanger=new Exchanger<>();
    public static void main(String[] args) {
        //第一个线程
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"初始占用资源:"+str1);
            //资源交换,将资源交给其他线程和获取到其他线程交换过来的资源
            try {
                String newStr = stringExchanger.exchange(str1);
                System.out.println(Thread.currentThread().getName()+"交换资源:"+newStr);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        //第二个线程
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"初始占用资源:"+str2);
            //资源交换,将资源交给其他线程和获取到其他线程交换过来的资源
            try {
                String newStr = stringExchanger.exchange(str2);
                System.out.println(Thread.currentThread().getName()+"交换资源:"+newStr);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        //第三个线程
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"初始占用资源:"+str2);
            //资源交换,将资源交给其他线程和获取到其他线程交换过来的资源
            try {
                //设置资源交换超时时间
                String newStr = stringExchanger.exchange(str2,1000, TimeUnit.MILLISECONDS);
                System.out.println(Thread.currentThread().getName()+"交换资源:"+newStr);
            } catch (InterruptedException | TimeoutException e) {
                e.printStackTrace();
            }
        }).start();
    }

  

结果,0和1正常的进行了交换资源,而2和3交换的时候缺报错了。为什么?

因为设置了交换等待的最长时间为1秒


2.线程池

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。

线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。

然而,增加可用线程数量是可能的。

线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中并等待下一次分配任务。

总而言之就是类似于一个池子,可以存放/管理线程

1.使用线程池的好处

  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 第三:提高线程的可管理性


2.如何使用线程池
1线程池分类
线程池顶级类ThreadPoolExecutor最终实现Executor接口,在JUC包下,通过Executors类可以创建不同类型的线程池,分类如下

  • 1.newScheduledThreadPool 定时任务线程池,可以设置任务时间
  • 2.newFixedThreadPool 定长线程池
  • 3.newSingleThreadExecutor 利用的是单线程,单线程处理任务,一般不用
  • 4.newCachedThreadPool 带缓存的线程池

2.2 构建线程池
1.newCachedThreadPool线程池,可缓存,可以重复利用

   public static void main(String[] args) {
        //构建一个线程池,可以重复利用线程
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 1; i <=10 ; i++) {
            //创建线程池
            executorService.execute(()->{
                System.out.println("创建线程池"+Thread.currentThread().getName());
            });
        }
    }

  

结果

2.newFixedThreadPool创建一个固定线程数量的线程池

  public static void main(String[] args) {
        //构建线程池对象
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            //创建线程
            executorService.execute(()->{
                System.out.println("创建线程:"+Thread.currentThread().getName());
            });
        }
    }

  

结果

3.newScheduledThreadPool

  public static void main(String[] args) {
        //创建一个线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 10; i++) {
            scheduledExecutorService.schedule(()->{
                System.out.println("创建线程:"+Thread.currentThread().getName());
            },1000, TimeUnit.MILLISECONDS);

        }
    }

  

结果

4.newSingleThreadExecutor单线程池

public static void main(String[] args) {
        //创建一个线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            executorService.execute(()->{
                System.out.println("创建线程:"+Thread.currentThread().getName());
            });
        }
    }

  

结果

3.所有的线程池分类底层调用的都是ThreadPoolExecutor()构造方法,该构造方法中每一个参数含义

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue)

  • 1.corePoolSize代表核心线程池大小,当有任务时,会创建对应线程处理对应任务,当线程到达一定数量后,则会缓存到队列当中,不会再次创建新的线程
  • 2.maximumPoolSize: 线程池最大线程数,它表示在线程池中最多能创建多少个线程
  • 3.keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止。
  • 4.unit: 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性

4.如何确定线程池中创建线程数量
考虑CPU密集和IO密集,如果不考虑IO情况下,一般是处理器数量+1,如果考虑IO,则处理器*2

猜你喜欢

转载自www.cnblogs.com/wishsaber/p/12526649.html