多线程之Callable接口、ReentrantLock、信号量 Semaphore以及CountDownLatch

简介: 多线程之Callable接口、ReentrantLock、信号量 Semaphore以及CountDownLatch

1237570d05884c5a95002b3f59ed7864.png


一、Callable接口

Callable的用法

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

代码示例: 创建线程计算 1 + 2 + 3 + ... + 1000, 使用 Callable 版本

  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;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        int ret = futureTask.get();
        System.out.println(ret);
    }


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


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


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

4.创建线程, 线程的构造方法传入 FutureTask ,此时新线程就会执行 FutureTask 内部的 Callable 的


call 方法, 完成计算,计算结果就放到了 FutureTask 对象中。


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


果。

小结

Callable 和 Runnable 都是描述一个 "任务",Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。


Callable 通常需要搭配 FutureTask 来使用,FutureTask 用来保存 Callable 的返回结果,因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定,FutureTask 就可以负责这个等待结果出来的工作。


二、ReentrantLock

ReentrantLock 的用法


ReentrantLock可重入的,和 synchronized类似, 保证线程安全。


ReentrantLock 的用法:


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


trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁


unlock(): 解锁


伪代码:

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
   // 业务逻辑
} finally {
   lock.unlock()
}


ReentrantLock 和 synchronized 的区别?(为什么有了 synchronized 还需要 juc(java.util.concurrent) 下的 lock?)


1.synchronized 是一个关键字, 是 JVM 内部实现的(可能是基于 C++ 实现的),ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现)。


2.synchronized 使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放,ReentrantLock使用起来更灵活,但是也容易遗漏 unlock。


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


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


5.ReentrantLock 有更强大的唤醒机制。synchronized 是通过 Object 的 wait / notify 实现等待-唤醒,每次唤醒的是一


个随机等待的线程。而ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程。如何选择使用哪个锁?


在锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便。


在锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等。


如果需要使用公平锁, 使用 ReentrantLock。

三、信号量 Semaphore

如何理解信号量?

信号量用来表示 "可用资源的个数",其本质上就是一个计数器。Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用。

代码示例:

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

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

3.创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源

观察程序的执行效果:当申请到4个资源后,就会进行线程等待,等待资源的释放。

public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(4);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("申请资源");
                    semaphore.acquire();
                    System.out.println("我获取到资源了");
                    Thread.sleep(1000);
                    System.out.println("我释放资源了");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 20; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }


信号量可以用在过哪些场景下?

信号量是用来表示 "可用资源的个数",其本质上就是一个计数器。使用信号量可以实现 "共享锁", 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作(acquire 方法)作为加锁, V 操作(release 方法)作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作。

线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步。

四、CountDownLatch

CountDownLatch是同时等待 N 个任务执行结束。

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


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


3.主线程中使用 latch.await(), 阻塞等待所有任务执行完毕,相当于计数器为 0 了

 public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep((long) (Math.random()*10000));
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
        latch.await();
        System.out.println("10个任务全部结束");
    }


相关文章
|
19天前
|
Java
java线程接口
Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。 Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。 通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。
36 1
|
24天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
1月前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
2月前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
37 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
31 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
38 1