多线程—— JUC 的常见类

目录

·前言

一、Callable 接口

1.Callable 介绍

2.代码示例

3.创建线程的方式

二、ReentrantLock 类

1.ReentrantLock 介绍

2.代码示例

3.与 synchronized 的区别

三、信号量 Semaphore 类

1.Semaphore 介绍

2.代码示例

3.保证线程安全的方式

四、CountDownLatch 类

1.CountDownLatch 介绍

2.代码示例

·结尾


·前言

        在我们前面的文章中介绍的线程池与阻塞队列,其中涉及到的 BlockingQueue 、ThreadPoolExecutor 、ThreadFactory 、TimeUnit 、RejectedExecutionHandler……这些内容都来自于 JUC 中,JUC 全称是 java.util.concurrent 在这里面放了很多进行多线程编程时需要用到的类,在本篇文章中,我会介绍在 JUC 中一些前面文章没有介绍到,然后还比较常见的类,它们的作用以及用法。 

一、Callable 接口

1.Callable 介绍

        使用 Callable 接口也可以创建一个线程,它和 Runnable 接口类似,但是 Runnable 在执行任务的时候关注的是执行过程,并不关注结果,所以 Runnable 提供的 run 方法返回值类型是 void,而 Callable 接口是要关注执行结果的,所以 Callable 提供的 call 方法,返回值类型就是当前线程执行任务得到的结果。

2.代码示例

        下面我用实现 Callable 接口与实现 Runnable 接口两种方式完成下面代码示例:用一个新的线程实现从 1+2+3+……+1000。先用实现 Runnable 创建线程的方法,代码及运行结果如下:

// 实现 Runnable 接口创建新线程计算 1+2+3+...+1000 的值
public class ThreadDemo16 {
    // sum 用来接收 Runnable 中 run 方法执行的结果
    public static int sum = 0;
    public static void main(String[] args) throws InterruptedException {
        // 创建线程,以实现 Runnable 的方法
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // result 用来接收计算 1+2+3+...+1000 的值
                int result = 0;
                for (int i = 1; i <= 1000; i++) {
                    result += i;
                }
                // 将计算结果赋给 sum
                sum = result;
            }
        });
        // 启动 t1 线程
        t1.start();
        // 等待 t1 线程执行完毕
        t1.join();
        // 打印最终结果
        System.out.println("1+2+3+....+1000 = " + sum);
    }
}

 

        通过上述结果可以看出这里的代码是没有问题的,但是上述代码的写法并不是特别的优雅,这是因为上述代码需要定义一个成员变量来接收 Runnable 中 run 方法执行的结果,试想一下,如果代码中有很多这种 run 方法需要返回一个结果,那么就要定义很多的成员变量,这会使我们代码的整体可读性降低,为了让上述代码看起来更优雅,下面我来使用实现 Callable 接口来创建一个新线程执行 1+2+3+……+1000 ,代码及运行结果如下:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 利用 Callable 接口创建线程完成 1+2+3+...+1000 的任务
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 实现 Callable 接口,这里需要指定一个泛型参数
        // 这里的泛型参数就是你要执行任务的返回值类型
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                // result 用来接收并返回 1+2+3+...+1000 的值
                int result = 0;
                for (int i = 1; i <= 1000; i++) {
                    result += i;
                }
                // 返回得到的结果
                return result;
            }
        };
        // 由于 Thread 中没有提供构造函数来传入 Callable,所以
        // 引入 FutureTask 作为 Callable 与 Thread 的粘合剂
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 创建线程 t 把 futureTask 传入
        Thread t = new Thread(futureTask);
        // 启动线程 t
        t.start();
        // 打印结果,这里的 futureTask 调用的 get 方法是带有阻塞功能的
        // 如果线程没有执行完毕, get 就会阻塞,等线程执行完了,return 结果
        // 后,就会被 get 返回回来
        System.out.println("1+2+3+...+1000 = " + futureTask.get());
    }
}

 

        如上图所示的运行结果可以看出我们这里的代码编写是没有问题的,使用实现 Callable 接口这种方法不需要引用额外的成员变量也做到将执行任务的结果获取到,这是利用 futureTask 作为一个凭据,用它来等任务执行完后去取结果(接收 call 方法的返回值)。

        其实使用 Callable 起到的一种“锦上添花”的效果,因为 Callable 能干的事情,使用 Runnable 也可以做到,只不过对于上述需要带有返回值的任务,使用 Callable 会更好一些,因为这样写出来的代码更直观,更简单,只不过我们在使用 Callable 的过程中不要忘记 FutureTask 起到的作用(在代码注释中)。 

