Java多线程(04)—— 保证线程安全的方法与线程安全的集合类

一、CAS 与原子类

1. CAS

CAS(compare and swap),是一条 cpu 指令,其含义为:CAS(M, A, B);  M 表示内存,A 和 B 分别表示一个寄存器;如果 M 的值和 A 的值相同,则把 M 和 B 的值交换,如果不同,则无事发生; 因为单条 cpu 指令本身就是原子的,因此可以基于 CAS 指令,不进行加锁,来编写线程安全代码;

CAS 指令操作经过操作系统,JVM 的层层封装,最后 Java 标准库,提供了一些工具类,其中最主要工具类就是 原子类,由于原子类内部用的是 CAS 实现,所以性能要比加锁实现高很多;在 java.util.concurrent.atomic 包下;

86443a2ba3384b66945c213492823769.png

2. AtomicInteger

其中常用的类 AtomicInteger:该类是对 int 的 CAS 实现,该类的常用方法如下:

AtomicInteger(int initialValue):  构造方法,创建一个值为 initialValue 的 AtomicInteger 对象;

count.getAndIncrement(): 相当于 count++,先返回 count,并将 count + 1;

count.incrementAndGet(): 相当于 ++count,将 count + 1,再返回 count;

count.getAndAdd(int delta): 先返回 count,再将 count + delta;

count.addAndGet(int delta): 先将 count + delta,再返回 count;

    public static void main(String[] args) {
        AtomicInteger count = new AtomicInteger(0);
        System.out.println(count.getAndAdd(2));
        System.out.println(count.addAndGet(2));
    }

193a0bd6f2c44520a9cf474da484c6f8.png

在多线程情况下使用原子类的变量,不会出现线程安全问题,例如:

public class Demo6 {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 20000; i++) {
                count.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 20000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

9c94d39b6dd54d2cb45e1062f81c0d43.png

以下是对 AtomicInteger 中 getAndIncrement 方法的 CAS 操作的伪代码实现,
 
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
CAS 操作的实现
 
假设两个线程同时调用 getAndIncrement 方法:
 
1)两个线程都读取 value 的值到 oldValue中 (oldValue 是栈上的⼀个局部变量,每个线程有自己的栈)
 
2)线程 1 先执行 CAS 操作,由于 oldValue 和 value 的值相同,会直接对 value 赋值;由于 CAS是直接读写内存的,并且 CAS 的读内存,比较,写内存操作是⼀条硬件指令,是原子的,故此时线程 2 无法穿插;
 
3)当线程 2 再执行 CAS 操作,第⼀次 CAS 的时候发现 oldValue 和 value 不相等,就不能进行赋值,需要进入循环,在循环里重新读取新的 value 的值赋给 oldValue;
 
综上,CAS 通过 "值没有发生改变" 来作为 "没有其他线程穿插执行的" 的判定依据;这样就存在了一个问题;即 ABA 问题:

3. ABA 问题

假设存在两个线程 t1 和 t2,有⼀个共享变量 num,初始值为 A,线程 t1 想使⽤ CAS 把 num 值从 A 改成 Z,就需要进行这两个操作:1)先读取 num 的值,并记录到 oldNum 变量中,2)使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z;但是在 t1 执行这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B,又从 B 改成了 A;此时 t1 线程无法区分当前这个变量始终是 A,还是经历了⼀个变化过程,这就是 ABA 问题;

ABA 引发的 bug:假设 巧巧 有 200 块钱存款,巧巧 想从 ATM 中取 100 块钱,取款机创建了两个线程,并发的来执行 -100 操作;于是

1)存款 200,线程1 获取到当前存款值为 200,期望更新为 100;线程2 获取到当前存款值为200,期望更新为 100;

2)线程1 执行扣款成功,存款被改成 100,线程2 阻塞等待中;

正常的过程:

3)轮到线程2 执行时,发现当前存款为 100,和之前读到的 200 不相同,执行失败;

发生异常的过程:

3)此时 巧巧 的朋友给她转账了 100 元,此时账户金额又变为 200;

4)轮到线程2 执行时,发现当前存款为 200,和之前读到的 200 相同,再次执行扣款操作;

ABA 问题的解决方案:

给要修改的值引入版本号,在 CAS 比较当前值和旧值的同时,也要比较版本号是否符合预期;

二、信号量

信号量,用来表示 "可用资源的个数",本质上就是⼀个计数器,锁就是可用资源个数为 1 的信号量,加锁(申请资源)对应 P 操作,解锁(释放资源)对应 V 操作;

