JUC(java.util.concurrent)的常见类: Callable interface/ ReentrantLock/原子类/ 线程池/ 信号量/CountDownLatch

文章目录

  • JUC(java.util.concurrent)的常见类
    • 1. Callable interface
    • 2. ReentrantLock
    • 3. 原子类
    • 4. 线程池
    • 5. 信号量 Semaphore
    • 6. CountDownLatch

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

1. Callable interface

使用Callable也可以创建线程

  • Runnable能表示一个任务(run方法)

    run方法返回的是void

  • Callable也能表示一个任务(call方法)

    返回一个值, 类型可以用泛型参数来指定

如果关心多线程的执行过程, 使用Runnable即可.

如果是关心多线程的计算结果, 使用Callable更合适

代码示例: 创建线程计算1 + 2 + 3 + … + 1000

不使用Callable

  • 创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象.

  • main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + … + 1000.

  • 主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了).

  • 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果

static class Result {
    public int sum = 0;
    public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
    Result result = new Result();
    Thread t = new Thread() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
            }
            synchronized (result.lock) {
                result.sum = sum;
                result.lock.notify();
            }
        }
    };
    t.start();
    synchronized (result.lock) {
        while (result.sum == 0) {
            result.lock.wait();
        }
        System.out.println(result.sum);
    }
}

可以看出, 上述代码比较复杂, 容易出错.

使用Callable

  • 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.

  • 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.

  • 把 callable 实例使用 FutureTask 包装一下.

  • 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.

  • 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Callable<Integer> callable = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 1; i <= 1000; i++) {
                sum += i;
            }
            return sum;
        }
    };
    //这里的泛型和上面Callable的泛型保持一致
    FutureTask<Integer> futureTask = new FutureTask<>(callable);
    Thread t = new Thread(futureTask);
    t.start();
    Integer result = futureTask.get();
    System.out.println(result);
}

Callable实例不能直接作为Thread类构造方法的参数, 需要借助一个辅助类FutureTask

2. ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”

  • lock(): 加锁, 如果获取不到锁就死等.
  • unlock(): 解锁

ReentrantLock有着synchronized没有的独特优势:

  1. 提供了trylock方法进行加锁.

    public boolean tryLock(): 加锁失败直接返回false

    public boolean tryLock(long timeout, TimeUnit unit)

    • timeout: 等待锁的时间
    • unit: timeout参数的时间单位
    • 如果当前线程已经持有此锁,该方法返回true。
    • 如果锁由另一个线程持有,那么当前线程将因线程调度目的而被禁用,并处于休眠状态,直到发生以下三种情况之一:
      • 锁由当前线程获取: 则返回值true
      • 其他线程中断当前线程: InterruptedException被抛出
      • 指定的等待时间已过: 如果超过了指定的等待时间,则返回值false。如果时间小于或等于零,则该方法根本不会等待。
  2. 有两种模式, 可以工作在公平锁状态下, 也可以工作在非公平锁的状态下, 通过构造方法中参数的设定来决定(synchronized只能工作在非公平锁的状态下)

  3. ReentrantLock也有等待通知的机制, 搭配Condition类来完成. 比wait notify功能更强大. 比如notify()只能随即唤醒一个线程; 但是Condition可以精确地唤醒其中某个线程

劣势就是: unlock()容易漏, 所以要放在finally里面执行

ReentrantLocksynchronized其他区别:

  1. synchronized的锁对象可以是任意对象; 而ReentrantLock的锁对象就是它本身
  2. synchronized 是一个关键字, 是 JVM 内部实现的. ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
  3. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
  4. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  5. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
  6. 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

3. 原子类

标准库中提供了java.util.concurrent.atomic包, 里面的类都是基于CAS来实现的原子类.

JUC(java.util.concurrent)的常见类: Callable interface/ ReentrantLock/原子类/ 线程池/ 信号量/CountDownLatch_第1张图片

原子类的应用场景有哪些呢?

  1. 计数需求

    例如, 播放量,点赞量,投币量,转发量,收藏量

    一个视频, 有很多人同时播放,点赞,收藏…

  2. 统计效果

    例如, 统计应用程序出错的数目, 可以使用原子类记录, 通过监控服务器, 获得线上服务器错误数量, 并以曲线图的方式会知道页面上

4. 线程池

上文已经介绍过了, 这里就不再赘述了. 在这

5. 信号量 Semaphore

在操作系统中经常出现, 是并发编程中的一个重要的概念/组件.

准确来讲, semaphore是一个计数器(变量), 描述了"可用资源的个数". 这个"可用资源", 也叫"临界资源", 是指多个线程/进程等并发执行的实体可以公共使用到的资源(多个线程修改同一个变量, 这个变量就可以认为是临界资源)

理解信号量

可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.

当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)

当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)

如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

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

代码示例

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

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

  • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.

public static void main(String[] args) throws InterruptedException {
    //计数器初始值为4
    Semaphore semaphore = new Semaphore(4);
    semaphore.acquire();//计数器-1
    System.out.println("执行P操作");
    semaphore.acquire();//计数器-1
    System.out.println("执行P操作");
    semaphore.acquire();//计数器-1
    System.out.println("执行P操作");
    semaphore.acquire();//计数器-1
    System.out.println("执行P操作");
    semaphore.acquire();//计数器的值为0, 阻塞等待
    System.out.println("执行P操作");
}

JUC(java.util.concurrent)的常见类: Callable interface/ ReentrantLock/原子类/ 线程池/ 信号量/CountDownLatch_第2张图片

6. CountDownLatch

这是针对特定场景的一个组件, 同时等待 N 个任务执行结束.

比如下载一个较大的文件, 会比较慢. 但是有一些"多线程下载器", 把一个文件拆分成多个小部分, 使用多个线程分别下载一部分, 每个线程分别是网络连接, 会大幅度提高下载速度.

假设分成十个线程, 10个部分来下载, 得等到10各部分都下载完成, 整体才算下载完成

那么如何判定整体已经下载完了呢? 这就需要用到CountDownLatch了

代码案例

public static void main(String[] args) throws InterruptedException {
    //构造方法中, 指定创建几个任务
    CountDownLatch countDownLatch = new CountDownLatch(10);
    //创建十个线程完成任务
    for (int i = 0; i < 10; i++) {
        int l = i;
        Thread t = new Thread(() -> {
            //涉及变量捕获, java的变量捕获要求被捕获的变量时final修饰的, 或者事实上是final的变量(值不变)
            //所以这里不能使用i, 因为i会改变
            ///解决这个问题, 可以新创建一个变量, 将i的值赋给它
            //这个新变量没有人改, 所以它就是事实上是final的变量
            System.out.println("线程" + l + "正在工作");
            try {
                //代指某些耗时任务
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(l + "结束");
            //任务结束后, 调用一下方法
            countDownLatch.countDown();
        });
        t.start();
    }
    //调用await方法, 等待所有任务全部结束, 在未结束之前, 主线程会在这里阻塞等待
    countDownLatch.await();
    System.out.println("end");
}

你可能感兴趣的:(Javaee,java,开发语言,jvm)