JUC包下面的四大天王+线程池部分知识

2023-11-11 17:52

本文主要是介绍JUC包下面的四大天王+线程池部分知识,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一)Semphore:限流器用我就对了

Java中信号量Semphore是把操作系统原生的信号量封装了一下,本质就是一个计数器,描述了 可用资源的个数,主要涉及到两个操作

如果计数器为0了,继续Р操作,就会出现阻塞等待的情况

P操作:申请一个可用资源,计数器-1
V操作:释放一个可用资源,计数器+1

停车场门口有一个灯牌,会显示停车位还剩余多少个,每进去一辆车,显示的停车位数量就-1,就相当于进行了一次P操作,每出去一辆车, 显示的停车位数量就+1,就相当于进行了一次V操作,而当停车场的剩余车位为0时,显示的停车位数量就为0了;

1)创建Semaphore示例, 初始化为4, 表示有4个可用资源.

2)acquire方法表示申请资源(P操作), release方法表示释放资源(V操作)

public class Main{public static void main(String[] args) {Semaphore semaphore=new Semaphore(10);Runnable runnable=new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"开始申请资源");try {semaphore.acquire();System.out.println(Thread.currentThread().getName()+"已经获取到资源了");semaphore.release();System.out.println(Thread.currentThread().getName()+"开始释放资源了");} catch (InterruptedException e) {throw new RuntimeException(e);}}};for(int i=0;i<10;i++){Thread t=new Thread(runnable);t.start();}}
}
public class Main{public static int count=0;public static void main(String[] args) throws InterruptedException {Semaphore semaphore=new Semaphore(1);Thread t1=new Thread(()-> {for (int i = 0; i < 10000; i++) {try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2=new Thread(()->{for(int i=0;i<10000;i++){try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

基于Semphore可以实现限流器:

什么是限流:比如说某一个广场,他的日载流量是6W,那么如果说假设有一天来了10W人,但是只能进去6W人,这个时候就只能排队入园了,因为工作人员始终会将人数控制在6W人

咱们再从生活中的事例回到程序当中,假设一个程序只能为 10W 人提供服务,突然有一天因为某个热点事件,造成了系统短时间内的访问量迅速增加到了 50W,那么导致的直接结果是系统崩溃,任何人都不能用系统了,显然只有少人数能用远比所有人都不能用更符合预期,因此这个时候要使用限流了

Semphore本身是依靠计数器的思想来进行实现的,它可以控制对于共享资源的访问数量,当线程需要访问该资源的时候,他必须先进行获取一个许可,就是从计数器中获取到资源

当计数器本身大于0的时候,线程可以获取到这个可用资源并且能够继续执行

当计数器本身等于0的时候,线程将会被阻塞,直到有其他的线程释放资源

Semphore本身有两个重要的操作,acquire()和realse()操作

1)当线程需要访问共享资源的时候,它会调用acquire方法来获取资源,如果计数器的值大于0,那么acquire()方法会将计数器的值减1,并且允许线程继续运行,如果计数器的值等于0,那么acquire()方法会使得线程阻塞,知道有其他线程释放资源

2)当线程使用完成共享资源以后,该线程可以调用realse方法来释放资源,realse()方法会使得计数器的值+1,表示有一个资源可以使用,其他被阻塞的线程可以有机会获得可用资源并且+1;

关于公平模式和非公平模式:

在这里面所谓的公平模式就是说线程调用acquire的先后顺序来获取到这个可用资源的,公平模式遵循先进先出原则,所以非公平模式是抢占式的,也就是说有可能一个新的获取线程恰好在一个许可证释放以后得到了这个许可证,但是这个已经获取许可证的线程前面还存在着一些其他的线程,当然在这里面非公平模式的性能比较高;

假设说,当有时候需要等待某一些线程执行完成了之后,再来执行主线程的代码,此时应该怎么做呢?可能有人会说,简单,用 join() 方法等待线程执行完成之后再执行主线程就行了,当然,如果使用的是 Thread 来执行任务,那这种写法也是可行的。然而真实的(编码)环境中我们是不会使用 Thread 来执行多任务的,而是会使用线程池来执行多任务,这样可以避免线程重复启动和销毁所带来的性能开销;

二)CountDownLatch:别急,等人齐了在开团

撞线:调用latch.countDown()

比赛结束,统计成绩:latch.await(),只要还存在着有任意的一个选手不进行撞线,那么比赛就无法结束,只有说所有的选手比赛撞了线,那么最终的比赛才可以结束

public class Main {public static void main(String[] args) throws InterruptedException {CountDownLatch latch=new CountDownLatch(10);for(int i=0;i<10;i++){Thread t=new Thread(()->{System.out.println("线程"+Thread.currentThread().getName()+"开始起跑");try {Thread.sleep(new Random().nextInt(10000));System.out.println("线程"+Thread.currentThread().getName()+"开始撞线");latch.countDown();} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();}latch.await();System.out.println("比赛完成");}
}

1)使用CountDownLatch可以实现等待所有任务执行完成以后再来执行主任务的功能,他就是类似于说好像比赛中等待所有运动员都完成比赛以后再来公布排名一样,当然咱们在大玩着荣耀的时候也是一样,只有说所有人集合完毕以后在开团

2)而CountDownLatch就是通过计数器来实现等待功能的,当创建CountDownLatch的时候会创建一个大于0的计数器,每一次调用countDown()方法的时候计数器的值会减1,直到计数器的值变成0以后,等待的任务就可以继续执行了

3)countDownLatch在底层实现的时候是依靠内部创建并维护了一个voltaile的计数器,当调用countDown()方法的时候,会尝试将整数计数器-1,CountDownLatch 在创建的时候需要传入一个整数,在这个整数“倒数”到 0 之前,主线程需要一直挂起等待,直到其他的线程都执行之后,主线才能继续执行

    public static void main(String[] args) throws InterruptedException {//创建CountDownLatch实现两个计数器CountDownLatch latch=new CountDownLatch(2);//创建线程池执行任务ExecutorService service= Executors.newFixedThreadPool(2);service.submit(new Runnable() {@Overridepublic void run() {System.out.println("我是线程池提交的第一个任务");latch.countDown();}});service.submit(new Runnable() {@Overridepublic void run() {System.out.println("我是线程池提交的第二个任务");latch.countDown();}});latch.await();System.out.println("线程池中的任务已经全部执行完成");}

三)循环栅栏(Cycbarrier):人齐了老司机就可以发车了

循环栅栏实现一个可以循环利用的屏障

https://img-blog.csdnimg.cn/img_convert/f10e1adb034e3ebaa2c02b11596386ee.gif

1)CycliBarrier作用是让一组线程之间可以相互等待,当到达一个共同点的时候,所有之前等待的线程会冲破栅栏,一起向下执行

2)现在举个例子来说:伟哥要做末班车回家,公交车站的司机会等待车上面的所有乘客坐满以后再来发车,还有比如说王者荣耀,得等待5个队友游戏都加载完了才可以进入到游戏

3)本质上来说是让多个线程共同相互等待,知道说当所有的线程都到达了屏障点以后,之前的所有线程才可以继续向下执行,CycBarrier本身就象老司机开车一样,如果车上面还有空闲的座位,那么司机就得等着,只有说当作为坐满以后,老司机才发车