Java 标准库提供了 Semaphore 类对信号量进行实现,Semaphore 的 P V 操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用;

常用方法:

Semaphore(int permits):构造方法:创建一个可用资源为 permits 的信号量对象;

acquire():申请资源;

release():释放资源;

可以借助信号量实现类似于锁的效果,代码示例:

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

7428baa26f36494ba8e8b6cc6018706f.png

三、ReentrantLock 类

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

该类常用的方法:

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

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

unlock():解锁;

ReentrantLock 和 synchronized 的区别:

1)synchronized 是⼀个关键字,是 JVM 内部实现的,ReentrantLock 是 Java 标准库的⼀个类,在 JVM 外实现的

2)synchronized 使用时不需要手动释放锁,而ReentrantLock 在使用时需要通过 unlock 手动释放锁,使用起来更灵活,但是也容易忘记释放锁(最好通过 try finally 的方式加锁并释放);

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

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

91446bdc59f4462da5fbd1814406dced.png

5)synchronized 是通过 Object 类的 wait / notify 实现等待唤醒,每次唤醒的是⼀个随机等待的线程,ReentrantLock 搭配 Condition 接口实现等待唤醒,可以更精确控制唤醒某个指定的线程;

四、CountDownLatch 类

该类可以判定多线程任务是否全部都执行完成;

常用方法:

CountDownLatch(int count):构造方法,count 表示任务的数量;

await():调用该方法的线程会阻塞,等待其他线程全部执行完任务之后,该线程才会继续执行

countDown():用于告诉 countDownLatch 对象,当前任务执行完毕;

代码示例:

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            int id = i;
            Thread t = new Thread(() -> {
                Random random = new Random();
                int time = random.nextInt(6) * 300;
                System.out.println("线程 " + id + "开始执行");
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程 " + id + "执行结束");
                countDownLatch.countDown();
            });
            t.start();
        }
        countDownLatch.await();
        System.out.println("所有线程执行完毕");
    }

以上代码创建了一个可以包含 5 个任务的 CountDownLatch 对象,并创建了 5 个线程,每个线程执行完都会通知该 CountDownLatch 对象,并在主线程中等待所有线程执行完毕;

e8fd45831ad34a31861563cd75587aef.png

五、线程安全的集合类

1. Vector,Stack,Hashtable

这三个集合类本身就是线程安全的,其内部都是通过对类中的整个方法加上 synchronized 实现的,但是效率都太低了;

2. 通过 Collections 工具类提供的方法;

e7c61dc058df4f3591eaf8d0819b6bc9.png

synchronizedList(List<T> list):将指定的 list 变为线程安全的并返回;

synchronizedSet(Set<T> set):将指定的 set 变为线程安全的并返回;

synchronizedMap(Map<K,V> map):将指定的 map 对象变为线程安全的并返回; 

3. 使用 CopyOnWrite 容器

CopyOnWriteArrayList():构造方法,返回一个 CopyOnWriteArrayList 对象;

CopyOnWriteArraySet():构造方法,返回一个 CopyOnWriteArraySet 对象;

CopyOnWrite 容器是写时复制的容器当我们往⼀个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 copy,复制出⼀个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器

例如两个线程使用同一个 CopyOnWriteArrayList 对象,两个线程可同时读,但如果有一个线程要修改,就把该对象复制一个副本,对副本进行修改,同时,不影响另一个线程继续读原来的数据,在修改完后,让原来的对象的引用指向修改后的副本;

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

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

缺点:1. 占用内存较多,不适合存储大量数据;

           2. 新写的数据不能被第⼀时间读取到;

4. BlockingQueue

多线程环境下使用队列,可以借助 BlockingQueue 接口下的实现子类;

e55912140e4640df87b089151625a577.png

1)ArrayBlockingQueue 基于数组实现的阻塞队列;

2)LinkedBlockingQueue 基于链表实现的阻塞队列;

3)PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列;

4)TransferQueue 最多只包含⼀个元素的阻塞队列;

5. ConcurrentHashMap

多线程环境下使用哈希表可以使用 Hashtable(但是效率很慢)或者 ConcurrentHashMap;

因为 Hashtable 只是简单的把关键方法加上了 synchronized 关键字,这相当于直接针对 Hashtable 对象本身加锁;此时如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突,size 属性也是通过 synchronized 来控制同步,也是比较慢的,并且⼀旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率会非常低;

而 ConcurrentHashMap 相比于 Hashtable 的优点如下:

