当前位置: 首页 > article >正文

不加锁解决线程安全

不加锁解决线程安全

一、使用原子类(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.kler.cn/a/386245.html

相关文章:

  • Mysql每日一题(行程与用户,困难※)
  • HarmonyOS NEXT应用开发实战 ( 应用的签名、打包上架,各种证书详解)
  • 软件工程笔记二—— 软件生存期模型
  • 整理iPhone空间:iphone怎么删除相簿
  • SHA-256哈希函数
  • 蓝桥杯c++算法学习【2】之搜索与查找(九宫格、穿越雷区、迷宫与陷阱、扫地机器人:::非常典型的必刷例题!!!)
  • AWS账号安全:如何防范与应对账号被盗风险
  • 【mysql相关】
  • 使用ChatGPT神速精读文献,12个高阶ChatGPT提示词指令,值得你复制使用
  • 哪些人群适合考取 PostgreSQL 数据库 PGCM 证书?
  • 【C++练习】使用海伦公式计算三角形面积
  • CDN到底是什么?
  • 《IDE 使用技巧与插件推荐》
  • 从xss到任意文件读取
  • vue组件传参的八种方式详细总结
  • qt QFile详解
  • 拓扑排序(C++类封装+数组模拟队列和邻接表)
  • 代码随想录之双指针刷题总结
  • wordpress判断page页与非page页
  • 【图论】图的C++实现代码
  • Python小白学习教程从入门到入坑------第二十八课 文件基础操作文件读写(语法进阶)
  • 【AIGC】如何通过ChatGPT轻松制作个性化GPTs应用
  • java后台生成模拟聊天截图并返回给前端
  • MySql中索引为什么用B+树,他有什么特点?时间复杂度是多少?能存多少数据?是不是只能三层?他与B-树有什么不同?还有其它的树你是是否知道?
  • AIPPT项目(提供完整API接入支持套壳)成熟产品线上运营
  • MySQL常用订单表复杂查询15例