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

Java多线程与高并发专题——保障原子性

保障原子性

synchronized

可以在方法上追加synchronized关键字或者采用同步代码块的形式来保证原子性

synchronized可以让避免多线程同时操作临街资源,同一时间点,只会有一个线程正在操作临界资源

CAS

到底什么是CAS

compare and swap也就是比较和交换,他是一条CPU的并发原语。

他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。

Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。

但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。

private static AtomicInteger count = new AtomicInteger(0);

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

Doug Lea在CAS的基础上帮助我们实现了一些原子类,其中就包括现在看到的AtomicInteger,还有其他很多原子类。

CAS的缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。

CAS的问题

1、ABA问题

ABA 问题是指一个值先从 A 变为 B,再从 B 变回 A,而 CAS 操作在比较时只关注值是否与预期相同,无法察觉这种中间的变化过程。

这可能会导致一些潜在的错误。比如在某些场景下,我们不仅关心值是否最终相同,还关心值的变化过程,如果忽略了中间的变化,就可能出现不符合预期的结果。

可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。

AtomicStampeReference

AtomicStampedReference在CAS时,不但会判断原值,还会比较版本信息。(解决ABA问题)

public static void main(String[] args) {
    AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);

    String oldValue = reference.getReference();
    int oldVersion = reference.getStamp();

    boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
    System.out.println("修改1版本的:" + b);

    boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
    System.out.println("修改2版本的:" + c);
}
2、自旋时间过长问题

在 CAS 操作中,如果更新操作一直不成功,线程就会不断地进行尝试,这个不断尝试的过程就称为自旋。当竞争激烈时,如多个线程同时尝试修改同一个共享变量,会导致某个线程一直无法成功完成 CAS 操作。也就导致 CAS 自旋时间过长,会消耗大量的 CPU 资源。

通常的解决方案如下:

  • 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁
  • 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。

Lock锁

Lock锁是在JDK1.5由Doug Lea研发的,他的性能相比synchronized在JDK1.5的时期,性能好了很多,但是在JDK1.6对synchronized优化之后,性能相差就不大,但是如果涉及并发比较多时,推荐ReentrantLock锁,性能会更好。

实现方式:

private static int count;

private static ReentrantLock lock = new ReentrantLock();

public static void increment()  {
    lock.lock();
    try {
        count++;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } finally {
        lock.unlock();
    }


}

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

ReentrantLock可以直接对比synchronized,在功能上来说,都是锁。

但是ReentrantLock的功能性相比synchronized更丰富。

ReentrantLock底层是基于AQS实现的,有一个基于CAS维护的state变量来实现锁的操作。

ThreadLocal

ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据

代码实现

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

ThreadLocal实现原理

  • 每个Thread中都存储着一个成员变量,ThreadLocalMap

  • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap

  • ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。

  • 每一个现有都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取

  • ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收

拓展

Java中的四种引用类型

Java中的使用引用类型分别是强,软,弱,虚。

  • 在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
  • 其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。
  • 然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。
  • 最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

不过在开发中,我们用的更多的还是强引用

ThreadLocal内存泄漏问题

如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。

只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可。


http://www.kler.cn/a/520636.html

相关文章:

  • 书生大模型实战营2
  • 深度学习|表示学习|卷积神经网络|详细推导每一层的维度变化|14
  • CentOS/Linux Python 2.7 离线安装 Requests 库解决离线安装问题。
  • 「 机器人 」扑翼飞行器混合控制策略缺点浅谈
  • 硬件学习笔记--35 AD23的使用常规操作
  • arm-linux平台、rk3288 SDL移植
  • 【FreeRTOS 教程 五】FreeRTOS 内存管理细致讲解
  • easyexcel-导入(读取)(read)-示例及核心部件
  • 记录让cursor帮我给ruoyi-vue后台管理项目整合mybatis-plus
  • 第05章 04 VTK标量算法概述
  • 【时时三省】(C语言基础)对比一组函数
  • 如何使用 OpenSSL 检查 Linux 中的 SSL 证书
  • 解决查看服务器ESN(许可证管理)
  • HarmonyOS:MVVM模式
  • 一文大白话讲清楚webpack基本使用——16——图片压缩
  • vscode无法格式化go代码的问题
  • 第24篇:Python开发进阶:掌握Python编程中的调试技巧
  • 【Leetcode 每日一题】40. 组合总和 II
  • 股指期货的交易规则及细节详解
  • web前端4--css盒模型
  • 渗透测试技法之口令安全
  • Day35:字符串的大小写转换
  • MFC常用操作
  • vue 返回页面时刷新
  • DBO优化LSBoost回归预测matlab
  • Android各个版本存储权限适配