3.创建线程的方式

        关于 Callable 接口的介绍也就这些,下面我再来把创建线程的几种方式给大家整理一遍,如下所示:

  1. 继承 Thread 类(包含使用匿名内部类的方式);
  2. 实现 Runnable 接口(包含使用匿名内部类的方式);
  3. 实现 Callable 接口;
  4. 基于 lambda 表达式;
  5. 基于线程池。

        以上这几种创建线程的方式在前面前面的文章中都有进行介绍,加上本篇文章介绍的实现 Callable 接口创建线程的方式,我一共介绍了五种,希望大家要理解这里的内容。 

二、ReentrantLock 类

1.ReentrantLock 介绍

        如下图所示:

         ReentrantLock 的中文意思就是可重入锁,它延续了操作系统中传统锁的风格,使用这个类实例出的对象有两个方法是 lock(加锁) 与 unlock(解锁),这种写法有一个问题就是容易引起加了锁之后忘记解锁,比如在 unlock 之前触发了 return 或者异常,就可能引起 unlock 执行不到了,所以正确使用 ReentrantLock 就需要把 unlock 操作放入 finally 中,让这个操作无论如何都会被执行到,只不过每次加锁都要套一层 try --- finally,比较繁琐,所以实际涉及到加操作时,使用 synchronized 的会更多一些。

2.代码示例

        介绍完 ReentrantLock 的基础加锁方式,下面我就使用 ReentrantLock 来写一个代码案例,这个代码案例就是创建两个线程,在两个线程中分别对变量 count 进行自增 5w 次的操作,要求保证线程安全,那么代码和运行结果如下所示:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        // 创建 ReentrantLock 对象,用来进行加锁操作
        ReentrantLock reentrantLock = new ReentrantLock();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                // 防止对 count 自增过程有其他线程对其进行修改,进行加锁
                // 使用 try---finally 套住,确保 unlock 方法被执行到
                try {
                    reentrantLock.lock();
                    count++;
                }finally {
                    reentrantLock.unlock();
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                // 防止对 count 自增过程有其他线程对其进行修改,进行加锁
                // 两个线程都要对 count++ 加锁,不然不产生锁冲突还是无法保证线程安全
                try {
                    reentrantLock.lock();
                    count++;
                }finally {
                    reentrantLock.unlock();
                }
            }
        });
        // 启动 t1 和 t2 线程
        t1.start();
        t2.start();
        // 等待 t1 和 t2 线程执行完毕
        t1.join();
        t2.join();
        // 打印最终结果
        System.out.println("count = " + count);
    }
}

        如运行结果所示,用 ReentrantLock 也可以保证线程安全,以后在使用 ReentrantLock 进行加锁解锁的操作就可以仿照上述代码来进行使用了。

3.与 synchronized 的区别

        ReentrantLock 是可重入锁,synchronized 也是可重入锁,那么既然有了 synchronized 为什么还要留有 ReentrantLock 呢?这就要谈到他们的区别了,有以下几点:

  1. ReentrantLock 提供了 tryLock 操作,使用 lock 直接进行加锁,如果加锁失败会进入阻塞等待,而使用 tryLock 尝试进行加锁,如果加锁失败会返回 false,不会产生阻塞,此时我们就可以有更多的操作空间,比如放弃加锁,或者先进行其他工作之后再来进行加锁,而 synchronized 没有这样的方法,使用 synchronized 进行加锁如果加锁失败就只能阻塞等待;
  2. ReentrantLock 提供了公平锁的实现,如下图 ReentrantLock 的构造方法所示:我们在使用构造方法创建 ReentrantLock 对象时可以在构造方法中指定一个参数,来表示当前创建的锁是公平锁还是非公平锁,而 synchronized 只能是非公平锁;
  3. ReentrantLock 与 synchronized 搭配的等待通知机制不同,对于synchronized ,搭配的是 wait / notify,此时唤醒操作是随机唤醒,对于 ReentrantLock ,搭配的是 Condition 类,功能要比 wait / notify 略强一点,这里可以指定唤醒的线程。

三、信号量 Semaphore 类

1.Semaphore 介绍

        信号量,是用来表示“可用资源的个数”,本质上可以看作是一个计数器,为了更直观的理解这里信号量的含义,我来举一个生活中的例子:

