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

news2025/2/24 6:44:00

文章目录

  • Callable接口
  • ReentrantLock
    • ReentrantLock 和 synchronized 的区别:
    • 如何选择使用哪个锁?
  • 信号量Semaphore
  • CountDownLatch
  • 多线程环境使用ArrayList
  • 多线程使用 哈希表
  • 相关面试题

JUC放了和多线程有关的组件

Callable接口

和Runnable一样是描述一个任务,但是有返回值,表示这个线程执行结束要得到的结果是啥

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

理解 FutureTask
想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你⼀张 “小票” . 这个小票就是FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没

public class Test {
    private static int sum = 0;

    public static void main(String[] args) throws InterruptedException {
        //创建一个线程,让这个线程实现 1+2+3+...+1000
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int result = 0;
                for (int i = 1; i <= 1000; i++) {
                    result += i;
                }
                //此处为了把result告知主线程,需要通过成员变量
                sum = result;
            }
        });
        t.start();
        t.join();

        System.out.println(sum);
    }
}

这个代码让主线程和t 线程耦合太大了
Callable就是为了降低耦合度的

• 创建⼀个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
• 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
• 把 callable 实例使用 FutureTask 包装一下.
• 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
• 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果

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

public class Test {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for(int i = 1; i <= 1000; i++) {
                    result += i;
                }
                return result;
            }
        };
        //创建线程,把callable搭载到线程内部执行
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        
        System.out.println(futureTask.get());
    }
}

ReentrantLock

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

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(): 尝试加锁,如果锁已经被占用了,直接返回失败,而不会继续等待。还可以指定等待超时时间加锁, 如果获取不到锁, 等待⼀定的时间之后就放弃加锁.
  • unlock(): 解锁

ReentrantLock 和 synchronized 的区别:

  • synchronized 是⼀个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的⼀个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入⼀个 true 开启公平锁模式

在这里插入图片描述
ReentrantLock的参数是true就是公平锁,false或者不写就是非公平锁

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
import java.util.concurrent.locks.ReentrantLock;

public class Test {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock(true);
        
        try {
            //加锁
            locker.lock();
        } finally {
            //解锁
            locker.unlock();
        }
    }
}
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待⼀段时间就放弃.

  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

• 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
• 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
• 如果需要使用公平锁, 使用 ReentrantLock

信号量Semaphore

信号量就是一个计数器,描述了可用资源的个数

围绕信号量有两个基本操作

  1. P操作:计数器-1,申请资源
  2. V操作:计数器+1,释放资源

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

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

代码示例
• 创建 Semaphore 实例, 初始化为 4, 表示有 4 个可用资源.
• acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)

import java.util.concurrent.Semaphore;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //4个可用资源
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");

        semaphore.release();
    }
}

在这里插入图片描述
总共四个可用资源,进行第五次P操作会阻塞直到其他线程执行V 操作

import java.util.concurrent.Semaphore;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //4个可用资源
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");
        semaphore.acquire();
        System.out.println("P 操作");

        semaphore.release();
    }
}

在这里插入图片描述
锁其实是特殊的信号量
如果信号量只有0 , 1两个取值,此时就称为"二元信号量",本质上就是一把锁

import java.util.concurrent.Semaphore;

