当前位置:   article > 正文

JUC(java.util.concurrent)的常见类

JUC(java.util.concurrent)的常见类

目录

前言

Callable和Future

Callable

Future

 使用Callable和FutureTask

ReentrantLock

 ReentrantLock和synchronized的区别

如何选择使用哪个锁?

原子类

线程池

Semaphore(信号量)

 CountDownLatch

相关面试题

1.线程同步的方式有哪些?

2.为什么有了synchronized还需要JUC下的lock?

3.AtomicInteger 的实现原理是什么?

4) 信号量听说过么?之前都用在过哪些场景下?

5. 解释⼀下ThreadPoolExecutor构造⽅法的参数的含义


前言

在前面我们已经学习java.util.concurrent包(实现多线程并发编程常用的包)中的一些操作,例如线程池、阻塞队列等,那么本篇我们就来学习一下JUC中几种其他常见的类。

Callable和Future

Callable

Callable是一个泛型接口,只有一个方法 call().用于创建可以返回结果的任务,与Runnable不同,Callable可以返回一个结果,并且可以抛出异常,需要依赖FutureTask类来获取返回结果

我们查看Callable的接口定义:

在使用Callable接口的时候,我们需要将我们的任务需求编写到call()方法中。

示例:现在要计算从1加到1000的和

  1. class Demo1 {
  2. public static void main(String[] args) throws ExecutionException, InterruptedException {
  3. Callable<Integer> call = new Callable<Integer>() {
  4. @Override
  5. public Integer call() throws Exception {
  6. int sum = 0;
  7. for (int i = 0; i <= 1000; i++) {
  8. sum += i;
  9. }
  10. return sum;
  11. }
  12. };
  13. }
  14. }

在这个代码中,我们实现了从1加到1000的和的计算,当在完成计算之后,会返回一个Integer类型的数据。

Future

