自学内容网 自学内容网

【多线程】JUC中的常见组件

🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈

在这里插入图片描述
JUC 是 java.util.concurrent 这个包的简写,里面存放了 Java 并发框架为协调并发任务所提供的一些工具,本文将介绍 JUC 中常见的工具类!

1. Callable 接口

1.1 Callable 接口是什么

Callable 的用法非常类似于 Runnable 接口用法,Runnable 是描述了一个任务,一个线程需要做什么,Runnable 通过 run 方法描述,返回类型为 void,但是在实际情况中,很多时候希望任务是要有返回值,即有一个具体的结果产出

Callable 接口是一个函数式接口, 只有一个方法 call()

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

在这里插入图片描述
其中,类型参数 V 是 call() 方法返回值的类型,如 Callable 这样写,表示的是一个最终返回 Integer 对象

下面具体通过一个例子理解

1.2 Callable 的用法

描述:使用 Callable 创建线程,用这个线程计算 1+2+3+…+1000

1)创建一个匿名内部类,并实现 Callable 接口,Callable 是带有泛型参数的,其泛型参数就表示返回值的类型,这个案例,泛型参数使用 Integer,重写 Callable 的 call() 方法,完成 1 到 1000 的累加过程,并返回其结果,如下:

      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 是一个函数式接口,也也可以使用 lambda 表达式的方式来定义,如下:

      Callable<Integer> callable = ()->{
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
        };

2) 创建好 callable 任务后,需要一个线程来启动(注意!!! 这里并不是在构造方法中直接将 callable 传入),这里需要先通过 FutureTask 包装一下,再将 FutureTask 传入 Thread 的构造方法中

创建线程 t,在线程 t 的构造方法中传入 FutureTask,此时 t 就会执行 FutureTask 内部的 Callable 的 call() 方法,完成计算,最终的计算结果会存在 futureTask 中

   FutureTask<Integer> futureTask = new FutureTask<>(callable);
   Thread t = new Thread(futureTask);

FutureTask
执行 Callable 的一种方法是使用 FutureTask,它实现了 Future 和 Runnable 接口,因此,可以构造一个线程来运行这个任务,在 Thread 构造方法里传入 FutureTask 对象,此时线程会执行 FutureTask 内部的 Callable 的 call() 方法,完成计算,最终计算的结果就会存在 futureTask 里,通过 get() 获取到值

Future
Future 接口代表一个异步计算的结果,可以在后台线程中进行计算,而不会阻塞当前线程,其中的 get() 方法可以获取这个结果,并且它的调用会阻塞,直到计算完成,类似于 join() 方法的效果,如果运行该计算的线程被中断, get() 方法将抛出 InterruptedExpection,如果计算已完成,可以通过 get 方法立即返回结果

// 计算结果
futureTask.get()

举一个栗子来理解 FutureTask:

小万去吃饭,排队买饭,拿到了小票,当餐点好了后,后厨就开始做这个饭了,同时前台也会给小万一张小票,做好了凭这个小票去领取,这个小票就相当于是这里的 FutureTask,这也意味着后面如果小万想知道自己的饭做好没,凭着这个小票去查看是否做好(不得不说,一切都源于生活呀~)

下面用图解说明:
在这里插入图片描述

完整代码如下:

public class Demo3 {
    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();

        System.out.println(futureTask.get());
    }
}

运行结果如下:

在这里插入图片描述

1.3 Runnable 和 Callable 接口的区别

Runnable 核心代码:

public interface Runnable {
void run();
}

Callable 核心代码:

public interface Callable {
V call() throws Exception;
}
  • 返回值:Runnable 没有返回值, Callable 有返回值,其返回值通过 FutureTask 对象获取
  • 异常处理:Runnable 的 run() 方法不能抛出检查异常,而 Callable 的 call() 方法可以抛出异常,因此,Callable 允许异常向上抛,可以在外部捕获异常

✨ 创建线程的方式

可以看到,上述又有一种创建线程的方式:Callable 的方式

具体创建线程的方式,可回顾前期内容:☔️ 创建线程到底是多少种方法?

2. ReentrantLock 类

2.1 ReentrantLock 是什么

synchronized 是关键字,基于代码块的方式来控制加锁解锁的,ReentrantLock 则是提供了 lock 和 unlock 独立的方法,来进行加锁和解锁。

有些小伙伴就有疑问了,手动 lock 和 unlock 很容易造成加了锁但忘记 unlock ,忘记释放锁的情况,这个角度来看, ReentrantLock 似乎不实用呀~ 其实不是,虽然在大部分情况下,使用 synchronized 就足够使用了,但是 ReentrantLock 也是一个重要的补充!

可以从三个方面来看:

  1. synchronized 只是加锁和解锁,加锁的时候,如果发现锁被占用了,只能阻塞等待,而 ReentrantLock 还提供一个 tryLock 方法,如果加锁成功,没啥特殊的,如果加锁失败,不会阻塞,直接返回 false,这会让程序猿更加灵活决定接下来应该怎么做~
  2. synchronized 是一个非公平锁(简单回顾以下,非公平锁就是概率相等,不遵循先来后到), ReentrantLock 提供了公平和非公平两种工作模式,默认是非公平锁,如果在构造方法中传入 true 就是开启了公平锁
  3. synchronized 搭配 wait 和 notify 使用,进行线程等待,如果多个线程 wait 同一个对象,notify 是随机唤醒一个,ReentrantLock 则是搭配 Condition 这个类,这个类也能起到等待通知的作用,可以功能更加强大
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