    public static void main(String[] args) {CyclicBarrier barrier=new CyclicBarrier(10, new Runnable() {@Overridepublic void run() {System.out.println("现在司机上面的人都到齐了开始进行发车");System.out.println("当前线程池中的任务都已经执行完成了");}});ExecutorService service=Executors.newFixedThreadPool(10);for(int i=0;i<10;i++){service.submit(new Runnable() {@Overridepublic void run() {try {Thread.sleep(new Random().nextInt(5000));System.out.println("当前乘客开始上车"+Thread.currentThread().getName());barrier.await();//当前判断线程池中的任务执行完成可以执行多次System.out.println("当前线程下车"+Thread.currentThread().getName());} catch (InterruptedException e) {throw new RuntimeException(e);} catch (BrokenBarrierException e) {throw new RuntimeException(e);}}});}}
}

在CycliBarrier底层是基于计数器来实现的,当count不为0的时候,每一个线程在到达屏障点以后会先进行调用await()方法将自己阻塞,此时计数器会减1,此时这个线程会阻塞在这个屏障处,当循环栅栏的计数器被减为0的时候,所有调用await()的线程就会被唤醒,就会冲破栅栏,一起执行,CountDownLatch和CycliBarrier在底层都是依靠计数器来实现的,但是CountDownLatch只能使用一次,但是CycliBarrier却可以使用多次,这就是两者最大的区别

总结:CycliBarrier在底层是依靠ReentranLock来实现计数器的原子性更新的,CycliBarrier最常使用的就是await()方法,使用该方法就会将计数器的值减1,并判断当前的计数器是否为0,如果不是0就阻塞等待,并且当计数器变成0以后,该线程也就是阻塞在循环栅栏的线程才可以继续执行剩余任务;

三)线程池的状态:

1)Running状态:运行状态,线程池创建完成以后就进入到这个状态,如果不手动调用关闭方法,那么线程池在整个程序运行过程中都是这个状态;

2)ShutDown状态:关闭状态,线程池本身不再接受新任务的提交,但是会有先将线程池中已经存在的任务处理完成

3)Stop停止状态:不再接受新任务的提交,并且会中断正在执行的任务,放弃任务队列中已经存在的任务

4)tidying状态:整理状态,所有的任务都执行完成以后,也包括任务队列中的任务执行完成,当前线程池中的活动线程数降为0的状态,到达此状态以后会调用线程池的terminated方法

5)terminated状态:销毁状态,当调用线程池的terminated方法以后会进入到这个状态

      ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 10,100, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread(r);}}){@Overrideprotected void terminated() {super.terminated();System.out.println("线程池终止");}};

