【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了(上)

简介: 【小家java】原子操作你还在用Synchronized?Atomic、LongAdder你真有必要了解一下了(上)

前言


写这篇博文的原因,是因为我今天在看阿里的规范手册的时候(记录在了这里:【小家java】《阿里巴巴 Java开发手册》读后感—拥抱规范,远离伤害),发现了有一句规范是这么写的:


如果是count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。


这里面提到了Atomic系列来进行原子操作。之前我在各个地方使用过AtomicInteger很多次,但一直没有做一个系统性的了解和做笔记。因此本此恰借此机会,把这块的知识点好好梳理一下, 并希望在学习的过程中解决掉问题


简单例子铺垫


废话不多说,展示代码:

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        Count count = new Count();
        // 100个线程对共享变量进行加1
        for (int i = 0; i < 100; i++) {
            service.execute(() -> count.increase());
        }
        // 等待上述的线程执行完   和三个方法的区别 这里不做概述,反正都能关闭
        service.shutdown();
        //service.shutdownNow();
        service.awaitTermination(2, TimeUnit.SECONDS);
        service.shutdown();
        System.out.println(count.getCount());
    }
    //计数类
    private static class Count {
        // 共享变量
        private Integer count = 0;
        public Integer getCount() {
            return count;
        }
        public void increase() {
            count++;
        }
    }


你们猜猜执行的结果会是多少?是100吗?


我相信稍微基础好一点的,或者说遇见过类似问题的,答案都是No吧。我执行了多次,结果是不确定的:29、69、48、99都有。。。

(备注:类似的方案,有时候可以通过volatile关键字,此处不对此关键字做过多的讨论,它是一种内存可见性方案,并不是真正意义上的锁哟)


根据结果我们得知:上面的代码是线程不安全的!如果线程安全的代码,多次执行的结果是一致的!

原因分析


什么上述的结果不确定呢?我们可以发现问题所在:**count++并不是原子操作。**因为count++需要经过读取-修改-写入三个步骤。举个例子还原一下真相:


1.如果某一个时刻:线程A读到count的值是10,线程B读到count的值也是10


2.线程A对count++,此时count的值为11


3.线程B对count++,此时count的值也是11(因为线程B读到的count是10)


4.所以到这里应该知道为啥我们的结果是不确定了吧。


怎么破?


要得出正确的结果100,怎么办?


   synchronized


在increase()加synchronized锁就好了:

public synchronized void increase() {
    count++;
}


这样子无论执行多少次,得出的都是100。这个对于只要求解决问题,但不在乎效率,不想深挖的人,肯定已经ok了。但是我们仅仅只是对于这么简单的一个++,就动用这么"强悍的"Synchronized未免有点太小题大作了。


Synchronized悲观锁,是独占的,意味着如果有别的线程在执行,当前线程只能是等待!


那么接下来针对我们频繁碰到这个问题,JDK5提供的原子操作就要登场了


Atomic原子操作


在JDK1.5+的版本中,Doug Lea和他的团队还为我们提供了一套用于保证线程安全的原子操作。


JDK1.5的版本中为我们提供了java.util.concurrent.atomic原子操作包。所谓“原子”操作,是指一组不可分割的操作:操作者对目标对象进行操作时,要么完成所有操作后其他操作者才能操作;要么这个操作者不能进行任何操作。


有了他们,我们就很好解决上面遇到的问题了,只需要采用AtomicInteger稍加改动就OK了~~

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        AtomicInteger count = new AtomicInteger();
        // 100个线程对共享变量进行加1
        for (int i = 0; i < 100; i++) {
            service.execute(() -> count.incrementAndGet());
        }
        // 等待上述的线程执行完   和三个方法的区别 这里不做概述,反正都能关闭
        service.shutdown();
        //service.shutdownNow();
        service.awaitTermination(2, TimeUnit.SECONDS);
        service.shutdown();
        System.out.println(count.get());
    }


改用Atomic来执行后,我们发现不管执行多少次,结果都是正确的100;


JDK1.5以后这种轻量级的解决方案不再推荐使用synchronized,而使用Atomic代替,因为效率更高


源码分析


AotmicInteger其实就是对int的包装,然后里面内部使用CAS算法来保证操作的原子性

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }


可以看到,内部主要依赖于unsafe提供的CAS算法来实现的,因此我们很有必要了解一下,到底什么是CAS呢?


CAS解释


先概念走一波


比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。


从定义中我们可以总结出CAS有三个操作数:


1.内存值V


2.旧的预期值A


3.要修改的新值B


为了方便大家理解也为了我记忆深刻点,我特意自己尝试着画了一些图解(下同):


image.png


可以发现CAS有两种情况:


如果内存值V和我们的预期值A相等,则将内存值修改为B,操作成功!

如果内存值V和我们的预期值A不相等,一般也有两种情况:

1、重试(自旋) 2、什么都不做


CAS失败重试(自旋)


上面的例子,我们启动的100个线程,实质上都对结果进行了+1。但是可以想象到,肯定存在多个线程同一时刻同时想+1的情况,因此可见下图:

image.png


image.png


image.png



虽然这幅图只画了两个线程的情况,举一反三,任意多个线程的情况都是一样的处理方式。

相关文章
|
14天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
15天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
41 3
|
2月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
50 4
|
3月前
|
算法 Java 程序员
Java中的Synchronized,你了解多少?
Java中的Synchronized,你了解多少?
|
3月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
24 1
|
3月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
34 2
|
3月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
43 0
|
4月前
|
存储 安全 Java
Java并发编程之深入理解Synchronized关键字
在Java的并发编程领域,synchronized关键字扮演着守护者的角色。它确保了多个线程访问共享资源时的同步性和安全性。本文将通过浅显易懂的语言和实例,带你一步步了解synchronized的神秘面纱,从基本使用到底层原理,再到它的优化技巧,让你在编写高效安全的多线程代码时更加得心应手。
|
4月前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
5月前
|
传感器 C# 监控
硬件交互新体验:WPF与传感器的完美结合——从初始化串行端口到读取温度数据,一步步教你打造实时监控的智能应用
【8月更文挑战第31天】本文通过详细教程,指导Windows Presentation Foundation (WPF) 开发者如何读取并处理温度传感器数据,增强应用程序的功能性和用户体验。首先,通过`.NET Framework`的`Serial Port`类实现与传感器的串行通信;接着,创建WPF界面显示实时数据;最后,提供示例代码说明如何初始化串行端口及读取数据。无论哪种传感器,只要支持串行通信,均可采用类似方法集成到WPF应用中。适合希望掌握硬件交互技术的WPF开发者参考。
93 0