Java 多线程系列Ⅵ(并发编程的六大组件)

简介: Java 多线程系列Ⅵ(并发编程的六大组件)

前言

JUC(Java.util.concurrent)是 Java 标准库中的一个包,它提供了一组并发编程工具,本篇文章就介绍几组常见的 JUC 组件:Callable、ReentranLock、Atomic原子类、线程池、Semaphore、CountDownLatch。

一、Callable

类似于 Runnable,Callable也是一个 interface,用来描述一个任务。与Runnable接口不同的是Callable接口是描述了一个具有返回值的任务。

📝例如我们创建1个线程计算1到10000的累加和,并且要求返回结果值,我们就可以使用 Callable:

public class TestCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        
        // 使用 Callable 创建一个有返回值的任务
        // Callable 带有泛型参数,泛型参数表示返回值的类型。
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 10000; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        
        // 使用 FutureTask 包装一下。相当于一张任务凭据,后续可以使用凭据拿到结果。
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        
        // 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的
        // call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中
        Thread t = new Thread(futureTask);

    // 启动线程
        t.start();
        
        // 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕.无需使用 join
        int sum = futureTask.get();

        System.out.println(sum);
    }
}

通过上述对Callable接口的简单使用,以及阅读代码中的注释,相信你已经对Callable接口有了一定的理解,那么Callable究竟是什么,下面我们再来总结一下:

Callable 是一个 interface ,相当于把线程封装了一个 “返回值”,方便程序猿借助多线程的方式计算结果。


另外 Callable 和 Runnable 相似,都是描述一个 “任务”。Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。Callable 通常需要搭配 FutureTask 来使用,FutureTask 用来保存 Callable 的返回结果。


因为 Callable 往往是在另一个线程中执行的,什么时候执行完并不确定,通过调用 FutureTask 的 get 方法能够阻塞等待新线程计算完毕。

实现Callable接口也是创建线程的新方式,目前为止我们已学过的线程创建如下:

继承 Thread 类:继承 Thread 类并重写其中的 run() 方法,然后创建 Thread 的子类实例并调用 start() 方法即可启动一个新线程。


实现 Runnable 接口:实现 Runnable 接口并重写其中的 run() 方法,然后创建 Thread 实例时传入该 Runnable 对象并调用 start() 方法即可启动一个新线程。


实现 Callable 接口:实现 Callable 接口并重写其中的 call() 方法,然后创建 FutureTask 对象并将其作为参数传入 Thread 构造函数中,再调用 start() 方法即可启动一个新线程。

二、ReentrantLock

synchronized 和 ReentrantLock 都是用于实现多线程同步的工具,它们的目的都是为了保证多个线程对共享资源的安全访问。但是它们的实现机制和用法略有不同:

ReentrantLock 和 synchronized 的区别

synchronized 关键字是JVM内部实现的。ReentrantLock 是标准库的一个类,是JVM外部实现的。


synchronized 是基于 代码块 的方式来控制加锁的,不需要手动释放锁。ReentrantLock 提供了 lock,unlock 独立的方法,来进行加锁解锁,使用起来更灵活,但是需要手动释放锁。


synchronized 在申请锁失败时,会 死等。ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃加锁。


synchronized 是非 公平锁。ReentrantLock 默认是 非公平锁,但它可以通过构造方法传入一个 true 开启公平锁模式。


synchronized 搭配 wait、notify 进行等待唤醒,如果多个线程 wati 同一个对象,notify 将随机唤醒一个。ReentrantLock 则是搭配 Condition 这个类,这个类也能起到等待-唤醒,但是功能更强大,可以更精确控制唤醒某个指定的线程。

Tips:当然在大部分情况下 synchronized 就足够了,但是 ReentrantLock 是一个重要补充!

三、Atomic 原子类

JUC中还提供了一些原子类,原子类内部用的是 CAS 实现,性能要比加锁高得多:

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicIntegerArray
  4. AtomicLong
  5. AtomicReference
  6. AtomicStampedReference

这些原子类大家了解,可以简单使用即可,这里就不做过多的展开介绍了。

四、线程池

点击这里 --> 转到多线程案例,线程池模块介绍。

五、Semaphore

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

其实信号量就相当于生活中的停车场:


停车场中有一定数量的停车位,假设这个停车场最多只能容纳50辆汽车,那么当停车场中已经有49辆汽车时,如果进来了一辆汽车,就相当于申请一个可用资源,可用车位就 -1。(-1操作称为信号量的 P 操作)


此时计数器的值已经为 0了,即现在停车场已经没有车位可用了,如果这时再来新的汽车,就需要进行等待,直到有一辆汽车离开,相当于释放一个可用资源,可用车位就+1,此时有了停车位可用,这辆汽车才能够进入停车场停车。(+1操作称为信号量的 V 操作)

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

使用示例:

在 JUC 的 Semaphore 中,acquire 方法表示申请资源(P操作),release 方法表示释放资源(V操作)。

public class Demo1 {
    public static void main(String[] args) {
      // 创建 1 个初始值为 4 的信号量
        Semaphore semaphore = new Semaphore(4);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("申请资源");
                    // acquire 方法表示申请
                    semaphore.acquire();
                    System.out.println("我获取到资源了");
                    Thread.sleep(1000);
                    System.out.println("我释放资源了");
                    // release 方法表示释放
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 20; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

Semaphore 实际开发中的场景——共享锁

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

六、CountDownLatch

CountDownLatch 线程同步工具类,可以理解为同时等待 N 个任务执行结束。


就例如跑步比赛,10个选手依次就位,哨声响才同时出发,所有选手都通过终点,才能公布成绩,即比赛结束取决于最后一个选手冲过终点。


具体实现:

(1)构造 CountDownLatch 实例 latch,初始化 10 表示有 10 个任务需要完成.

(2)每个任务执行完毕,都调用 latch.countDown() ,在 CountDownLatch 内部的计数器同时自减。

(3)主线程中使用 latch.await(); 阻塞等待所有任务执行完毕,当计数器为 0 了,阻塞就解除,继续进行后续操作。

public class Demo2 {
    public static void main(String[] args) throws Exception {
        // 创建 1 个原子类,用于多线程计数
        AtomicInteger atomicInteger = new AtomicInteger(1);
        // 创建 1 个初始值为 10 的线程同步工具类
        CountDownLatch latch = new CountDownLatch(10);
        Runnable r = new Runnable() {
            @Override
            public void run() {
                try {
                    // 生成一个 0-9999之间的随机数,表示比赛时间
                    Thread.sleep((long) (Math.random() * 10000));
                    System.out.println("第: "+atomicInteger.getAndIncrement()+"选手完成了比赛");
                    latch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
        }
        // 必须等到 10 人全部回来
        latch.await();
        System.out.println("比赛结束");
    }
}
相关文章
|
4天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
45 14
|
7天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
37 13
|
8天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
1月前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
66 11
|
1月前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
1月前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
109 17
|
2月前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
2月前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
2月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。

热门文章

最新文章