不加锁解决线程安全

news/2024/12/5 16:52:02/

不加锁解决线程安全

一、使用原子类(Atomic Classes)

  • 原理:

    • Java.util.concurrent.atomic 包提供了一系列原子类,如 AtomicInteger、AtomicLong、AtomicBoolean 等。这些原子类内部利用 CAS(Compare and Swap)算法来实现原子性操作。CAS 包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。在执行操作时,先比较内存位置 V 的值是否等于预期原值 A,如果相等,就将内存位置 V 的值更新为新值 B;否则,说明该内存位置的值已被其他线程修改,操作不进行更新,而是返回当前内存位置 V 的实际值。通过这种方式,原子类对变量的操作是不可分割的,要么全部完成,要么全部不完成,从而保证了数据的安全性,无需显式加锁。

  • 示例代码(以 AtomicInteger 为例)

 import java.util.concurrent.atomic.AtomicInteger;​class AtomicCounter {private AtomicInteger count = new AtomicInteger(0);​public void increment() {count.incrementAndGet();}​public int getCount() {return count.get();}}​public class AtomicExample {public static void main(String[] args) {AtomicCounter counter = new AtomicCounter();Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {counter.increment();}});​thread1.start();thread2.start();​try {thread1.join();thread2.join();} catch (Exception e) {e.printStackTrace();}​System.out.println("最终计数:" + counter.getCount());}}

在上述示例中,AtomicCounter 类使用 AtomicInteger 管理计数。通过 count.incrementAndGet () 方法实现原子性的计数增加操作,多个线程同时执行该操作时,能保证计数的正确更新,无需加锁。

