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

JUC 组件

  • 前言
  • 一、Callable
  • 二、ReentrantLock
  • 三、Atomic 原子类
  • 四、线程池
  • 五、Semaphore
  • 六、CountDownLatch


前言

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究竟是什么,下面我们再来总结一下:

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

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

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

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

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

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

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

二、ReentrantLock

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

ReentrantLock 和 synchronized 的区别

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

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

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

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

  5. 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 操作)

Java 多线程系列Ⅵ(并发编程的六大组件)_第1张图片

: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();
        }
    }
}

Java 多线程系列Ⅵ(并发编程的六大组件)_第2张图片

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("比赛结束");
    }
}

Java 多线程系列Ⅵ(并发编程的六大组件)_第3张图片

你可能感兴趣的:(并发编程,java,并发编程,JUC组件,java-ee)