【小家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



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

相关文章
|
1天前
|
存储 安全 Java
Java中synchronized锁的深入理解
Java中synchronized锁的深入理解
21 1
|
1天前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
57 2
|
1天前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
12 0
|
1天前
|
存储 安全 Java
【亮剑】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制
【4月更文挑战第30天】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制。`ThreadLocal`为每个线程提供独立变量副本;`Volatile`确保变量可见性,但不保证原子性;`Synchronized`实现同步锁,保证单线程执行;`Atomic`类利用CAS实现无锁并发控制。理解其原理有助于编写高效线程安全代码。根据业务场景选择合适机制至关重要。
|
1天前
|
安全 Java 编译器
【Java EE】总结12种锁策略以及synchronized的实现原理
【Java EE】总结12种锁策略以及synchronized的实现原理
|
1天前
|
安全 Java 编译器
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。
26 0
|
1天前
|
安全 Java 调度
Java中,synchronized关键字你了解多少?
【4月更文挑战第16天】
55 14
|
1天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
1天前
|
Java
浅谈Java的synchronized 锁以及synchronized 的锁升级
浅谈Java的synchronized 锁以及synchronized 的锁升级
8 0
|
1天前
|
Java
Java中的线程同步:synchronized关键字的深度解析
【4月更文挑战第14天】在多线程环境下,线程同步是一个重要的话题。Java提供了多种机制来实现线程同步,其中最常用且最重要的就是synchronized关键字。本文将深入探讨synchronized关键字的工作原理,使用方法以及注意事项,帮助读者更好地理解和使用这一重要的线程同步工具。