1)当进行调用shutDown方法的时候,线程池中的状态会由Running状态到达shutDown状态,最后在到达tidying状态,最后到达terminated状态

2)当进行调用shutDownNow方法的时候,线程池中的状态会由running状态到达stop状态,最后在到达tidying状态,最后到达terminated状态

3)进行调用terminated方法,线程池会直接从tidying状态到达terminated状态,可以在阻塞队列的时候重写此方法,默认来说这个方法是空的

四)如何判断线程池中的任务都已经执行完成了?

1)在很多场景下,都希望等待线程池中的所有任务都执行完,然后再来执行下一步操作,对于Thread类来说,这样的实现是很简单的,加上一个join方法就解决了,但是对于线程池的判断就比较麻烦了

2)从上面的执行结果可以看出来,程序先打印了任务执行完成,再来继续打印并执行线程池的任务,这种执行顺序混乱的结果不是我们想要看到的,我们期望的结果就是等到鲜橙汁中的所有任务都执行完成了,再来进行打印线程池执行完成的信息;

3)产生少数问题的原因就是主线程main和线程池是并发执行的,所以说当线程池还没有执行完main现成的打印结果就已经执行了,想要解决这个问题就需要在打印结果之前,先判断线程池中的任务是否已经执行完成,如果没有执行完成就等到任务执行完成再来打印结果

 public static void main(String[] args) {ExecutorService service=Executors.newFixedThreadPool(10);for(int i=0;i<10;i++){service.submit(new Runnable() {@Overridepublic void run() {System.out.println("开始执行线程池中的任务");}});}System.out.println("线程池中的所有任务执行完成");}
1)使用isTerminated()方法来判断:

1)使用线程池的终止状态来进行判断线程池中的任务是否已经全部执行完成,但是如果想要让线程之中的状态改变就需要调用shutDown()方法,不然线程池会一直处于Running运行状态那么就没有办法来进行判断是否处于终止状态来判断线程池中的任务是否已经全部执行

2)shutdown方法是启动线程池有序关闭的方法,它在关闭之前会执行完成所有已经提交的任务,并且不会再进行接收新的任务,当线程池中的所有任务都执行完成以后,线程池就处于终止状态了,此时isTerminated()方法返回的结果也就是true了;

缺点:需要关闭线程池

 ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 10,100, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), new ThreadFactory() {@Overridepublic Thread newThread(Runnable r) {return new Thread(r);}}){@Overrideprotected void terminated() {super.terminated();System.out.println("线程池终止");}};executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务1");}});executor.submit(new Runnable() {@Overridepublic void run() {System.out.println("执行任务2");}});executor.shutdown();while(!executor.isTerminated()){}System.out.println("线程池中的任务已经执行完成了");
2)判断getCompletedTaskCount和getTaskCount是否相等

getTaskCount()返回执行计划任务的总数,但是因为本身任务和线程的状态都在不断地发生变化,因此返回的值是一个近似值;