信号量可以看作是停车场门口的电子牌,上面显示当前剩余的车位,当有车开进停车场,电子牌上的数字就会 -1,开出来一个车,电子牌上的数字就会 +1 此时如果电子牌上的数字为 0 就无法再向停车场中停车,这时就要阻塞等待,直到有车从停车场出来了。

        在上述例子中,可以直观看出信号量表示的就是“可用资源的个数”,在这里申请一个可用资源,就会使数字 -1 这个操作也称为 P 操作,释放一个可用资源,就会使数字 +1 这个操作也称为 V 操作,如果数值为 0 再进行 P 操作,P 操作就会阻塞。

        对于信号量来说,这也是操作系统内部给我们提供的一个机制,操作系统对应的 API 被 JVM 封装了起来形成 Semaphore 类,所以我们就可以使用 Java 代码来调用这里的相关操作了。 

2.代码示例

        下面我来通过一个简单的代码来演示一下 Semaphore 的基本用法,代码及运行结果如下所示:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo1 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个信号量对象,设置初始信号量的值为 1
        // 表示当前可以资源个数为 1
        Semaphore semaphore = new Semaphore(1);
        // acquire 方法就表示 P 操作
        semaphore.acquire();
        System.out.println("执行 P 操作");
        // release 方法就代表 V 操作
        semaphore.release();
        System.out.println("执行 V 操作");
        semaphore.acquire();
        System.out.println("执行 P 操作");
        semaphore.acquire();
        System.out.println("执行 P 操作");
        semaphore.acquire();
        System.out.println("执行 P 操作");
    }
}

 

        从运行结果可以看出,由于信号量可用资源只有一个,所以在进行多次 P 操作时,第二个 P 操作就会阻塞,等待执行 V 操作来释放一个可用资源再继续运行,这种执行逻辑不知道有没有让大家想起锁,其实所谓的锁本质上也是一种特殊的信号量,锁可以认为是可用资源数为 1 的信号量,释放锁的状态信号量值就为 1,加锁状态的信号量值就为 0,对于这种非 0 即 1 的信号量就可以称为是“二元信号量”。

        下面我来使用信号量作为锁来完成两个线程对变量 count 分别进行 5w 次自增操作的代码示例,并保证代码的线程安全,代码及运行结果如下所示:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo2 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        // 创建信号量对象,设置可用资源数为 1
        Semaphore semaphore = new Semaphore(1);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                try {
                    // 申请资源,如果当前资源数为 0 就阻塞等待
                    // 否则就进行资源申请的操作,达到加锁的效果
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                // 释放当前申请的资源,达到解锁的效果
                semaphore.release();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                try {
                    // 申请资源,如果当前资源数为 0 就阻塞等待
                    // 否则就进行资源申请的操作,达到加锁的效果
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                // 释放当前申请的资源,达到解锁的效果
                semaphore.release();
            }
        });
        // 启动 t1 与 t2 线程
        t1.start();
        t2.start();
        // 等待 t1 与 t2 线程执行完毕
        t1.join();
        t2.join();
        // 打印结果
        System.out.println("count = " + count);
    }
}

        从运行结果,可以看出使用 Semaphore 也能达到加锁的效果来保证线程安全。

3.保证线程安全的方式

        介绍完 Semaphore 保证线程安全的方式就又多了一种,下面我来对前面文章与本篇文章中介绍到保证线程安全的方式做一个汇总,一共有以下几种方式:

  1. 使用 synchronized 进行加锁;
  2. 使用 ReentrantLock 进行加锁;
  3. 使用 CAS 机制(原子类);
  4. 使用 Semaphore 二元信号量来加锁。

四、CountDownLatch 类

1.CountDownLatch 介绍

        CountDownLatch 是一个针对特定场景解决问题的小工具,比如,多线程执行一个任务,把大的任务拆成几个部分,由每个线程分别执行,最典型的场景我感觉应该是“多线程下载”,这里的机制就是把一个大的文件拆成多个部分,每个线程负责下载一部分,下载完成后最终把下载结果拼接到一起,像“多线程下载”这样的场景,我们需要等最终执行完成后把所有内容拼接到一起,这个拼接操作一定是要等所有线程都执行完成,使用 CountDownLatch 这个类就可以方便的感知这个每个线程执行的情况,会比调用很多次 join 方法简单方便一些。

2.代码示例

        下面我来写一个简单的代码来模拟一下“多线程下载”的场景,代码及运行结果如下所示:

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 此处构造方法中写 10 代表有 10 个线程/任务
        CountDownLatch latch = new CountDownLatch(10);
        // 创建出 10 个线程负责下载
        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(()->{
                // 引入随机数模拟每个线程下载速度不一致
                Random random = new Random();
                int time = (random.nextInt(5) + 1) * 1000;
                System.out.println("线程 " + id + " 开始下载");
                try {
                    // 这里休眠模拟当前线程正在下载
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程 " + id + " 下载完成");
                // 2. 调用 countDown 方法告知 CountDownLatch 我下载完成了
                latch.countDown();
            });
            // 启动线程
            t.start();
        }
        // 3. 通过 await 方法来等待所有任务执行结束,也就是 countDown 方法被调用 10 次了
        latch.await();
        // 4. 此时任务就算已经下载完成了
        System.out.println("所有任务都下载完成");
    }
}

         CountDownLatch 可以用来协调多个线程的执行,而不必等待单个线程完成。上述代码如果使用 join 的方式,那就只能使用每个线程执行一个任务。