2.2 ReentrantLock 的用法

  • lock():加锁,如果获取不到锁,就死等
  • trylock(超时时间):加锁,如果获取不到锁,等待一定时间之后就放弃加锁,返回 false
  • unlock():解锁
  • lockInterruptibly():能够中断等待锁的线程机制
public class Demo1 {
    public static void main(String[] args) {
        // ReentrantLock 加锁
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try {
            //业务代码
        } finally {
            lock.unlock();
        }
    }
}

2.3 ReentrantLock 的实现原理

ReentrantLock 是 Lock 接口的一个实现,它提供了与 synchronized 关键字类似的锁功能,是 synchronized 关键字的补充, 它更加灵活~

下面通过代码具体理解:

class Demo2 {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();  // 获取锁
        try {
            count++;
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    public int getCount() {
        return count;
    }
}
  • increment 方法先上锁,尝试增加 count 的值,在完成操作后释放锁,保证 count++ 的操作是线程安全的
  • ReentrantLock 是可重入的独占锁,可重入表示当前线程获取锁后再次获取不会被阻塞,独占表示只能有一个线程获取该锁,其它想获取该锁的线程会被阻塞
  • ReentrantLock 内部会通过一个计数器来跟踪锁的持有次数,当线程调用 lock() 方法获取锁的时候,锁没有被其它线程占用,则获取到锁,如果被其它线程持有,当前线程根据锁的公平性( ReentrantLock可以是公平锁也可以是非公平锁),可能会被加入等待队列中
  • 线程首次获取锁,计数器值变为 1,如果同一线程再次获取锁,计数器增加+1,每释放一次锁,计数器-1
  • 当线程调用 unlock() 方法的时候,ReentrantLock 会将持有的锁计数-1,当计数达到 0 ,则释放锁,并唤醒等待队列中的线程来竞争锁

具体流程图如下:

在这里插入图片描述

2.4 ReentrantLock 如何实现公平锁/非公平锁

1)ReentrantLock 默认构造方法是非公平锁 NonfairSync
2)ReentrantLock 在构造方法中传入true创建 公平锁 FairSync

    ReentrantLock lock = new ReentrantLock(true);

    // true:公平锁,false:非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

(NonfairSync 和 FairSync 两个类都是继承Sync,Sync 也是继承自 AQS)

补充:AQS 是什么?
AQS定义:是一套多线程访问共享资源的同步器框架,是 Java 并发包 java.util.concurrent的核心框架类,如 ReentrantLock、Semaphore、CountDownLatch 等同步类都依赖它。AQS 通过一个 int 类型的 volatile 变量来表示同步状态,并通过内置的 FIFO 队列来实现线程的阻塞和唤醒,这个 FIFO 队列就是 CLH 队列的变体,它是一个虚拟的双向队列,不存在队列实例,仅存在结点之间的关联关系
AQS思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,则需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制就是基于 CLH 队列来实现的,它将暂时获取不到锁的线程封装成一个节点加入到队列中,等待锁的分配。

2.5 synchronized 和 ReentrantLock 的区别

synchronized 和 ReentrantLock 之间的联系是,它们都可以用来实现同步~

  • 类型:synchronized 是一个关键字,是 JVM 内部实现的,大概率是基于 C++ 实现,而 ReentrantLock 是标准库的一个类,是在 JVM 外实现的,是 Lock 接口的一个实现,基于 Java 实现的;
  • 使用方式:synchronized 可以直接在方法上加锁,也可以在代码块上加锁,而 ReentrantLock 必须手动声明来加锁和释放锁
  • 是否需要手动释放锁:synchronized 使用时不需要手动释放锁,ReentrantLock 使用时需要手动释放,使用起来更灵活,但是也容易忘记写 unlock,导致忘记释放锁;
  • 申请锁后的处理方式:synchronized 在申请锁失败时会死等,ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃
  • 锁类型:synchronized 是非公平锁, ReentrantLock 默认是非公平锁,但可以通过构造方法传入一个 true 开启公平锁模式

2.6 并发量大的情况下,使用 synchronized 还是 ReentrantLock?

在并发量特别高的情况下,ReentrantLock 的性能可能要优于 synchronized

上述总结了 synchronized 和 ReentrantLock 的区别,基于以上特性,可以分析一下原因:

  • ReentrantLock 提供了超时和公平锁等特性,更加灵活,更好应对复杂的并发场景
  • ReentrantLock 允许更细粒度的锁控制,可以有效减少锁竞争,减少开销
  • ReentrantLock 支持条件变量 Condition,可以实现比 synchronized 更加复杂的线程间通信机制

💛💛💛本期内容回顾💛💛💛

在这里插入图片描述

✨✨✨本期内容到此结束啦~


原文地址:https://blog.csdn.net/m0_61814277/article/details/143447879

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!