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("比赛结束");
    }
}
相关文章
|
9天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
11天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
11天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
11天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
35 3
|
11天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
93 2
|
19天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
46 6
|
5月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
142 1
|
8月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
245 2
|
8月前
|
Java 数据库
【Java多线程】对线程池的理解并模拟实现线程池
【Java多线程】对线程池的理解并模拟实现线程池
77 1
|
5月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
89 6