public class Test {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);
        
        Thread t1 = new Thread(()-> {
            try {
                for (int i = 0; i < 50000; i++) {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        Thread t2 = new Thread(()-> {
            try {
                for (int i = 0; i < 50000; i++) {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}

在这里插入图片描述

CountDownLatch

当我们把一个任务拆分成多个的时候,可以通过这个工具类识别任务是否整体执行完毕了

IDM这种比较专业的下载工具就是多线程下载,把一个大的文件拆分成多个部分,每个线程都独立和人家服务器建立连接,分多个连接进行下载,等所有线程下载完毕之后,再对结果进行合并。
这时候就需要识别出所有线程是否都执行完毕了,此处就可以使用CountDownLatch

代码示例

  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了
import java.util.concurrent.CountDownLatch;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);//有10个线程
        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread t = new Thread(()-> {
                try {
                    //假设这里进行一些"下载"这样的耗时操作
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
                System.out.println("线程结束" + id);
                latch.countDown();
            });
            t.start();
        }
        //通过 await 等待所有的线程调用countDown
        //await 会阻塞等待到countDown调用的次数和构造方法指定的次数一致的时候,await才会返回
        latch.await();
        System.out.println("所有线程结束");
    }
}

在这里插入图片描述
await 不仅仅能替代 join,还可以判断任务是否全部完成

多线程环境使用ArrayList

Vector每个方法都有synchronized加锁
如果ArrayList这样没加锁的集合类想达到类似于Vector的效果就可以用Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的⼀个基于 synchronized 进行线程同步的 List.

CopyOnWriteArrayList(写时拷贝)也是一种解决线程安全问题的做法
假设有个数组有1,2,3,4这四个数据
多个线程读取,一个线程将2改为200,这样就有可能读取不到2,这是bug

我们就可以用原来的数组去读,新建一个数组去修改,写完之后用新的数组的引用代替旧的数组的引用(引用赋值的操作是原子的)

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。CopyOnWrite容器也是⼀种读写分离的思想,读和写不同的容器。

  • 优点:

在读多写少的场景下, 性能很高, 不需要加锁竞争.

  • 缺点:
  1. 占用内存较多.
  2. 新写的数据不能被第⼀时间读取到.

上述过程,没有任何加锁和阻塞等待,也能确保读线程不会读出"错误的数据"

有些服务器程序,需要更新配置文件/数据文件,就可以采取上述策略

显卡渲染画面到显示器就是按照写时拷贝的方式,在显示上一个画面的时候,在背后用额外的空间生成下一个画面,生成完毕就用下一个画面代替上一个画面

多线程使用 哈希表

HashMap 是不带锁的
Hashtable 虽然带锁,但线程不一定更安全,只是简单的把关键方法加上了 synchronized 关键字.

  • 一个Hashtable就只有一把锁,如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突.
  • size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
  • ⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低

标准库提供了更好的代替: ConcurrentHashMap

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是是用 synchronized, 但是不是"一把全局锁", 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率。不同线程针对不同的链表进行操作是不涉及锁冲突的(不涉及修改"公共变量",也就不涉及到线程安全问题),这样大部分的操作没有锁冲突,就只是偏向锁
  • 像size方法,即使插入/删除的元素是不同链表上的元素,也会涉及到多线程修改同一个变量。引入CAS的方式来修改size,提高了效率也避免了加锁的操作
  • 优化了扩容方式: 化整为零

HashMap要在一次put的过程中完成整个扩容的过程,就会使put操作效率变得很低。 ConcurrentHashMap在扩容的时候就会搞两份空间,一份是扩容之前的空间,一份是扩容之后的空间。后续每个来操作 ConcurrentHashMap 的线程,都会把一部分数据从旧空间搬运到新空间,分多次搬运。

搬的过程中:

  • 插入操作就插入到新的空间里面
  • 删除操作就是新的旧的空间里面的都要删除掉
  • 查找就是新的旧的空间都要查找

相关面试题

  1. 线程同步的方式有哪些?

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

  1. 为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例,

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待⼀段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入⼀个 true 开启公平锁模式.
  • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程.ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
  1. 信号量听说过么?之前都用在过哪些场景下?

信号量,用来表示 “可用资源的个数”. 本质上就是⼀个计数器.

使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.

  1. 谈谈 volatile关键字的用法?

volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第⼀时间读取到最新的值.

  1. Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.

  1. 在多线程下,如果对⼀个数进行叠加,该怎么做?
  • 使用 synchronized / ReentrantLock 加锁
  • 使用 AtomInteger 原子操作.

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

查询某个县区数据,没有的数据用0补充。

加油&#xff0c;新时代打工人&#xff01; 思路&#xff1a; 先查出有数据的县区&#xff0c;用县区编码判断&#xff0c;不存在县区里的数据。然后&#xff0c;用union all进行两个SQL拼接起来。 SELECTt.regionCode,t.regionName,t.testNum,t.sampleNum,t.squareNum,t.crop…

springboot+vue+mybatis图书馆借阅管理系统+PPT+论文+讲解+售后

21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存储达到…

三分钟内了解卷轴模式

在数字化时代的浪潮中&#xff0c;卷轴商业模式巧妙地将积分体系、互动任务、社交裂变、虚拟经济体系以及个性化成长路径等多元要素融为一体。 积分体系&#xff1a;激发参与动力的源泉 卷轴商业模式的核心在于其精心构建的积分系统。新用户踏入平台&#xff0c;即获赠一笔启…

基于自编码器的时间序列异常检测方法(以传感器数据为例,MATLAB R2021b)

尽管近年来研究者对自编码器及其改进算法进行了深入研究&#xff0c;但现阶段仍存在以下问题亟须解决。 1) 无监督学习模式对特征提取能力的限制与有监督学习相比&#xff0c;无监督学习模式摆脱了对样本标签的依赖、避免了人工标注的困难&#xff0c;但也因此失去了样本标签的…

LLM - 循环神经网络(RNN)

1. RNN的关键点&#xff1a;即在处理序列数据时会有顺序的记忆。比如&#xff0c;RNN在处理一个字符串时&#xff0c;在对字母表顺序有记忆的前提下&#xff0c;处理这个字符串会更容易。就像人一样&#xff0c;读取下面第一个字符串会更容易&#xff0c;因为人对字母出现的顺序…

一站式解决方案:用ChatGPT和AutoGPT组建你的个人写作团队

ChatGPT 在 AI 内容创作领域带来了巨大的飞跃&#xff0c;然而它在撰写完整文章时偶尔会陷入废话和奇怪主题。作为专业作家、AI专家及OpenAI Beta测试人员&#xff0c;我一直探索AI写作。虽然ChatGPT表现出色&#xff0c;但有时难以达到创造高质量文章的标准。 最近&#xff0…

EtherCAT转Profinet网关配置说明第二讲:上位机软件配置

EtherCAT协议转Profinet协议网关模块&#xff08;XD-ECPNS20&#xff09;&#xff0c;不仅可以实现数据之间的通信&#xff0c;还可以实现不同系统之间的数据共享。EtherCAT协议转Profinet协议网关模块&#xff08;XD-ECPNS20&#xff09;具有高速传输的特点&#xff0c;因此通…

githup开了代理push不上去

你们好&#xff0c;我是金金金。 场景 git push出错 解决 cmd查看 git config --global http.proxy git config --global https.proxy 如果什么都没有&#xff0c;代表没设置全局代理&#xff0c;此时如果你开了代理&#xff0c;则执行如下&#xff0c;设置代理 git con…

Github:git提交代码到github

创建 GitHub 仓库 a. 登录到您的 GitHub 账户。 b. 点击右上角的 "" 图标&#xff0c;选择 "New repository"。 c. 填写仓库名称&#xff08;例如 "Mitemer"&#xff09;。 d. 添加项目描述&#xff08;可选&#xff09;。 e. 选择仓库为 &…

微信小程序的轻松音乐-计算机毕业设计源码48092

目 录 摘要 1 绪论 1.1研究背景与意义 1.2研究现状 1.3论文结构与章节安排 2 基于微信小程序的轻松音乐系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.3 系统用例分析 2.4 系统…

排序——数据结构与算法 总结8

目录 8.1 排序相关概念 8.2 插入排序 8.2.1 直接插入排序&#xff1a; 8.2.2 折半插入排序&#xff1a; 8.2.3 希尔排序&#xff1a; 8.3 交换排序 8.3.1 冒泡排序&#xff1a; 8.3.2 快速排序&#xff1a; 8.4 选择排序 8.4.1 简单选择排序 8.4.2 堆排序 8.5 归并…

C++--partition库函数

介绍 在C中&#xff0c;partition函数通常是指STL&#xff08;Standard Template Library&#xff09;中的std::partition算法&#xff0c;它用于对一个序列进行分区操作。具体来说&#xff0c;std::partition接受一个范围和一个谓词&#xff08;predicate&#xff09;作为参数…

策略为王股票软件源代码-----如何修改为自己软件73------------主界面右下角,大盘指数,时间显示 ,

IDS_MAINFRAME_SHINDEXTIP "沪:%2.f %+.2f %.2f亿" IDS_MAINFRAME_SZINDEXTIP "深:%2.f %+.2f %.2f亿" 主界面右下角,大盘指数,时间显示 , if( TIMER_TIME == nIDEvent ) { CSPTime time = CSPTime::GetCurrentTime(); …

去除gif动图背景的工具网站

选择视频或GIF - 取消屏幕 (unscreen.com)https://www.unscreen.com/upload

【论文解读】LivePortrait:具有拼接和重定向控制的高效肖像动画

&#x1f4dc; 文献卡 英文题目: LivePortrait: Efficient Portrait Animation with Stitching and Retargeting Control;作者: Jianzhu Guo; Dingyun Zhang; Xiaoqiang Liu; Zhizhou Zhong; Yuan Zhang; Pengfei Wan; Di ZhangDOI: 10.48550/arXiv.2407.03168摘要翻译: *旨在…

以腾讯为例,手把手教你搭建产品帮助中心

一个精心设计的产品帮助中心对于提高用户满意度和体验至关重要。腾讯&#xff0c;作为全球领先的互联网企业&#xff0c;通过其多样化的产品线&#xff08;包括微信、QQ、腾讯游戏、腾讯视频等&#xff09;吸引了亿万用户。下面将以腾讯为例&#xff0c;向您展示如何搭建一个高…

mysql修改字符集为UTF-8

启动 mysql 服务 systemctl start mysqld 登录 mysql mysql -uroot -p 查询 mysql 字符集 ## 在 mysql 命令行下查询 mysql 状态 mysql>status; 退出 mysql 并关闭 mysql ## 退出 mysql mysql>exit; ## 关闭 mysql systemctl stop mysqld 编辑 my.cnf 配置文…

Golang语法规范和风格指南(一)——简单指南

1. 前引 一个语言的规范的学习是重要的&#xff0c;直接关系到你的代码是否易于维护和理解&#xff0c;同时学习好对应的语言规范可以在前期学习阶段有效规避该语言语法和未知编程风格的冲突。 这里是 Google 提供的规范&#xff0c;有助于大家在开始学习阶段对 Golang 进行一…

【IT领域新生必看】深入了解Java中的静态成员变量和实例成员变量:初学者的全方位指南

文章目录 引言什么是静态成员变量&#xff1f;定义和使用静态成员变量示例&#xff1a; 静态成员变量的特点示例&#xff1a; 什么是实例成员变量&#xff1f;定义和使用实例成员变量示例&#xff1a; 实例成员变量的特点示例&#xff1a; 静态成员变量与实例成员变量的区别作用…

lodash-es 基本使用

中文文档&#xff1a;https://www.lodashjs.com/ cloneDeep方法文档&#xff1a;https://www.lodashjs.com/docs/lodash.cloneDeep#_clonedeepvalue 参考掘金文章&#xff1a;https://juejin.cn/post/7354940462061715497 安装&#xff1a; pnpm install lodash-esnpm地址&a…