getCompetedTaskCount()返回完成执行的任务总数,但是因为本身任务和线程的状态都在不断地发生变化,因此返回的值是一个近似值,但是在连续的调用中并不会减少

虽然不需要关闭线程池,但是可能会造成一定的误差

3)调用countDownLatch和CycliBarrier

需要注意的是countDownLatch中的countDown()方法和CycliBarrier中的await()方法需要在线程池的run方法的最后调用

4)使用FutureTask

FutureTask中的优势就是判断比较精准,调用每一个线程的FutureTask的get方法就是等待该任务执行完成的,需要使用submit进行提交:

 public static void main(String[] args) throws ExecutionException, InterruptedException {ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0,TimeUnit.SECONDS,new LinkedBlockingDeque<>(100));FutureTask<Integer> task1=new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int a=10;a++;System.out.println("a++完成");return a;}});FutureTask<Integer> task2=new FutureTask<>(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int b=11;b++;System.out.println("b++完成");return b;}});executor.submit(task1);executor.submit(task2);Integer result1= task1.get();Integer result2=task2.get();System.out.println("线程池中的任务都已经执行完成");}

五)submit和execute的区别:

1)接收到的参数不同:submit方法只能接受到runnable接口的任务,但是submit方法及可以接受到runnable方法的任务,也可以接收到callable,futureTask类型的任务,前者没有返回值,后者可以后返回值;

2)execute()的返回值是void,线程提交后不能得到线程的返回值,submit()的返回值是Future,通过Future的get()方法可以获取到线程执行的返回值,get()方法是同步的,执行get()方法时,如果线程还没执行完,会同步等待,直到线程执行完成

注意:虽然submit()方法可以提交Runnable类型的参数,但执行Future方法的get()时,线程执行完会返回null,不会有实际的返回值,这是因为Runable本来就没有返回值

这篇关于JUC包下面的四大天王+线程池部分知识的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/391838

相关文章

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

Java子线程无法获取Attributes的解决方法(最新推荐)

《Java子线程无法获取Attributes的解决方法(最新推荐)》在Java多线程编程中,子线程无法直接获取主线程设置的Attributes是一个常见问题,本文探讨了这一问题的原因,并提供了两种解决... 目录一、问题原因二、解决方案1. 直接传递数据2. 使用ThreadLocal(适用于线程独立数据)

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

sqlite3 相关知识

WAL 模式 VS 回滚模式 特性WAL 模式回滚模式(Rollback Journal)定义使用写前日志来记录变更。使用回滚日志来记录事务的所有修改。特点更高的并发性和性能;支持多读者和单写者。支持安全的事务回滚,但并发性较低。性能写入性能更好,尤其是读多写少的场景。写操作会造成较大的性能开销,尤其是在事务开始时。写入流程数据首先写入 WAL 文件,然后才从 WAL 刷新到主数据库。数据在开始

高效录音转文字:2024年四大工具精选!

在快节奏的工作生活中,能够快速将录音转换成文字是一项非常实用的能力。特别是在需要记录会议纪要、讲座内容或者是采访素材的时候,一款优秀的在线录音转文字工具能派上大用场。以下推荐几个好用的录音转文字工具! 365在线转文字 直达链接:https://www.pdf365.cn/ 365在线转文字是一款提供在线录音转文字服务的工具,它以其高效、便捷的特点受到用户的青睐。用户无需下载安装任何软件,只

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

poj 2976 分数规划二分贪心(部分对总体的贡献度) poj 3111

poj 2976: 题意: 在n场考试中,每场考试共有b题,答对的题目有a题。 允许去掉k场考试,求能达到的最高正确率是多少。 解析: 假设已知准确率为x,则每场考试对于准确率的贡献值为: a - b * x,将贡献值大的排序排在前面舍弃掉后k个。 然后二分x就行了。 代码: #include <iostream>#include <cstdio>#incl

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

笔记整理—内核!启动!—kernel部分(2)从汇编阶段到start_kernel

kernel起始与ENTRY(stext),和uboot一样,都是从汇编阶段开始的,因为对于kernel而言,还没进行栈的维护,所以无法使用c语言。_HEAD定义了后面代码属于段名为.head .text的段。         内核起始部分代码被解压代码调用,前面关于uboot的文章中有提到过(eg:zImage)。uboot启动是无条件的,只要代码的位置对,上电就工作,kern