·结尾

        文章到此也就要结束了,本篇文章补充介绍了 JUC 中一些常见的类,这里就介绍了四种,其中使用实现 Callable 接口的方式又为我们创建线程增添一种方式,使用 ReentrantLock 和 Semaphore 可以进行加锁操作为我们保证线程安全增添了两种不一样的思路,使用 CountDownLatch 这样的工具类也可以让我们在特殊场景下写出更便捷的代码,如果本篇文章对你有所帮助,希望能收到你的三连支持,如果对文章介绍的内容有所疑问,欢迎在评论区进行留言,那么我们下一篇文章再见咯~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/902181.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

二、Spring的执行流程

文章目录 1. spring的初始化过程1.1 ClassPathXmlApplicationContext的构造方法1.2 refresh方法&#xff08;核心流程&#xff09;1.2.1 prepareRefresh() 方法1.2.2 obtainFreshBeanFactory() 方法1.2.3 prepareBeanFactory() 方法1.2.4 invokeBeanFactoryPostProcessors() 方…

(linux驱动学习 - 12). IIC 驱动实验

目录 一.IIC 总线驱动相关结构体与函数 1.i2c_adapter 结构体 2.i2c_algorithm 结构体 3.向系统注册设置好的 i2c_adapter 结构体 - i2c_add_adapter 4.向系统注册设置好的 i2c_adapter 结构体 - i2c_add_numbered_adapter 5.删除 I2C 适配器 - i2c_del_adapter 二.IIC 设…

【C++算法】11.滑动窗口_最大连续1的个数lll

文章目录 题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;图解 题目链接&#xff1a; 1004. 最大连续 1 的个数 III 题目描述&#xff1a; 解法 解法一&#xff1a;暴力枚举zero计数器 转化找出最长的子数组&#xff0c;0的个数不超过k个。 例如&#xf…

计算机网络——有连接传输层协议TCP

序号 序号一般不从0开始&#xff0c;这个在双方建立连接后约定一个数 这样做可以避免网络中滞留的TCP段对新的连接的干扰

Flutter状态管理

StatefulWidget按状态划分StatelessWidgetStatefulWidget 按照作用域划分组件内私有状态实现跨组件状态管理全局状态 状态组件的组成 DataTableInheritedWidget生命周期无状态组件有状态组件initState()didChangeDependencies()build()setState()didUpdateWidget()deactivate()…

Redis 集群 总结

前言 相关系列 《Redis & 目录》&#xff08;持续更新&#xff09;《Redis & 集群 & 源码》&#xff08;学习过程/多有漏误/仅作参考/不再更新&#xff09;《Redis & 集群 & 总结》&#xff08;学习总结/最新最准/持续更新&#xff09;《Redis & 集群…

二十二、Python基础语法(模块)

模块(module)&#xff1a;在python中&#xff0c;每个代码文件就是一个模块&#xff0c;在模块中定义的变量、函数、类别人都可以直接使用&#xff0c;如果想要使用别人写好的模块&#xff0c;就必须先导入别人的模块&#xff0c;模块名须满足标识符规则&#xff08;由字母、数…

【国潮来袭】华为原生鸿蒙 HarmonyOS NEXT(5.0)正式发布:鸿蒙诞生以来最大升级,碰一碰、小艺圈选重磅上线

在昨日晚间的原生鸿蒙之夜暨华为全场景新品发布会上&#xff0c;华为原生鸿蒙 HarmonyOS NEXT&#xff08;5.0&#xff09;正式发布。 华为官方透露&#xff0c;截至目前&#xff0c;鸿蒙操作系统在中国市场份额占据 Top2 的领先地位&#xff0c;拥有超过 1.1 亿 的代码行和 6…

布隆过滤器:极简存储,高效检索

引言 在海量数据的存储与检索中&#xff0c;如何在保持快速检索的同时&#xff0c;降低内存占用是个巨大的挑战。有没有一种既能快速检索又能节省内存的方案&#xff1f;布隆过滤器&#xff08;Bloom Filter&#xff09;就是这样一种数据结构。 布隆过滤器的基本原理 如果我…

