并发编程之原子操作

简介: 这并不是一个化学课,而是巧妙的借用了化学上的一个概念,即原子是最小的粒子,不可再分;原子操作也是不能再分的操作;为了能把这个讲明白,下文基本都是大白话,其实Java本来并不是很难,而是总有一些人喜欢把简单的概念给复杂化。小编不喜欢那种说辞,所以尽量简单易懂。如有问题,欢迎提出问题。共同交流进步,最后谢谢你的阅读

作者: 西魏陶渊明
博客: https://blog.springlearn.cn/

原子特性: 原子是最小的粒子,不可再分

这并不是一个化学课,而是巧妙的借用了化学上的一个概念,即原子是最小的粒子,不可再分;原子操作也是不能再分的操作;
为了能把这个讲明白,下文基本都是大白话,其实Java本来并不是很难,而是总有一些人喜欢把简单的概念给复杂化。小编不喜欢
那种说辞,所以尽量简单易懂。如有问题,欢迎提出问题。共同交流进步,最后谢谢你的阅读。


举例说明原子操作重要性

在很多场景中我们需要我们的操作是原子特性的,如果我们写的程序都是单线程的,其实我们没必要考虑原子操作。但是假如
我们写多线程操作,或者是在Web服务中来更新对象属性,那么就必须要来考虑原子操作问题了。

举一个🌰例子A:

int a = 1;

可以看到程序对变量 a 操作,其实是有多个步骤进行的。在单线程环境下基本不会发生任何问题

举一个🌰例子B(单线程操作):

public class Tester {

    private static Integer a = 1;

    private static AtomicInteger aa = new AtomicInteger(1);

    private static void restore() {
        a = 1;
        aa = new AtomicInteger(1);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            test("第" + i + "次");
            restore();
        }
    }

    private static void test(String str) {
        for (int i = 1; i <= 1000; i++) {
            new Thread(() -> a = a + 1).start();
            new Thread(() -> aa.addAndGet(1)).start();
        }
        System.out.print(str + "常规操作a=" + a);
        System.out.println(" <===> "+str+"原子操作操作aa=" + aa);
    }
}

规律:

        /**
         * i              i+1
         * 1: a = 1 + 1 = 2
         * 2: a = 2 + 1 = 3
         * 3: a = 3 + 1 = 4
         * 4: a = 4 + 1 = 5
         * 5: a = 5 + 1 = 6
         * 6: a = 6 + 1 = 7
         * 7: a = 7 + 1 = 8
         * 8: a = 8 + 1 = 9
         * 9: a = 9 + 1 = 10
         * 10:a = 10 + 1 = 11
         */

如上面代码变量a是基本类型,变量aa是原子类型,正常情况对a或者aa进行1000次操作结果都应该是
1001。正常情况我们可以理解是单线程操作。结果也是没有问题的。

举一个🌰例子C(多线程操作):

public class Tester {

    private static Integer a = 1;

    private static AtomicInteger aa = new AtomicInteger(1);

    private static void restore() {
        a = 1;
        aa = new AtomicInteger(1);
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            test("第" + i + "次");
            restore();
        }
    }

    private static void test(String str) throws Exception {
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> a = a + 1).start();
            new Thread(() -> a = a + 1).start();

            new Thread(() -> aa.addAndGet(1)).start();
            new Thread(() -> aa.addAndGet(1)).start();
            Thread.sleep(1);
        }
        System.out.print(str + "常规操作a=" + a);
        System.out.println(" <===> " + str + "原子操作操作aa=" + aa);
    }
    
}

规律:

    /**
     * i          2 * i + 1
     * 1: a = 1 + 1 + 1 = 3
     * 2: a = 3 + 1 + 1 = 5
     * 3: a = 5 + 1 + 1 = 7
     * 4: a = 7 + 1 + 1 = 9
     * 5:                 11
     * 6:                 13
     * 7:                 15
     * 8:                 17
     * 9:                 19
     * 10:                21
     */