1)对读操作没有加锁,但针对读操作使用了 volatile 保证从内存读取结果),只对写操作进行加锁,加锁的方式仍然是使用 synchronized,但不是锁住整个对象,而是 "锁住每个桶" (用每个链表的头结点作为锁对象),只有两个线程访问的是同一个链表上的数据时才会发生锁冲突,这就大大的降低了锁冲突的概率;

2)充分利用 CAS 特性,比如 size 属性通过 CAS 来更新,避免出现重量级锁的情况;

3)在扩容时,每个来操作 ConcurrentHashMap 的线程,都会参与搬家的过程,每个线程都负责搬运一小部分元素,搬完最后⼀个元素再把旧的数组删掉,并且在这个搬运期间,插入只会往新数组增加,而查找需要同时查新数组和旧的数组,保证了数据的准确性;

 

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

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

相关文章

我成功创建了一个Electron应用程序

1.创建electron项目命令&#xff1a; yarn create quick-start/electron electron-memo 2选择&#xff1a;√ Select a framework: vue √ Add TypeScript? ... No √ Add Electron updater plugin? ... Yes √ Enable Electron download mirror proxy? ... Yes 3.命令&a…

渲染100为什么是高性价比网渲平台?渲染100邀请码1a12

市面上主流的网渲平台有很多&#xff0c;如渲染100、瑞云、炫云、渲云等&#xff0c;这些平台各有特色和优势&#xff0c;也都声称自己性价比高&#xff0c;以渲染100为例&#xff0c;我们来介绍下它的优势有哪些。 1、渲染100对新用户很友好&#xff0c;注册填邀请码1a12有3…

09.责任链模式

09. 责任链模式 什么是责任链设计模式&#xff1f; 责任链设计模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为设计模式&#xff0c;它允许将请求沿着处理者对象组成的链进行传递&#xff0c;直到有一个处理者对象能够处理该请求为止。这种模式的目的…

go语言linux安装

下载&#xff1a;https://go.dev/dl/ 命令行使用 wget https://dl.google.com/go/go1.19.3.linux-amd64.tar.gz解压下载的压缩包&#xff0c;linux建议放在/opt目录下 我放在/home/ihan/go_sdk下 sudo tar -C /home/ihan/go_sdk -xzf go1.19.3.linux-amd64.tar.gz 这里的参数…

STM32作业实现(一)串口通信

目录 STM32作业设计 STM32作业实现(一)串口通信 STM32作业实现(二)串口控制led STM32作业实现(三)串口控制有源蜂鸣器 STM32作业实现(四)光敏传感器 STM32作业实现(五)温湿度传感器dht11 STM32作业实现(六)闪存保存数据 STM32作业实现(七)OLED显示数据 STM32作业实现(八)触摸按…

谷歌发布文生视频模型——Veo,可生成超过一分钟高质量1080p视频

前期我们介绍过OpenAI的文生视频大模型-Sora 模型&#xff0c;其模型一经发布&#xff0c;便得到了大家疯狂的追捧。而Google最近也发布了自己的文生视频大模型Veo&#xff0c;势必要与OpenAI进行一个正面交锋。 Veo 是Google迄今为止最强大的视频生成模型。它可以生成超过一分…

学习小心意——python创建类与对象

在python中&#xff0c;类表示具有相同属性和方法的对象的集合&#xff0c;一般而言都是先定义类再创建类的实例&#xff0c;然后再通过类的实例去访问类的属性和方法 定义类 类中可以定义为数据成员和成员函数。数据成员用于描述对象特征&#xff08;相当于看人的面貌&#…

针对大模型的上下文注入攻击

大型语言模型&#xff08;LLMs&#xff09;的开发和部署取得了显著进展。例如ChatGPT和Llama-2这样的LLMs&#xff0c;利用庞大的数据集和Transformer架构&#xff0c;能够产生连贯性、上下文准确性甚至具有创造性的文本。LLMs最初和本质上是为静态场景设计的&#xff0c;即输入…

【文档智能】符合人类阅读顺序的文档模型-LayoutReader原理及权重开源

引言 阅读顺序检测旨在捕获人类读者能够自然理解的单词序列。现有的OCR引擎通常按照从上到下、从左到右的方式排列识别到的文本行&#xff0c;但这并不适用于某些文档类型&#xff0c;如多栏模板、表格等。LayoutReader模型使用seq2seq模型捕获文本和布局信息&#xff0c;用于…

libcef.dll丢失的解决方法-多种libcef.dll亲测有效解决方法分享