Future同样是一个泛型接口,用来表示异步计算的结果,在接口中提供了一些方法来检查任务是否完成、获取计算结果以及取消任务的执行。以下是Future接口的定义:

  1. public interface Future<V> {
  2. boolean cancel(boolean mayInterruptIfRunning);
  3. boolean isCancelled();
  4. boolean isDone();
  5. V get() throws InterruptedException, ExecutionException;
  6. V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
  7. }
  • cancel((boolean mayInterruptIfRunning):尝试取消任务的执行;
  • isCancelled():检查任务是否已经被取消;
  • isDone():检查任务是否已经完成;
  • get():获取任务的结果,如果任务尚未完成,就会阻塞当前线程,直到任务完成;
  • get(long timeout, TimeUnit unit):在指定的时间内获取任务的结果,如果任务尚未完成,则阻塞当前线程。

Future有它的实现类FutureTask供我们使用,我们可以看其文档解释,这段话的意思就是说:可取消的异步计算。FutureTask类提供了Future的基本实现,其中包括启动和取消计算、查询计算是否完成以及检索计算结果的方法。只有在计算完成后才能检索结果;如果计算尚未完成,get方法将阻塞。一旦计算完成,就不能重新启动或取消计算(除非使用runAndReset调用计算)。
FutureTask可以用来包装一个Callable或Runnable对象。因为FutureTask实现了Runnable,所以FutureTask可以提交给Executor执行。
除了作为一个独立的类,这个类还提供了受保护的功能,这在创建自定义任务类时可能很有用。
 

 我们可以在java8文档中查看相关方法:

 使用Callable和FutureTask

示例一:创建线程计算1+2+...+1000的和,使用Callable.

  1. /**
  2. * 主函数,用于演示如何通过Callable接口实现多线程计算
  3. * 本函数的目标是计算1到1000的和,通过创建一个Callable任务,并在新线程中执行该任务来实现
  4. * @param args 命令行参数
  5. * @throws ExecutionException 如果任务未能成功完成,则抛出此异常
  6. * @throws InterruptedException 如果当前线程在等待任务完成时被中断,则抛出此异常
  7. */
  8. public static void main(String[] args) throws ExecutionException, InterruptedException {
  9. // 创建一个Callable对象,用于计算1到1000的和
  10. Callable<Integer> call=new Callable<Integer>() {
  11. /**
  12. * 执行计算任务的方法
  13. * 本方法通过循环计算1到1000的和
  14. * @return 计算结果,即1到1000的和
  15. * @throws Exception 如果在计算过程中发生错误,则抛出此异常
  16. */
  17. @Override
  18. public Integer call() throws Exception {
  19. int sum=0;
  20. for(int i=0;i<=1000;i++){
  21. sum+=i;
  22. }
  23. return sum;
  24. }
  25. };
  26. // 使用Callable对象创建一个FutureTask,以便可以在新线程中执行任务
  27. FutureTask<Integer> futureTask=new FutureTask<>(call);
  28. // 创建一个新线程,并将FutureTask对象作为任务传递给该线程
  29. Thread t=new Thread(futureTask);
  30. // 启动新线程,开始执行计算任务
  31. t.start();
  32. // 获取计算任务的结果,主线程将在这里等待直到任务完成
  33. int result=futureTask.get();
  34. // 打印计算结果
  35. System.out.println(result);
  36. }

 结果

示例二:使用单个线程的线程池

  1. public static void main(String[] args) throws ExecutionException, InterruptedException {
  2. // 创建一个固定大小的线程池,这里只创建一个线程
  3. ExecutorService ex=Executors.newFixedThreadPool(1);
  4. // 创建一个Callable对象,用于执行计算任务
  5. Callable<Integer> calls=new Callable<Integer>() {
  6. @Override
  7. public Integer call() throws Exception {
  8. // 初始化求和变量
  9. int sum=0;
  10. // 计算从0到1000的累加和
  11. for(int i=0;i<=1000;i++){
  12. sum+=i;
  13. }
  14. // 返回计算结果
  15. return sum;
  16. }
  17. };
  18. // 创建一个FutureTask对象,并将Callable对象作为参数传递给它
  19. FutureTask<Integer> futureTask= (FutureTask<Integer>) ex.submit(calls);
  20. // 获取计算任务的结果,主线程将在这里等待直到任务完成
  21. int result=futureTask.get();
  22. // 打印计算结果
  23. System.out.println(result);
  24. // 关闭ExecutorService
  25. ex.shutdown();
  26. }

在这段代码中,我们创建了一个只有一个线程的线程池,并且创建了一个Callable对象并重写其中的call方法。通过调用submit方法提交任务并获取FutureTask对象。但由于此时返回的是Futue类型的结果,所以我们需要将其强转为FutureTask<Integer>。同时我们通过调用其中的get方法来等待任务执行完成并获取到结果。需要注意,这里我们需要手动关闭线程池。释放资源。

示例三:拥有多个线程的线程池

  1. class Demo4{
  2. // 主函数,展示如何使用固定大小的线程池执行Callable任务
  3. public static void main(String[] args) throws ExecutionException, InterruptedException {
  4. // 创建一个固定大小为4的线程池
  5. ExecutorService ex=Executors.newFixedThreadPool(4);
  6. // 创建一个可返回结果的Callable任务
  7. Callable<String> calls=new Callable<String>() {
  8. // 实现call方法,当任务被线程池执行时,此方法将被调用
  9. @Override
  10. public String call() throws Exception {
  11. // 输出当前线程的名称和执行的信息
  12. System.out.println(Thread.currentThread().getName()+"执行了call方法");
  13. // 返回任务执行结果
  14. return "result";
  15. }
  16. };
  17. // 循环提交4个任务到线程池
  18. for(int i=0;i<4;i++){
  19. // 提交任务并获取FutureTask对象,用于查询任务执行结果
  20. FutureTask<String> futureTask= (FutureTask<String>) ex.submit(calls);
  21. // 获取并输出任务的执行结果
  22. System.out.println(futureTask.get());
  23. }
  24. // 关闭线程池,不再接受新的任务
  25. ex.shutdown();
  26. }
  27. }

ReentrantLock

ReentrantLock是一个可重入互斥锁,允许一个线程多次获取同一个锁而不会产生死锁,和synchronized类似,都是用来保证线程安全的。

ReentrantLock加锁有两种方式:

  1. lock():加锁,如果获取不到锁就进入阻塞等待。
  2. trylock():加锁,如果获取不到锁,就会放弃加锁。
  3. unlock():解锁。

在使用ReentrantLock的时候需要手动释放锁unlock(),如果忘记解锁,可能会带来比较严重的后果。所以我们可以使用try-finally来进行结果操作

  1. ReentrantLock lock=new ReentrantLock();
  2. try{
  3. lock.lock();
  4. //working
  5. }finally {
  6. lock.unlock();
  7. }

 ReentrantLock和synchronized的区别

  • ReentrantLock可以使用lock()和tryLock()进行加锁,使用lock()的时候,若没有获取到锁就会进入阻塞等待,而使用tryLock()的时候,如果没有获取到锁,就会放弃获取而不是阻塞等待
  • synchronized是一个关键字,是JVM内部实现的;ReentrantLock是标准库中的一个类,在JVM外部实现的(基于Java实现)。
  • synchronized不需要手动解锁,而ReentrantLock需要手动释放锁,使用起来更加灵活,但是也容易遗漏unlock,所以最好在加上try-finally使用。
  • synchronized在申请锁失败时,会死等.ReentrantLock可以通过trylock的⽅式等待⼀段时间就放弃.
  • synchronized是⾮公平锁,ReentrantLock默认是⾮公平锁.可以通过构造⽅法传⼊⼀个true开启 公平锁模式.
  • 更强⼤的唤醒机制.synchronized是通过Object的wait/notify实现等待-唤醒.每次唤醒的是⼀个 随机等待的线程.而ReentrantLock搭配Condition类实现等待-唤醒,可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

  • 锁竞争不激烈的时候,使⽤synchronized,效率更⾼,⾃动释放更⽅便

  • 锁竞争激烈的时候,使⽤ReentrantLock,搭配trylock更灵活控制加锁的⾏为,⽽不是死等.

  • 如果需要使⽤公平锁,使⽤ReentrantLock. 

 虽然ReentrantLock使用起来比较灵活,但一般情况下建议使用synchronized。

原子类

原子类内部其实是使用CAS实现的,CAS是一个原子的操作,不需要加锁,性能比加锁的要好多

• AtomicBoolean

• AtomicInteger

• AtomicIntegerArray

• AtomicLong

• AtomicReference

• AtomicStampedReference

CAS的原理以及如何使用CAS在上一篇我已经讲过,想了解更多的可以看看CAS原理

线程池

线程池是为了解决频繁创建和销毁线程带来的性能和资源浪费问题

在java中使用线程池我们需要用到ExecutorService和Executors

  • ExecutorService 表示一个线程池实例.
  • Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
  • ExecutorService 的 submit 方法能够向线程池中提交若干个任务

Executors是一个工厂类,里面提供了创建不同风格线程池的方法。

  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer。

Executors本质上是对ThreradPoolExecutor的封装

感兴趣的可以看看这一篇【JavaEE】线程池-CSDN博客,里面讲解了关于ThreadPoolExecutor构造方法的相关参数的含义。

示例:

  1. class Demo5{
  2. /**
  3. * 程序入口
  4. * @param args 命令行参数
  5. */
  6. public static void main(String[] args) {
  7. // 创建一个固定大小的线程池,用于执行任务
  8. ExecutorService ex=Executors.newFixedThreadPool(4);
  9. // 提交一个运行时打印"hello"的无返回值任务到线程池
  10. ex.submit(()->{
  11. System.out.println("hello");
  12. });
  13. // 关闭线程池,不再接受新的任务提交,但已提交的任务将继续执行完成
  14. ex.shutdown();
  15. }
  16. }

Semaphore(信号量)

信号量(Semaphore)又称为信号灯。用来表示“可用资源的个数”,本质上就是一个计数器。在多线程环境下用于协调各个线程, 以保证它们能够正确、合理的使用公共资源。

信号量是一个非负整数,当我们申请资源的时候,计数器就会-1,称为“P操作”,释放资源的时候,计数器就会+1,也称为“V操作”。当计数器为0时,如果再申请资源,就会阻塞等待,直到有其他线释放资源。

Semaphore的PV操作中的加减计数器操作都是原⼦的,可以在多线程环境下直接使用.

信号量可以分为二元信号量和计数信号量两种,二元信号量就相当于一把锁,当有线程申请资源时,就相当于加锁,此时计数器-1,信号量就为0;当有其他线程想要申请资源就会陷入阻塞等待,直到占用资源的线程释放,才能获取到。计数信号量就表示此时有多少可以被申请的资源,当有线程申请资源,计数器就-1,当有线程释放资源,计数器就+1。

加锁和解锁的操作就可以看做,加锁时信号量为0,解锁信号量为1.

在java中,把信号量的相关操作封装在Semaphore中,acquire()方法表示申请资源,release()表示释放资源。availablePermits()可以查看信号量中还有多少资源可用。

示例:

  1. /**
  2. * SemaphoreDemo类用于演示信号量的应用
  3. */
  4. class SemaphoreDemo{
  5. // 用于计数的共享变量
  6. static int count=0;
  7. /**
  8. * 程序的入口点
  9. * @param args 命令行参数
  10. * @throws InterruptedException 如果线程被中断
  11. */
  12. public static void main(String[] args) throws InterruptedException {
  13. // 创建一个信号量,初始值为1,用于控制同时只能有一个线程执行临界区代码
  14. Semaphore semaphore=new Semaphore(1);
  15. // 创建一个公平的可重入锁,本例中未直接使用,但展示了如何创建
  16. ReentrantLock locker=new ReentrantLock(true);
  17. // 创建第一个线程,用于增加计数器
  18. Thread t1=new Thread(()->{
  19. for (int i=0;i<1000;i++) {
  20. try {
  21. // 获取信号量,允许进入临界区
  22. semaphore.acquire();
  23. // 执行临界区操作:增加计数器
  24. count++;
  25. // 释放信号量,允许其他线程进入临界区
  26. semaphore.release();
  27. } catch (InterruptedException e) {
  28. // 如果线程被中断,则抛出运行时异常
  29. throw new RuntimeException(e);
  30. }
  31. }
  32. });
  33. // 创建第二个线程,同样用于增加计数器
  34. Thread t2=new Thread(()->{
  35. for (int i=0;i<1000;i++) {
  36. try {
  37. // 获取信号量,允许进入临界区
  38. semaphore.acquire();
  39. // 执行临界区操作:增加计数器
  40. count++;
  41. // 释放信号量,允许其他线程进入临界区
  42. semaphore.release();
  43. } catch (InterruptedException e) {
  44. // 如果线程被中断,则抛出运行时异常
  45. throw new RuntimeException(e);
  46. }
  47. }
  48. });
  49. // 启动第一个线程
  50. t1.start();
  51. // 启动第二个线程
  52. t2.start();
  53. // 等待第一个线程结束
  54. t1.join();
  55. // 等待第二个线程结束
  56. t2.join();
  57. // 输出计数结果
  58. System.out.println(count);
  59. }
  60. }

 CountDownLatch

CountDownLatch是java中的一个同步工具类,用来协调多个线程之间的同步,初始值为线程的数量。

CountDownLatch通过一个计数器功能,当一个线程完成了自己的任务,计数器的值就-1,当计数器为0时,说明所有的线程都完成任务,此时在CountDownLatch等待的线程就可以恢复执行。

CountDownLatch一个典型用法就是把一个大任务拆分成N个小任务,让多个线程来执行小任务,每个线程执行完自己的任务计数器就-1,当所有小任务都完成后,等待所有小任务完成的线程才继续往下执行。就好比富士康手机加工的流水线一样,组装一步手机需要一条条的流水线来相互配合完成。一条条流水线(Worker),每条线都干自己的活。有的流水线是贴膜的,有的流水线是打螺丝的,有的流水线是质检的、有的流水线充电的、有的流水线贴膜的。等这些流水线都干完了就把一部手机组装完成了。

方法

CountDownLatch(int count):count为计数器的初始值(一般需要多少个线程执行,count就设为几)。
countDown(): 每调用一次计数器值-1,直到count被减为0,代表所有线程全部执行完毕。
getCount():获取当前计数器的值。
await(): 等待计数器变为0,即等待所有异步线程执行完毕。
boolean await(long timeout, TimeUnit unit): 

  1. 此方法至多会等待指定的时间,超时后会自动唤醒,若 timeout 小于等于零,则不会等待
  2. boolean 类型返回值:若计数器变为零了,则返回 true;若指定的等待时间过去了,则返回 false
  1. class CountDownLatchDemo{
  2. public static void main(String[] args) throws InterruptedException {
  3. //创建一个固定大小的线程池,用于处理下载任务
  4. ExecutorService service= Executors.newFixedThreadPool(4);
  5. //创建一个计数器,用于等待所有下载任务完成
  6. CountDownLatch count=new CountDownLatch(20);//20个任务
  7. //提交20个下载任务到线程池
  8. for(int i=1;i<=20;i++){
  9. int id=i;
  10. //使用lambda表达式创建匿名内部类,定义每个下载任务的执行逻辑
  11. service.submit(()->{
  12. System.out.println("下载任务"+id+"正在执行");
  13. try {
  14. //模拟下载任务的执行时间
  15. Thread.sleep(3000);
  16. } catch (InterruptedException e) {
  17. //处理中断异常
  18. throw new RuntimeException(e);
  19. }
  20. //任务完成
  21. System.out.println("下载任务"+id+"执行完毕");
  22. //计数器减1,表示一个任务完成
  23. count.countDown();
  24. });
  25. }
  26. //等待所有任务完成,计数器归零
  27. count.await();
  28. System.out.println("所有下载任务已完成");
  29. //关闭线程池
  30. service.shutdown();
  31. }
  32. }

 

当调用了20次 countDown() 方法之后,await() 方法才会结束等待,继续执行后面的代码。

需要注意的是,CountDownLatch是一次性的,一旦计数器的值达到0,就不能再次使用。如果需要多次使用类似的功能,可以考虑使用CyclicBarrier等其他同步工具类。

相关面试题

1.线程同步的方式有哪些?

synchronized、ReentrantLock、Semaphorer等都可以用于线程同步。

2.为什么有了synchronized还需要JUC下的lock?

以 juc 的 ReentrantLock 为例,

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
  • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

3.AtomicInteger 的实现原理是什么?

基于CAS机制,伪代码如下:

  1. class AtomicInteger {
  2. private int value;
  3. public int getAndIncrement() {
  4. int oldValue = value;
  5. while ( CAS(value, oldValue, oldValue+1) != true) {
  6. oldValue = value;
  7. }
  8. return oldValue;
  9. }
  10. }

4) 信号量听说过么?之前都用在过哪些场景下?

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.

使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作.

5. 解释⼀下ThreadPoolExecutor构造⽅法的参数的含义

可以查看【JavaEE】线程池-CSDN博客


以上就是本篇所有内容,若有不足,欢迎指正~
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/1011915
推荐阅读
相关标签
  

闽ICP备14008679号