精华内容
下载资源
问答
  • 原子性

    2017-02-18 10:23:47
    当在一个表达式中使用一个non-long或者non-double型字段时,原子性可以确保你将获得这个字段的初始值或者某个线程对这个字段写入之后的值;但不会是两个或更多线程在同一时间对这个字段写入之后产生混乱的结果值(即...

      当在一个表达式中使用一个non-long或者non-double型字段时,原子性可以确保你将获得这个字段的初始值或者某个线程对这个字段写入之后的值;但不会是两个或更多线程在同一时间对这个字段写入之后产生混乱的结果值(即原子性可以确保,获取到的结果值所对应的所有bit位,全部都是由单个线程写入的)。但是,如下面(译注:指可见性章节)将要看到的,原子性不能确保你获得的是任意线程写入之后的最新值。

      在Java 中除了 long 和 double 之外的所有基本类型的读和赋值,都是原子性操作。而64位的long 和 double 变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性,会产生字撕裂问题。但是当你定义long或double变量时,如果使用 volatile关键字,就会获到(简单的赋值与返回操作的)原子性

      虽然java内存模型不保证non-volatile long 和 non-volatile double的原子性,当然它们在某些场合也具有原子性。(译注:non-volatile long在64位JVM,OS,CPU下具有原子性)
      注意,在JSR-133之前的旧内存模型中,一个64位long/double型变量的读/写操作可以被拆分为两个32位读/写操作来执行。从JSR-133内存模型(JDK5)开始,仅仅只允许把一个64位long/double型变量的写操作分拆成两个32位的写操作来执行,任意的读操作在JSR-133中都必须具有原子性。

    展开全文
  • 在并发编程中有三个非常重要的特性:原子性、有序性,、可见性,学妹发现你对它们不是很了解,她很着急,因为理解这三个特性对于能够正确地开发高并发程序有很大的帮助,接下来的面试中也极有可能被问到,小学妹就忍...

    在并发编程中有三个非常重要的特性:原子性、有序性,、可见性,学妹发现你对它们不是很了解,她很着急,因为理解这三个特性对于能够正确地开发高并发程序有很大的帮助,接下来的面试中也极有可能被问到,小学妹就忍不住开始跟你逐一介绍起来。

    Java内存模型

    在讲三大特性之前先简单介绍一下Java内存模型(Java Memory Model,简称JMM),了解了Java内存模型以后,可以更好地理解三大特性。

    Java内存模型是一种抽象的概念,并不是真实存在的,它描述的是一组规范或者规定。JVM运行程序的实体是线程,每一个线程都有自己私有的工作内存。Java内存模型中规定了所有变量都存储在主内存中,主内存是一块共享内存区域,所有线程都可以访问。但是线程对变量的读取赋值等操作必须在自己的工作内存中进行,在操作之前先把变量从主内存中复制到自己的工作内存中,然后对变量进行操作,操作完成后再把变量写回主内存。线程不能直接操作主内存中的变量,线程的工作内存中存放的是主内存中变量的副本。

    原子性(Atomicity)

    什么是原子性

    原子性是指:在一次或者多次操作时,要么所有操作都被执行,要么所有操作都不执行。

    一般说到原子性都会以银行转账作为例子,比如张三向李四转账100块钱,这包含了两个原子操作:在张三的账户上减少100块钱;在李四的账户上增加100块钱。这两个操作必须保证原子性的要求,要么都执行成功,要么都执行失败。不能出现张三的账户减少100块钱而李四的账户没增加100块钱,也不能出现张三的账户没减少100块钱而李四的账户却增加100块钱。

    原子性示例

    示例一
    i = 1;
    

    根据上面介绍的Java内存模型,线程先把i=1写入工作内存中,然后再把它写入主内存,就此赋值语句可以说是具有原子性。

    示例二
    i = j;
    

    这个赋值操作实际上包含两个步骤:线程从主内存中读取j的值,然后把它存入当前线程的工作内存中;线程把工作内存中的i改为j的值,然后把i的值写入主内存中。虽然这两个步骤都是原子性的操作,但是合在一起就不是原子性的操作。

    示例三
    i++;
    

    这个自增操作实际上包含三个步骤:线程从主内存中读取i的值,然后把它存入当前线程的工作内存中;线程把工作内存中的i执行加1操作;线程再把i的值写入主内存中。和上一个示例一样,虽然这三个步骤都是原子性的操作,但是合在一起就不是原子性的操作。

    从上面三个示例中,我们可以发现:简单的读取和赋值操作是原子性的,但把一个变量赋值给另一个变量就不是原子性的了;多个原子性的操作放在一起也不是原子性的。

    如何保证原子性

    在Java内存模型中,只保证了基本读取和赋值的原子性操作。如果想保证多个操作的原子性,需要使用synchronized关键字或者Lock相关的工具类。如果想要使int、long等类型的自增操作具有原子性,可以用java.util.concurrent.atomic包下的工具类,如:AtomicIntegerAtomicLong等。另外需要注意的是,volatile关键字不具有保证原子性的语义。

    可见性(Visibility)

    什么是可见性

    可见性是指:当一个线程对共享变量进行修改后,另外一个线程可以立即看到该变量修改后的最新值。

    可见性示例

    package onemore.study;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class VisibilityTest {
        public static int count = 0;
    
        public static void main(String[] args) {
            final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
    
            //读取count值的线程
            new Thread(() -> {
                System.out.println("开始读取count...");
                int i = count;//存放count的更新前的值
                while (count < 3) {
                    if (count != i) {//当count的值发生改变时,打印count被更新
                        System.out.println(sdf.format(new Date()) + " count被更新为" + count);
                        i = count;//存放count的更新前的值
                    }
                }
            }).start();
    
            //更新count值的线程
            new Thread(() -> {
                for (int i = 1; i <= 3; i++) {
                    //每隔1秒为count赋值一次新的值
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(sdf.format(new Date()) + " 赋值count为" + i);
                    count = i;
    
                }
            }).start();
        }
    }
    

    在运行代码之前,先想一下运行的输出是什么样子的?在更新count值的线程中,每一次更新count以后,在读取count值的线程中都会有一次输出嘛?让我们来看一下运行输出是什么:

    开始读取count...
    17:21:54.796 赋值count为1
    17:21:55.798 赋值count为2
    17:21:56.799 赋值count为3
    

    从运行的输出看出,读取count值的线程一直没有读取到count的最新值,这是为什么呢?因为在读取count值的线程中,第一次读取count值时,从主内存中读取count的值后写入到自己的工作内存中,再从工作内存中读取,之后的读取的count值都是从自己的工作内存中读取,并没有发现更新count值的线程对count值的修改。

    如何保证可见性

    在Java中可以用以下3种方式保证可见性。

    使用volatile关键字

    当一个变量被volatile关键字修饰时,其他线程对该变量进行了修改后,会导致当前线程在工作内存中的变量副本失效,必须从主内存中再次获取,当前线程修改工作内存中的变量后,同时也会立刻将其修改刷新到主内存中。

    使用synchronized关键字

    synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法或者代码块,并且确保在锁释放之前,会把变量的修改刷新到主内存中。

    使用Lock相关的工具类

    Lock相关的工具类的lock方法能够保证同一时刻只有一个线程获得锁,然后执行同步代码块,并且确保执行Lock相关的工具类的unlock方法在之前,会把变量的修改刷新到主内存中。

    有序性(Ordering)

    什么是有序性

    有序性指的是:程序执行的顺序按照代码的先后顺序执行。

    在Java中,为了提高程序的运行效率,可能在编译期和运行期会对代码指令进行一定的优化,不会百分之百的保证代码的执行顺序严格按照编写代码中的顺序执行,但也不是随意进行重排序,它会保证程序的最终运算结果是编码时所期望的。这种情况被称之为指令重排(Instruction Reordering)。

    有序性示例

    package onemore.study;
    
    public class Singleton {
        private Singleton (){}
    
        private static boolean isInit = false;
        private static Singleton instance;
    
        public static Singleton getInstance() {
            if (!isInit) {//判断是否初始化过
                instance = new Singleton();//初始化
                isInit = true;//初始化标识赋值为true
            }
            return instance;
        }
    }
    

    这是一个有问题的单例模式示例,假如在编译期或运行期时指令重排,把isInit = true;重新排序到instance = new Singleton();的前面。在单线程运行时,程序重排后的执行结果和代码顺序执行的结果是完全一样的,但是多个线程一起执行时就极有可能出现问题。比如,一个线程先判断isInit为false进行初始化,本应在初始化后再把isInit赋值为true,但是因为指令重排没后初始化就把isInit赋值为true,恰好此时另外一个线程在判断是否初始化过,isInit为true就执行返回了instance,这是一个没有初始化的instance,肯定造成不可预知的错误。

    如何保证有序性

    这里就要提到Java内存模型的一个叫做先行发生(Happens-Before)的原则了。如果两个操作的执行顺序无法从Happens-Before原则推到出来,那么可以对它们进行随意的重排序处理了。Happens-Before原则有哪些呢?

    • 程序次序原则:一段代码在单线程中执行的结果是有序的。
    • 锁定原则:一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。
    • volatile变量原则:同时对volatile变量进行读写操作,写操作一定先于读操作。
    • 线程启动原则:Thread对象的start方法先于此线程的每一个动作。
    • 线程终结原则:线程中的所有操作都先于对此线程的终止检测。
    • 线程中断原则:对线程interrupt方法的调用先于被中断线程的代码检测到中断事件的发生。
    • 对象终结原则:一个对象的初始化完成先于它的finalize方法的开始。
    • 传递原则:操作A先于操作B,操作B先于操作C,那么操作A一定先于操作C。

    除了Happens-Before原则提供的天然有序性,我们还可以用以下几种方式保证有序性:

    • 使用volatile关键字保证有序性。
    • 使用synchronized关键字保证有序性。
    • 使用Lock相关的工具类保证有序性。

    总结

    • 原子性:在一次或者多次操作时,要么所有操作都被执行,要么所有操作都不执行。
    • 可见性:当一个线程对共享变量进行修改后,另外一个线程可以立即看到该变量修改后的最新值。
    • 有序性:程序执行的顺序按照代码的先后顺序执行。

    synchronized关键字和Lock相关的工具类可以保证原子性、可见性和有序性,volatile关键字可以保证可见性和有序性,不能保证原子性。

    文章持续更新,微信搜索「 万猫学社 」第一时间阅读。
    关注后回复「 电子书 」,免费获取12本Java必读技术书籍。

    展开全文
  • 在 Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例中,对并发中的三个特性(原子性、可见性和有序性)进行了初步学习。 本章主要就Java中保障原子性的技术进行更加全面的学习。 1.整体回顾 原子性...

    [超级链接:Java并发学习系列-绪论]

    在Java并发编程中,如果要保证代码的安全性,则必须保证代码的原子性、可见性和有序性。

    Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例中,对并发中的三个特性(原子性、可见性和有序性)进行了初步学习。

    本章主要就Java中保障原子性的技术进行更加全面的学习。

    1.整体回顾

    • 原子性定义:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
    • Java自带原子性:对基本数据类型的变量读取赋值操作是原子性操作。

    2.原子性问题

    由上面的章节已知,不采取任何的原子性保障措施的自增操作并不是原子性的。
    下面的代码实现了一个自增器(不是原子性的)。

    /**
     * <p>原子性示例:不是原子性</p>
     *
     * @author hanchao 2018/3/10 14:58
     **/
    static class Increment {
        private int count = 1;
    
        public void increment() {
            count++;
        }
    
        public int getCount() {
            return count;
        }
    }

    下面的代码展示了在多线程环境中,调用此自增器进行自增操作。

    int type = 0;//类型
    int num = 50000;//自增次数
    int sleepTime = 5000;//等待计算时间
    int begin;//开始的值
    Increment increment;
    //不进行原子性保护的大范围操作
    increment = new Increment();
    begin = increment.getCount();
    LOGGER.info("Java中普通的自增操作不是原子性操作。");
    LOGGER.info("当前运行类:" +increment.getClass().getSimpleName() +  ",count的初始值是:" + increment.getCount());
    for (int i = 0; i < num; i++) {
        new Thread(() -> {
            increment.increment();
        }).start();
    }
    //等待足够长的时间,以便所有的线程都能够运行完
    Thread.sleep(sleepTime);
    LOGGER.info("进过" + num + "次自增,count应该 = " + (begin + num) + ",实际count = " + increment.getCount());

    某次运行结果:

    2018-03-17 22:52:23 INFO  ConcurrentAtomicityDemo:132 - Java中普通的自增操作不是原子性操作。
    2018-03-17 22:52:23 INFO  ConcurrentAtomicityDemo:133 - 当前运行类:Increment,count的初始值是:1
    2018-03-17 22:52:33 INFO  ConcurrentAtomicityDemo:141 - 进过50000次自增,count应该 = 50001,实际count = 49999

    通过观察结果,发现程序确实存在原子性问题。

    3.原子性技术保障

    在Java中提供了多种原子性保障措施,这里主要涉及三种:

    • 通过synchronized关键字定义同步代码块或者同步方法保障原子性。
    • 通过Lock接口保障原子性。
    • 通过Atomic类型保障原子性。

    3.1.synchronized关键字

    Increment类进行扩展:

    /**
     * <p>原子性示例:通过synchronized保证代码块的原子性</p>
     *
     * @author hanchao 2018/3/10 15:07
     **/
    static class SynchronizedIncrement extends Increment {
        /**
         * <p>添加关键字synchronized,使之成为同步方法</p>
         *
         * @author hanchao 2018/3/10 15:12
         **/
        @Override
        public synchronized void increment() {
            super.count++;
        }
    }

    在多线程环境中进行SynchronizedIncrement 的自增:

    //synchronized关键字能够保证原子性(代码块锁,多线程操作某一对象时,在某个代码块内只能单线程执行)
    increment = new SynchronizedIncrement();
    begin = increment.getCount();
    LOGGER.info("可以通过synchronized关键字保障代码的原子性");
    LOGGER.info("当前运行类:" +increment.getClass().getSimpleName() +  ",count的初始值是:" + increment.getCount());
    for (int i = 0; i < num; i++) {
        new Thread(() -> {
            increment.increment();
        }).start();
    }
    //等待足够长的时间,以便所有的线程都能够运行完
    Thread.sleep(sleepTime);
    LOGGER.info("进过" + num + "次自增,count应该 = " + (begin + num) + ",实际count = " + increment.getCount());

    运行结果(多次):

    2018-03-18 00:41:30 INFO  ConcurrentAtomicityDemo:147 - 可以通过synchronized关键字保障代码的原子性
    2018-03-18 00:41:30 INFO  ConcurrentAtomicityDemo:148 - 当前运行类:SynchronizedIncrement,count的初始值是:1
    2018-03-18 00:41:40 INFO  ConcurrentAtomicityDemo:156 - 进过50000次自增,count应该 = 50001,实际count = 50001

    通过多次运行,发现运行结果一致,所以可以确定synchronized关键字能够保证代码的原子性

    3.2.Lock接口

    Increment类进行扩展:

    /**
    * <p>原子性示例:通过Lock接口保证指定范围代码的原子性</p>
    *
    * @author hanchao 2018/3/10 15:14
    **/
    static class LockIncrement extends Increment {
       //定义个读写锁:锁内运行多线程读,单线程写
       private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    
       /**
        * <p>运用读写所重写方法</p>
        *
        * @author hanchao 2018/3/10 15:13
        **/
       @Override
       public void increment() {
           //写锁 加锁
           readWriteLock.writeLock().lock();
           try {
               //开始写
               super.count++;
           } finally {
               //将解锁放在finally块中,保证必然执行,防止死锁
               readWriteLock.writeLock().unlock();
           }
       }
    }

    在多线程环境中进行LockIncrement的测试:

    //通过Lock接口保证原子性操作
    increment = new LockIncrement();
    begin = increment.getCount();
    LOGGER.info("可以通过Lock接口保证代码的原子性");
    LOGGER.info("当前运行类:" +increment.getClass().getSimpleName() +  ",count的初始值是:" + increment.getCount());
    for (int i = 0; i < num; i++) {
        new Thread(() -> {
            increment.increment();
        }).start();
    }
    //等待足够长的时间,以便所有的线程都能够运行完
    Thread.sleep(sleepTime);
    LOGGER.info("进过" + num + "次自增,count应该 = " + (begin + num) + ",实际count = " + increment.getCount());

    运行结果(多次):

    2018-03-18 10:12:12 INFO  ConcurrentAtomicityDemo:163 - 可以通过Lock接口保证代码的原子性
    2018-03-18 10:12:12 INFO  ConcurrentAtomicityDemo:164 - 当前运行类:LockIncrement,count的初始值是:1
    2018-03-18 10:12:29 INFO  ConcurrentAtomicityDemo:172 - 进过50000次自增,count应该 = 50001,实际count = 50001

    通过多次运行,发现运行结果一致,所以可以确定Lock接口能够保证代码的原子性

    3.3.Atomic类型

    Increment类进行扩展:

    /**
    * <p>原子性示例:通过Atomic类型保证类型的原子性</p>
    *
    * @author hanchao 2018/3/10 15:19
    **/
    static class AtomicIncrement {
       private AtomicInteger count = new AtomicInteger(1);
    
       /**
        * <p>无需其他处理,直接自增即可</p>
        *
        * @author hanchao 2018/3/10 15:21
        **/
       public void increment() {
           count.getAndIncrement();
       }
    
       public AtomicInteger getCount() {
           return count;
       }
    }

    在多线程环境中进行AtomicIncrement的测试:

    //通过Atomic变量保证变量操作的原子性
    AtomicIncrement increment1 = new AtomicIncrement();
    begin = increment1.getCount().get();
    LOGGER.info("可以通过Atomic类型保证变量的原子性");
    LOGGER.info("当前运行类:" +increment1.getClass().getSimpleName() +  ",count的初始值是:" + increment1.getCount());
    for (int i = 0; i < num; i++) {
        new Thread(() -> {
            increment1.increment();
        }).start();
    }
    //等待足够长的时间,以便所有的线程都能够运行完
    Thread.sleep(sleepTime);
    LOGGER.info("进过" + num + "次自增,count应该 = " + (begin + num) + ",实际count = " + increment1.getCount());

    运行结果(多次):

    2018-03-18 10:14:37 INFO  ConcurrentAtomicityDemo:178 - 可以通过Atomic类型保证变量的原子性
    2018-03-18 10:14:37 INFO  ConcurrentAtomicityDemo:179 - 当前运行类:AtomicIncrement,count的初始值是:1
    2018-03-18 10:14:48 INFO  ConcurrentAtomicityDemo:187 - 进过50000次自增,count应该 = 50001,实际count = 50001

    通过多次运行,发现运行结果一致,所以可以确定Atomic类型能够保证代码的原子性

    4.总结

    经验证,以下三种措施,可以保证Java代码在运行时的原子性:

    • synchronized关键字
    • Lock接口
    • Atomic类型

    并发三特性总结

    特性 volatile关键字 synchronized关键字 Lock接口 Atomic变量
    原子性 无法保障 可以保障 可以保障 可以保障
    可见性 可以保障 可以保障 可以保障 可以保障
    有序性 一定程度保障 可以保障 可以保障 无法保障
    展开全文
  • 诡异的并发之原子性

    千次阅读 多人点赞 2020-03-03 09:19:24
    上一节我和大家一起打到了并发中的恶霸可见性,这一节我们继续讨伐三恶之一的原子性。 序、原子性的阐述 一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。 我理解是一个操作不可再分,即为原子性。而...

    上一节我和大家一起打到了并发中的恶霸可见性,这一节我们继续讨伐三恶之一的原子性。

    序、原子性的阐述

    一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

    我理解是一个操作不可再分,即为原子性。而在并发编程的环境中,原子性的含义就是只要该线程开始执行这一系列操作,要么全部执行,要么全部未执行,不允许存在执行一半的情况。

    我们试着从数据库事务和并发编程两个方面来进行对比:

    1、在数据库中

    原子性概念是这样子的:事务被当做一个不可分割的整体,包含在其中的操作要么全部执行,要么全部不执行。且事务在执行过程中如果发生错误,会被回滚到事务开始前的状态,就像这个事务没有执行一样。(也就是说:事务要么被执行,要么一个都没被执行)

    2、在并发编程中

    原子性概念是这样子的:

    • 第一种理解:一个线程或进程在执行过程中,没有发生上下文切换。
      • 上下文切换:指CPU从一个进程/线程切换到另外一个进程/线程(切换的前提就是获取CPU的使用权)。
    • 第二种理解:我们把一个线程中的一个或多个操作(不可分割的整体),在CPU执行过程中不被中断的特性,称为原子性。(执行过程中,一旦发生中断,就会发生上下文切换)

    从上文中对原子性的描述可以看出,并发编程和数据库两者之间的原子性概念有些相似: 都是强调,一个原子操作不能被打断!

    而非原子操作用图片表示就是这样子的:

    线程A在执行一会儿(还没有执行完成),就出让CPU让线程B执行。这样的操作在操作系统中有很多,牺牲切换线程的极短耗时,来提高CPU的利用率,从而在整体上提高系统性能;操作系统的这种操作就被称为“时间片”切换。

    一、出现原子性问题的原因

    通过序中描述的原子性的概念,我们总结出了:导致共享变量在线程之间出现原子性问题的原因是上下文切换。

    那么接下来,我们通过一个例子来重现原子性问题。

    首先定义一个银行账户实体类:

        @Data
        @AllArgsConstructor
        public static class BankAccount {
            private long balance;
    
            public long deposit(long amount){
                balance = balance + amount;
                return balance;
            }
        }

    然后开启多个线程对这个共享的银行账户进行存款操作,每次存款1元:

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import java.util.ArrayList;
    
    /**
     * @author :mmzsblog
     * @description:并发中的原子性问题
     * @date :2020/2/25 14:05
     */
    public class AtomicDemo {
    
        public static final int THREAD_COUNT = 100;
        static BankAccount depositAccount = new BankAccount(0);
    
        public static void main(String[] args) throws Exception {
    
            ArrayList<Thread> threads = new ArrayList<>();
            for (int i = 0; i < THREAD_COUNT; i++) {
                Thread thread = new DepositThread();
                thread.start();
                threads.add(thread);
            }
    
            for (Thread thread : threads) {
                thread.join();
            }
    
            System.out.println("Now the balance is " + depositAccount.getBalance() + "元");
        }
    
        static class DepositThread extends Thread {
            @Override
            public void run() {
                for (int j = 0; j < 1000; j++) {
                    depositAccount.deposit(1);   // 每次存款1元
                }
            }
        }
    }

    多次运行上面的程序,每次的结果几乎都不一样,偶尔能得到我们期望的结果100*1*1000=100000元,如下是我列举的几次运行结果:

    出现上面情况的原因就是因为

    balance = balance + amount;

    这段代码并不是原子操作,其中的balance是一个共享变量。在多线程环境下可能会被打断。就这样原子性问题就赤裸裸的出现了。如图所示:

    当然,如果balance是一个局部变量的话,即使在多线程的情况也不会出现问题(但是这个共享银行账户不适用局部变量啊,否则就不是共享变量了,哈哈,相当于废话),因为局部变量是当前线程私有的。就像图中for循环里的j变量。

    欢迎关注公众号"Java学习之道",查看更多干货!

    但是呢,即使是共享变量,小编我也绝不允许这样的问题出现,所以我们需要解决它,然后更加深刻的理解并发编程中的原子性问题。

    二、解决上下文切换带来的原子性问题

    2.1、使用局部变量

    局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就被抛弃了,局部变量和方法同生共死。而调用栈的栈帧也是和方法同生共死的,所以局部变量放到调用栈里那儿是相当的合理。而事实上,局部变量的确是放到了调用栈里。

    正是因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。总结起来就是:没有共享,就不会出错。

    但此处如果用局部变量的话,100个线程各自存1000元,最后都是从0开始存,不会累计,也就失去了原本想要展现的结果。故此方法不可行。

    正如此处使用单线程也能保证原子性一样,因为不适合当前场景,因此并不能解决问题。

    2.2、自带原子性保证

    在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作。

    比如下面这几行代码:

    // 原子性
    a = true;  
    
    // 原子性
    a = 5;     
    
    // 非原子性,分两步完成:
    //          第1步读取b的值
    //          第2步将b赋值给a
    a = b;     
    
    // 非原子性,分三步完成:
    //          第1步读取b的值
    //          第2步将b值加2
    //          第3步将结果赋值给a
    a = b + 2; 
    
    // 非原子性,分三步完成:
    //          第1步读取a的值
    //          第2步将a值加1
    //          第3步将结果赋值给a
    a ++;      

    2.3、synchronized

    把所有java代码都弄成原子性那肯定是不可能的,计算机一个时间内能处理的东西永远是有限的。所以当没法达到原子性时,我们就必须使用一种策略去让这个过程看上去是符合原子性的。因此就有了synchronized。

    synchronized既可以保证操作的可见性,也可以保证操作结果的原子性。

    某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法。

    如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。

    所以,此处我们只需要将存款的方法设置成synchronized的就能保证原子性了。

     private volatile long balance;
    
     public synchronized long deposit(long amount){
         balance = balance + amount; //1
         return balance;
     }

    加了synchronized后,当一个线程没执行完加了synchronized的deposit这个方法前,其他线程是不能执行这段被synchronized修饰的代码的。因此,即使在执行代码行1的时候被中断了,其它线程也不能访问变量balance;所以从宏观上来看的话,最终的结果是保证了正确性。但中间的操作是否被中断,我们并不知道。如需了解详情,可以看看CAS操作。

    PS:对于上面的变量balance大家可能会有点疑惑:变量balance为什么还要加上volatile关键字?其实这边加上volatile关键字的目的是为了保证balance变量的可见性,保证进入synchronized代码块每次都会去从主内存中读取最新值。

    故此,此处的

     private volatile long balance;

    也可以换成synchronized修饰

     private synchronized long balance;

    因为而这都能保证可见性,我们在第一篇文章诡异的并发之可见性中已经介绍过了。

    2.4、Lock锁

    public long deposit(long amount) {
        readWriteLock.writeLock().lock();
        try {
            balance = balance + amount;
            return balance;
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    Lock锁保证原子性的原理和synchronized类似,这边不进行赘述了。

    可能有的读者会好奇,Lock锁这里有释放锁的操作,而synchronized好像没有。其实,Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解锁 unlock(),这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的,毕竟忘记解锁 unlock() 可是个致命的 Bug(意味着其他线程只能死等下去了)。

    2.5、原子操作类型

    如果要用原子类定义属性来保证结果的正确性,则需要对实体类作如下修改:

        @Data
        @AllArgsConstructor
        public static class BankAccount {
            private AtomicLong balance;
    
            public long deposit(long amount) {
                return balance.addAndGet(amount);
            }
        }

    JDK提供了很多原子操作类来保证操作的原子性。比如最常见的基本类型:

    AtomicBoolean
    AtomicLong
    AtomicDouble
    AtomicInteger

    这些原子操作类的底层是使用CAS机制的,这个机制保证了整个赋值操作是原子的不能被打断的,从而保证了最终结果的正确性。

    和synchronized相比,原子操作类型相当于是从微观上保证原子性,而synchronized是从宏观上保证原子性。

    上面的2.5解决方案中,每个小操作都是原子性的,比如AtomicLong这些原子类的修改操作,它们本身的crud操作是原子的。

    那么,仅仅是将每个小操作都符合原子性是不是代表了这整个构成是符合原子性了呢?

    显然不是。

    它仍然会产生线程安全问题,比如一个方法的整个过程是读取A-读取B-修改A-修改B-写入A-写入B;那么,如果在修改A完成以后,失去操作原子性,此时线程B却开始执行读取B操作,此时就会出现原子性问题。

    总之不要以为使用了线程安全类,你的所有代码就都是线程安全的!这总归都要从审查你代码的整体原子性出发。就比如下面的例子:

        @NotThreadSafe
        public class UnsafeFactorizer implements Servlet {
    
            private final AtomicReference<BigInteger> lastNum = new AtomicReference<BigInteger>();
            private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
    
            @Override
            public void service(ServletRequest request, ServletResponse response) {
                BigInteger tmp = extractFromRequest(request);
                if (tmp.equals(lastNum.get())) {
                    System.out.println(lastFactors.get());
                } else {
                    BigInteger[] factors = factor(tmp);
                    lastNum.set(tmp);
                    lastFactors.set(factors);
                    System.out.println(factors);
                }
            }
        }

    虽然它全部用了原子类来进行操作,但是各个操作之间不是原子性的。也就是说:比如线程A在执行else语句里的lastNumber.set(tmp)完后,也许其他线程执行了if语句里的lastFactorys.get()方法,随后线程A才继续执行lastFactors.set(factors)方法更新factors

    从这个逻辑过程中,线程安全问题就已经发生了。

    它破坏了方法的读取A-读取B-修改A-修改B-写入A-写入B这一整体过程,在写入A完成以后其他线程去执行了读取B,就导致了读取到的B值不是写入后的B值。就这样原子性就出现了。

    好了,以上内容就是我对并法中的原子性的一点理解与总结了,通过这两篇文章我们也就大致掌握了并发中常见的可见性、原子性问题以及它们常见的解决方案。

    最后

    贴一段经常看到的原子性实例问题。

    :常听人说,在32位的机器上对long型变量进行加减操作存在并发隐患,到底是不是这样呢?

    :在32位的机器上对long型变量进行加减操作存在并发隐患的说法是正确的。

    原因就是:线程切换带来的原子性问题。

    非volatile类型的long和double型变量是8字节64位的,32位机器读或写这个变量时得把人家咔嚓分成两个32位操作,可能一个线程读了某个值的高32位,低32位已经被另一个线程改了。所以官方推荐最好把longdouble 变量声明为volatile或是同步加锁synchronize以避免并发问题。

    参考文章:

    • 1、极客时间的Java并发编程实战
    • 2、https://juejin.im/post/5d52abd1e51d4561e6237124
    • 3、https://www.cnblogs.com/54chensongxia/p/12073428.html
    • 4、https://www.dazhuanlan.com/2019/10/04/5d972ff1e314a/


    欢迎关注公众号:Java学习之道

    个人博客网站:www.mmzsblog.cn


    展开全文
  • 原子性、有序性和可见性解释

    千次阅读 2019-05-17 18:26:53
    原子性(Atomicity) 由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而...
  • 原子性、可见性、有序性解决方案

    千次阅读 2020-07-11 09:38:27
    原子性、可见性、有序性解决方案 (一)原子性 原子性是指:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在Java中当我们讨论一个操作具有原子性问题是一般就是指这个操作会被线程...
  • 程序的原子性操作

    万次阅读 2020-08-14 00:21:09
    作为开发人员经常会用到锁, 因为锁通常来说具有原子性,那原子性又是什么? 有些人说,原子性就是同一个代码块在任何时刻只能被一个线程访问执行,其他线程则等待(阻塞). 细想一下这句话应该属于排它性. 也有人说不可...
  • 原子性操作 原子性是指事务的一个完整性操作。对一件事务进行操作,操作成功则提交,失败则回滚。得以保障事务的完整性,要么做完,要么什么也不做。在进行该事务的过程中不能被其它进程(线程)中断。
  • 原子性与可见性

    2018-10-21 11:12:55
    Java 内存模型是围绕着原子性、可见性、有序性三个方面建立的,理解原子性与可见性有助于我们理解 Java 内存模型,加深对多线程编程的理解。 原子性 原子性指一个操作不能被打断,要么全部执行完毕,要么不执行。 ...
  • 原子性与原子操作

    千次阅读 2017-10-12 15:27:57
    原子性:如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。 原子性达到的目标:就是能使一个程序被完整的执行。 原子操作:不可被中断的一个或一系列的操作。 CAS有3个操作数,内存值V,...
  • 问题:并发编程中的原子性是什么?数据库的ACID中,A也是指原子性,这两个原子性之间完全一样吗? 解答:原子性是指:一个操作是不可中断的,要全部执行完成,要不就都不执行。 数据库事务中,保证原子性通过...
  • java原子性

    2017-11-02 16:31:14
    Java内存模型是围绕着并发过程中如何处理原子性、可见性、有序性这三个特征来建立的,下面是这三个特性的实现原理: 1.原子性(Atomicity)  由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign...
  • 什么是原子性、可见性、有序性? 原子性原子性指的是一个操作不会被中断,操作不会受到其他线程的影响。两个线程同时对一个变量赋值,则该值要么是1要么是2,线程A和线程B互不干扰,不会被中断。 要知道基本...
  • 目录定义原子性AtomicXxxAtomicStampedReference可见性有序性参考资料 定义 首先大家需要思考一下何为线程安全性呢??? 《Java并发编程实战》书中给出定义:当多个线程访问某个类时,不管运行时环境采用何种调度...
  • 为什么Redis的操作是原子性的,怎么保证原子性的? 对于Redis而言,命令的原子性指的是: 一个操作的不可以再分,操作要么执行,要么不执行。 Redis的操作之所以是原子性的,是因为Redis是单线程的。 Redis本身提供...
  • volatile的原子性

    千次阅读 2020-04-15 11:18:25
    volatile可以保证可见性,有序性,但是不能保证原子性,这也是今天写代码时候忽略的问题。 什么是原子性 原子性即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 测试代码 ...
  • volatile可以保证原子性

    万次阅读 2021-03-11 13:12:10
    在之前我们了解到了线程的三大特性:原子性,可见性,有序性。 前面的例子我们知道了volatile可以保证共享变量的可见性,但是volatile可以保证原子性吗? 我们来看看: public class Test { public volatile int in...
  • AtomicInteger原子性

    2018-09-01 10:04:29
    保证多线程,可见变量的原子性 package com.bjsxt.base.sync007; import java.util.concurrent.atomic.AtomicInteger; /** * volatile关键字不具备synchronized关键字的原子性(同步) * */ public class ...
  • 原子性: 原子性就是说一个操作不可以被中途cpu暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的, 那么在多线程环境下, 就不会出现变量被修改等奇怪的问题. 原子性的操作是线程...
  • java安全编码指南之:可见性和原子性

    万次阅读 热门讨论 2020-09-25 10:57:36
    java类中会定义很多变量,有类变量也有实例变量,这些变量在访问的过程中,会遇到一些可见性和原子性的问题。这里我们来详细了解一下怎么避免这些问题。
  • 原子性:先创建临时文件 当用户写入完成之后才会将临时文件拷贝到正式文件; 非原子性:用户边写边读入
  • 多线程:原子性与非原子性

    千次阅读 2016-03-24 14:23:38
    // 07-原子性和非原子性 // // Created by gzxzmac on 16/1/28. // Copyright © 2016年 gzxzmac. All rights reserved. //#import "ViewController.h"@interface ViewController () @property (non
  • 可见性和原子性

    2017-08-29 20:01:32
    原子性原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个...
  • 原子性 幂等性

    2017-11-15 11:51:27
    原子性: 如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只...
  • 什么是java的原子性原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 一个很经典的例子就是银行账户转账问题: 比如从账户A向账户B转1000元,那么必然包括2个...
  • 原子性、可见性、有序性的概念都是基于多线程操作条件下的。 原子性:一个操作具有原子操作,那么我们称它具有原子性。(什么是原子操作?一个操作是不可分割的。) 原子是世界上的最小单位,具有不可分割性...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 49,679
精华内容 19,871
关键字:

原子性