libcef.dll是Chromium Embedded Framework (CEF)的核心动态链接库&#xff0c;它为开发者提供了一个将Chromium浏览器嵌入到本地桌面应用程序中的解决方案。这个库使得开发者能够利用Chromium的强大功能&#xff0c;如HTML5、CSS3、JavaScript等&#xff0c;来创建跨平台的应用…

Llama(一):Mac M1芯片运行Llama3

目录 安装Ollama for Mac 下载Llama 3模型 运行Llama3 试用Llama3 在命令行中使用Llama3 背景 本地环境&#xff1a;Mac M1,16GB内存 安装Ollama for Mac 官方地址 https://ollama.com/download/Ollama-darwin.zip 链接: 百度网盘 提取码: 8wqx 下载Llama 3模型 oll…

jmeter性能优化之tomcat配置与基础调优

一、 修改tomcat初始和最大堆内存 进入到/usr/local/tomcat7-8083/bin目录下&#xff0c;编辑catalina.sh文件&#xff0c;&#xff0c;默认堆内存是600m&#xff0c;初始堆内存和最大堆内存保持一致&#xff0c; 可以更改到本机内存的70%&#xff0c;对于Linux系统&#xff0…

《平渊》· 柒 —— 大道至简?真传一句话,假传万卷书!

《平渊》 柒 "真传一句话, 假传万卷书" 对于 "大道至简"&#xff0c;不少专家可能会说出一大堆乱七八糟的名词, 比如这样&#xff1a; 所谓 "大道" 即支撑天地运转的 "系统自动力"&#xff0c;更具体地来说&#xff0c;即是天地人以…

前端Vue小兔鲜儿电商项目实战Day07

一、会员中心 - 整体功能梳理和路由配置 1. 整体功能梳理 ①个人中心 - 个人信息和猜你喜欢数据渲染②我的订单 - 各种状态下的订单列表展示 2. 路由配置&#xff08;包括三级路由配置&#xff09; ①准备个人中心模板组件 - src/views/Member/index.vue <script setup&g…

【Leetcode 705 】设计哈希集合——数组嵌套链表(限制哈希Key)

题目 不使用任何内建的哈希表库设计一个哈希集合&#xff08;HashSet&#xff09;。 实现 MyHashSet 类&#xff1a; void add(key) 向哈希集合中插入值 key 。bool contains(key) 返回哈希集合中是否存在这个值 key 。void remove(key) 将给定值 key 从哈希集合中删除。如果…

构建智慧银行保险系统的先进技术架构

随着科技的不断发展&#xff0c;智慧银行保险系统正日益受到关注。在这个数字化时代&#xff0c;构建一个先进的技术架构对于智慧银行保险系统至关重要。本文将探讨如何构建智慧银行保险系统的先进技术架构&#xff0c;以提升服务效率、降低风险并满足客户需求。 ### 1. 智慧银…

德克萨斯大学奥斯汀分校自然语言处理硕士课程汉化版(第五周) - Transformer

Transformer 1. 注意力机制 在语言建模中&#xff0c;注意力(attention)是一个关键机制&#xff0c;用于在给定上下文中访问相关信息以进行预测。注意力机制允许模型根据输入上下文中的重要信息来加权关注不同的部分&#xff0c;并根据其重要性来决定对不同部分的关注程度。 …

短视频毫无营养:四川京之华锦信息技术公司

短视频毫无营养&#xff1a;现象背后的深度剖析 在数字时代&#xff0c;短视频以其短小精悍、易于传播的特点迅速崛起&#xff0c;成为社交媒体上的热门内容。然而&#xff0c;随着短视频的泛滥&#xff0c;关于其内容质量参差不齐、缺乏营养价值的争议也日益加剧。四川京之华…

【代码随想录训练营】【Day 37】【贪心-4】| Leetcode 840, 406, 452

【代码随想录训练营】【Day 37】【贪心-4】| Leetcode 840, 406, 452 需强化知识点 python list sort的高阶用法&#xff0c;两个key&#xff0c;另一种逆序写法python list insert的用法 题目 860. 柠檬水找零 思路&#xff1a;注意 20 块找零&#xff0c;可以找3张5块升…

jpeg压缩算法学习(1)——离散余弦变换

离散余弦变换是jpeg压缩算法的关键步骤 思想 离散余弦变换的基本原理是&#xff1a;每一组离散的数据都可以由一组不同频率的余弦波来表示。 应用于图片上就是&#xff1a;将像素值转换为不同频率的余弦函数的系数&#xff08;权重&#xff09; 像素值——>权重 一维离…