多线程环境下操作会不会有问题呢? 出现了问题。我们看到使用常规操作的a变量出现了数据不一致情况。

实际上当循环的次数越多,出现错误的几率就越大,如下我们循环了1000次。

问题分析

我们思考为什么基本类型进行多线程操作时候会出现这种情况呢? 其实问题答案最开始已经说了。 我们通过这张图
就可以找到原因。

对变量的每次操作其实都有3个步骤

  1. 读取变量值
  2. 变量值操作
  3. 变量重新赋值。

我们模拟一下错误的原因。

当A线程读取a=1,并对1+1。但是还未对变量重新赋值a=2的时候,
B线程也读取了A还未赋值的变量,此时变量还是1,那么B线程因为读取了还未更新的数据,所以也做1+1的操作。然后B对a
重新赋值了此时a=2,是B赋值的。这个时候A因为已经执行完了前两个步骤,最后也重新赋值了a=2。

这样数据就更新丢了。这就是因为数据更新不是原子性从而导致的问题。

因为数据更新丢了,所以出现了。

如何解决这种问题

如何解决这种问题,其实很简单只要我们保证我们的操作是原子操作即可,简单来说就是将更新的三个步骤合并成一个步骤即可,在Java中JDK已经为我们提供了很多的
原子操作每一个基本类型都对应一个原子操作。

原子基础类

原子基础类API

原子数组类

原子更新数组API

原子引用类

注意:

想要原子的更新字段,需要两个步骤:

  1.每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性

  2.更新类的字段(属性)必须使用public volatile修饰符

最后我们看一下原子操作的原理

最后求关注,求订阅,谢谢你的阅读!

相关文章
|
7月前
|
缓存 Java 编译器
JUC 并发编程之JMM
Java内存模型是Java虚拟机(JVM)规范中定义的一组规则,用于屏蔽各种硬件和操作系统的内存访问差异,保证多线程情况下程序的正确执行。Java内存模型规定了线程之间如何交互以及线程和内存之间的关系。它主要解决的问题是可见性、原子性和有序性。
|
7月前
|
Java 程序员 开发者
深入理解Java并发编程:线程同步与锁机制
【4月更文挑战第30天】 在多线程的世界中,确保数据的一致性和线程间的有效通信是至关重要的。本文将深入探讨Java并发编程中的核心概念——线程同步与锁机制。我们将从基本的synchronized关键字开始,逐步过渡到更复杂的ReentrantLock类,并探讨它们如何帮助我们在多线程环境中保持数据完整性和避免常见的并发问题。文章还将通过示例代码,展示这些同步工具在实际开发中的应用,帮助读者构建对Java并发编程深层次的理解。
|
7月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
4月前
|
Java C语言 C++
并发编程进阶:线程同步与互斥
并发编程进阶:线程同步与互斥
48 0
|
7月前
|
安全 中间件 编译器
【C/C++ 原子操作】深入浅出:从互斥锁到无锁编程的转变 - 理解C++原子操作和内存模型
【C/C++ 原子操作】深入浅出:从互斥锁到无锁编程的转变 - 理解C++原子操作和内存模型
3633 3
|
7月前
|
设计模式 并行计算 安全
【C/C++ 多线程编程】深入探讨双检锁与原子操作
【C/C++ 多线程编程】深入探讨双检锁与原子操作
246 0
多线程的原子操作
多线程的原子操作
68 0
|
缓存 算法 安全
06.一文看懂并发编程中的锁
大家好,我是王有志。相信你经常会听到读锁/写锁,公平锁/非公平锁,乐观锁/悲观锁等五花八门的锁,那么每种锁有什么用呢?它们又有什么区别呢?今天我们就一起聊聊并发编程中的各种锁。
208 1
06.一文看懂并发编程中的锁
|
算法 安全 C++
C++并发编程中的锁的介绍(一)
C++并发编程中的锁的介绍(一)
202 0
|
安全 编译器 C++
C++并发编程中的锁的介绍(二)
C++并发编程中的锁的介绍(二)
169 0