Vue.js 学习总结(11)—— Vue3 Hook 函数实战总结

前言 在 Vue 3 中&#xff0c;Hook 函数是一种特殊的函数&#xff0c;用于封装可重用的逻辑和状态管理。Hook 函数允许你在 Vue 组件中提取和复用逻辑&#xff0c;而不是将所有逻辑都放在组件的选项对象中。它们可以帮助你更好地组织代码&#xff0c;提高代码的可维护性和可测…

算法题总结(十九)——图论

图论 DFS框架 void dfs(参数) { if (终止条件) {存放结果;return; }for (选择&#xff1a;本节点所连接的其他节点) {处理节点;dfs(图&#xff0c;选择的节点); // 递归回溯&#xff0c;撤销处理结果 } }深搜三部曲 确认递归函数&#xff0c;参数确认终止条件处理目前搜索节…

JAVA基础:IO流 (学习笔记)

IO流 一&#xff0c;IO流的理解 i &#xff1a; input 输入 o&#xff1a;output 输入 流&#xff1a;方式&#xff0c;传递数据的方式---------相当于生活中的“管道”&#xff0c;“车”&#xff0c;将资源从一个位置&#xff0c;传递到另一个位置 二&#xff0c;IO流的分…

从0开始深度学习(16)——暂退法(Dropout)

上一章的过拟合是由于数据不足导致的&#xff0c;但如果我们有比特征多得多的样本&#xff0c;深度神经网络也有可能过拟合 1 扰动的稳健性 经典泛化理论认为&#xff0c;为了缩小训练和测试性能之间的差距&#xff0c;应该以简单的模型为目标&#xff0c;即模型以较小的维度的…

Qt中使用线程之QConcurrent

QConcurrent可以实现并发&#xff0c;好处是我们可以不用单独写一个类了&#xff0c;直接在类里面定义任务函数&#xff0c;然后使用QtConcurrent::run在单独的线程里执行一个任务 1、定义一个任务函数 2、定义1个QFutureWatcher的对象&#xff0c;使用QFutureWatcher来监测任…

新手直播方案

简介 新手直播方案 &#xff0c;低成本方案 手机/电脑 直接直播手机软件电脑直播手机采集卡麦电脑直播多摄像机 机位多路采集卡 多路麦加电脑&#xff08;高成本方案&#xff09; 直播推流方案 需要摄像头 方案一 &#xff1a;手机 电脑同步下载 网络摄像头 软件&#xff08…

【学术论文投稿】Windows11开发指南:打造卓越应用的必备攻略

【IEEE出版南方科技大学】第十一届电气工程与自动化国际会议&#xff08;IFEEA 2024)_艾思科蓝_学术一站式服务平台 更多学术会议论文投稿请看&#xff1a;https://ais.cn/u/nuyAF3 目录 引言 一、Windows11开发环境搭建 二、Windows11关键新特性 三、Windows11设计指南 …

小程序开发实战:PDF转换为图片工具开发

目录 一、开发思路 1.1 申请微信小程序 1.2 编写后端接口 1.3 后端接口部署 1.4 微信小程序前端页面开发 1.5 运行效果 1.6 小程序部署上线 今天给大家分享小程序开发系列&#xff0c;PDF转换为图片工具的开发实战&#xff0c;感兴趣的朋友可以一起来学习一下&#xff01…

linux中级(NFS服务器)

NFS&#xff1a;用于在NNIX/Linux主机之间进行文件共享的协议 流程&#xff1a;首先服务端开启RPC服务&#xff0c;并开启111端口&#xff0c;服务器端启动NFS服务&#xff0c;并向RPC注册端口信息&#xff0c;客户端启动RPC&#xff0c;向服务器RPC服务请求NFS端口&#xff0…

anaconda 创建环境失败 解决指南

anaconda 创建环境失败 解决指南 一、问题描述 我在宿舍有一台电脑。由于我经常泡在实验室&#xff0c;所以那台电脑不是经常用&#xff0c;基本吃灰。昨天晚上突然有在那台电脑上使用Camel-AI部署多智能体协同需求&#xff0c;便戳开了电脑&#xff0c;问题也随之而来。 当…

汽车级DC-DC转换器英飞凌TLF35584

上汽荣威都在用的汽车级DC-DC转换器英飞凌TLF35584 今天平台君从IPBrain数据库中给大家带来的一款由Infineon(英飞凌)推出的一款多路输出安全电源芯片,具备高可靠性和安全性。适用于汽车电子系统中的多种应用场景,如车身控制、安全气囊、防抱死制动系统,电子稳定控制系统等。…