二、利用 volatile 关键字结合特定编程模式

  • 原理:

    • volatile 关键字主要用于修饰变量,它保证了变量的可见性,即当一个线程修改了被 volatile 修饰的变量的值后,其他线程能立即看到这个新的值。虽然它本身不能完全解决线程安全问题(因为它不能保证原子性),但在一些特定场景下,结合合适的编程模式可以起到一定作用。

  • 示例代码(基于 volatile 的状态标记模式)

 class FlagExample {private volatile boolean flag = false;​public void setFlag() {flag = true;}​public boolean getFlag() {return flag;}}​public class VolatileExample {public static void main(String[] args) {FlagExample example = new FlagExample();Thread thread1 = new Thread(() -> {while (!example.getFlag()) {// 可进行一些等待操作,如睡眠一小段时间try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}}System.out.println("线程1检测到标志已设置");});Thread thread2 = new Thread(() -> {example.setFlag();System.out.println("线程2已设置标志");});​thread1.start();thread2.start();​try {thread1.join();thread2.join();} catch (Exception e) {e.printStackTrace();}}}

在此例中,FlagExample 类的 flag 变量被 volatile 修饰。线程 2 可通过 setFlag 方法修改 flag 值,线程 1 通过 getFlag 方法获取 flag 值。由于 flag 是 volatile 修饰的,线程 2 修改后,线程 1 能立即看到新值,从而可根据标志进行相应操作。通过这种状态标记模式,在一定程度上实现了线程间的协调,避免了复杂的锁机制。

三、采用不可变对象(Immutable Objects)

  • 原理:

    • 不可变对象是指一旦创建,其状态就不能被修改的对象。在多线程环境下,如果多个线程都只对不可变对象进行读取操作,那么就不存在线程安全问题,因为对象的状态不会发生改变。即使需要对不可变对象进行更新操作,也是通过创建一个新的不可变对象来代替原来的对象,这样可以保证在更新过程中,其他线程看到的仍然是旧的、完整的对象状态。

  • 示例代码(以创建不可变的字符串对象为例)

 class ImmutableStringExample {public static void main(String[] args) {// 创建不可变的字符串对象String str = "Hello World";// 多个线程可以对这个字符串进行读取操作,不存在线程安全问题Thread thread1 = new Thread(() -> {System.out.println("线程1读取到的字符串:" + str);});Thread thread2 = new Thread(() -> {System.out.println("线程2读取到的字符串:" + str);});​thread1.start();thread2.start();​try {thread1.join();thread2.join();} catch (Exception e) {e.printStackTrace();}}}

在上述例子中,创建了不可变的字符串对象 "Hello World",多个线程对其进行读取操作,由于字符串对象是不可变的,所以不存在线程安全问题。

四、运用线程本地变量(Thread Local Variables)

  • 原理:

    • 线程本地变量是每个线程特有的变量,它的值只对当前线程可见,其他线程无法访问。这样,每个线程都可以独立地使用自己的线程本地变量进行操作,避免了因共享变量而导致的线程安全问题。

  • 示例代码(以线程本地存储一个计数器为例)

 class ThreadLocalCounterExample {private static final ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();​public static void main(String[] args) {Thread thread1 = new Thread(() -> {threadLocalCounter.set(0);for (int i = 0; i < 1000; i++) {Integer count = threadLocalCounter.get();threadLocalCounter.set(count + 1);}System.out.println("线程1的计数器值:" + threadLocalCounter.get());});Thread thread2 = new Thread(() -> {threadLocalCounter.set(0);for (i = 0; i < 1000; i++) {Integer count = threadLocalCounter.get();threadLocalCounter.set(count + 1);}System.out.println("线程2的计数器值:" + threadLocalCounter.get());});​thread1.start();thread2.start();​try {thread1.join();thread2.join();} catch (Exception e) {e.printStackTrace();}}}

在这个例子中,通过 ThreadLocal 创建了线程本地变量 threadLocalCounter,每个线程在启动时将其初始化为 0,然后在循环中不断更新自己的计数器值。由于每个线程的计数器值存储在自己的线程本地变量中,所以不存在因共享变量而导致的线程安全问题。

五、借助函数式编程特性(Java 8 及以上版本适用)

  • 原理:

    • Java 8 引入了函数式编程特性,其中的 Stream API 和 Lambda 表达式等可以在一定程度上帮助解决线程安全问题。例如,在对集合进行操作时,可以使用 Stream API 的并行流(parallelStream)功能,它会自动将集合中的元素分配到不同的线程中进行处理,并且在处理过程中会尽量保证数据的安全性,通常是通过内部机制(如使用原子类等)来实现的。

  • 示例代码(以对集合进行并行求和为例)

 import java.util.ArrayList;import java.util.List;​class FunctionalProgrammingExample {private List<Integer> numbers = new ArrayList<>();​public int parallelSum() {// 初始化集合for (int i = 1; i <= 1000; i++) {numbers.add(i);}// 使用并行流对集合进行求和return numbers.stream().parallel().mapToInt(Integer::intValue).sum();}}

在上述示例中,通过使用 Java 8 的 Stream API 的并行流功能,对包含 1000 个整数的集合进行求和操作。并行流会自动将集合中的元素分配到不同的线程中进行处理,并且会保证数据的安全性,无需额外加锁就可实现对集合元素的并行处理,避免了线程安全问题。

这些方法在不同的场景下可以有效地解决线程安全问题,实际应用中可根据具体需求和场景选择合适的方法。


http://www.ppmy.cn/news/1546566.html

相关文章

Linux基础之病毒编写

声明&#xff1a;学习视频来自b站up主 泷羽sec&#xff0c;如涉及侵权马上删除文章 声明&#xff1a;本文主要用作技术分享&#xff0c;所有内容仅供参考。任何使用或依赖于本文信息所造成的法律后果均与本人无关。请读者自行判断风险&#xff0c;并遵循相关法律法规。 感谢泷…

MongoDB——服务端连接及查询

一、登录 登录命令&#xff1a;mongo localhost/db -u username -p。其中db为要登录的db&#xff0c;username为账户&#xff0c;然后输入密码回车&#xff1a; 登录成功 二、数据库操作 1、创建db&#xff1a;use testDB。 2、查看有哪些db&#xff1a;show dbs。 3、查看…

【React】默认导出和具名导出

这是 JavaScript 里两个主要用来导出值的方式&#xff1a;默认导出和具名导出。一个文件里有且仅有一个 默认导出&#xff0c;但是可以有任意多个具名导出。 1.默认导出&#xff1a; 导出语句 export default function Button() {}导入语句 import Button from ./Button.js;…

ACIS中wire与edge的区别是什么

在ACIS中&#xff0c;Wire表示没有附着在面上、连接在一起的有向边的集合&#xff0c;可以表示闭合曲线或一些理想化的对象&#xff1b;而Edge表示由两个顶点连接而成、具有方向属性的直线段或圆弧&#xff0c;用于定义曲线的拓扑结构。 • Wire&#xff08;线框&#xff09;&a…

MongoDB 详解:深入理解与探索

在当今的数据库领域&#xff0c;MongoDB 以其独特的特性和强大的功能&#xff0c;成为了众多开发者和企业的首选。本文将对 MongoDB 进行详细的介绍&#xff0c;包括其特点、应用场景、流程图以及源码分析。 一、MongoDB 概述 MongoDB 是一个基于分布式文件存储的开源数据库系…

传输协议设计与牧村摆动(Makimoto‘s Wave)

有一条活鱼和一条死鱼&#xff0c;你准备怎么做&#xff0c;你会将活鱼红烧或将死鱼清蒸吗&#xff1f;好的食材只需要最简单的烹饪&#xff0c;不好的食材才需要花活儿。 我此前的文字几乎都在阐述一个观点&#xff0c;广域网就是那条死鱼&#xff0c;数据中心则是那条活鱼。…

【报错记录】Steam迁移(移动)游戏报:移动以下应用的内容失败:XXX: 磁盘写入错误

前言 由于黑神话悟空&#xff0c;导致我的2TB的SSD系统盘快满了&#xff0c;我又买了一块4TB的SSD用来存放游戏&#xff0c;我就打算把之前C盘里的游戏移动到D盘&#xff0c;结果Steam移动游戏居然报错了&#xff0c;报的还是“磁盘写入错误”&#xff0c;如下图所示&#xff…

Unity3D UI 双击和长按

Unity3D 实现 UI 元素双击和长按功能。 UI 双击和长按 上一篇文章实现了拖拽接口&#xff0c;这篇文章来实现 UI 的双击和长按。 双击 创建脚本 UIDoubleClick.cs&#xff0c;创建一个 Image&#xff0c;并把脚本挂载到它身上。 在脚本中&#xff0c;继承 IPointerClickHa…