精华内容
下载资源
问答
  • java并发三特性

    2021-03-14 21:31:51
    在Java内存模型中,有大性质:原子性、有序性和可见性。一、原子性:熟悉数据库特性的我们都知道数据库sql执行中也有原子性,数据库中的原子性是这样定义的在一个事务中要么所有的sql都执行,要么都不执行。java...

    在Java内存模型中,有三大性质:原子性、有序性和可见性。

    一、原子性:熟悉数据库特性的我们都知道数据库sql执行中也有原子性,数据库中的原子性是这样定义的在一个事务中要么所有的sql都执行,要么都不执行。

    java内存模型中的原子性也是类似,要么所有的指令都执行,要么都不执行。这样才能保证并发操作的安全性和一致性。但是并发在带给我们方便的同时,却不能很好的

    解决原子性的问题。下面我们看一下Java并发操作中是怎么产生原子性问题的。

    操作系统为了提高并行处理问题的能力,会将时间分成一个个小的分片,例如50毫秒,过了50毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”)

    a529f47d8103

    图1 线程切换

    针对同一个cpu,线程A获取到cpu的使用权后开始执行自己的任务,但是当时间超过一个时间分片之后需要将cpu的使用权让出来,此时线程B会占有cpu的使用权。但是如果此时线程A的任务没有执行完成,又进行了线程切换,此时线程A的操作就无法保证原子性了。下面举一个例子 针对代码 count+=1;至少需要三条 CPU 指令.

    (1) 将count的值从内存中读取到寄存器中。

    (2) 在寄存器中进行+1操作。

    (3) 将结果写入内存中。

    但是当上述三个过程中线程A 刚执行完步骤(1)后,进行了线程切换,线程B重新执行上述操作,最后内存中存储的数据是1而不是2。具体流程如图2所示。

    a529f47d8103

    图2

    以上便是线程切换带来的Java并发原子性问题。

    二、可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

    每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。这个就属于硬件程序员给软件程序员挖的“坑”。

    a529f47d8103

    图3

    针对前面的理论看下面一段代码

    public class Test {

    private int count = 0;

    private void add() {

    int idx = 0;

    while(idx++ < 1000) {

    count += 1;

    }

    }

    public static long calc() {

    final Test test = new Test();

    // 创建两个线程,执行add()操作

    Thread th1 = new Thread(()->{

    test.add();

    });

    Thread th2 = new Thread(()->{

    test.add();

    });

    // 启动两个线程

    th1.start();

    th2.start();

    // 等待两个线程执行结束

    th1.join();

    th2.join();

    return count;

    }

    }

    启动了2个线程分别对一个从0开始的数字分别加1000次,最后返回的结果一定是介于1000--2000中间的数字。出现这个问题的原因是多线程操作时一个线程修改了内存中的数值时没有来得及通知其他线程所在cpu的缓存中。导致另外一个线程从缓存中读到的数据还是原来的数据,所以最后的结果达不到2000。这也就是并发问题中的可见性问题。

    三、有序性

    有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,但是不会影响最终的执行结果。有序性比较经典的例子就是利用双重检查创建单例对象。

    public class Singleton {

    static Singleton instance;

    static Singleton getInstance(){

    if (instance == null) {

    synchronized(Singleton.class) {

    if (instance == null)

    instance = new Singleton();

    }

    }

    return instance;

    }

    }

    在Java创建对象的时候主要进行以下几个步骤:

    (1)分配一块内存区域W;

    (2)在内存W上初始化 Singleton 对象;

    (3) 然后 W 的地址赋值给 instance 变量。

    但是Java在编译的时候会进行编译优化,把方法的执行顺序修改成

    (1)分配一块内存区域W;

    (2) 然后 W 的地址赋值给 instance 变量。

    (3)在内存W上初始化 Singleton 对象;

    如果线程A按照优化好的逻辑执行到第(2)步骤给开辟的内存地址分配给instance对象后,线程B的请求也打过来,此时getInstance()方法时 instance 是不等于null的,线程B会认为已经创建好单例对象,直接返回,后面的业务代码在进行对象操作的时候会出现空指针的问题。具体流程如下图所示。

    a529f47d8103

    图4

    以上便是java并发的三大特性以及并发给程序带来的问题,后续我将针对Java并发带来的问题更新相关的解决办法以及Java并发的其他相关知识,敬请期待。

    展开全文
  • 并发三特性——可见性

    千次阅读 2021-10-28 22:14:12
    熟悉并发的童鞋们都知道,并发编程有特性,分别是可见性、有序性、原子性,今天我们从一个demo中分析可见性,以及我们如何保障可见性。 JMM模型 在我们分析可见性之前,我们需要了解一个概念,就是JMM模型,也...

    引言

    熟悉并发的童鞋们都知道,并发编程有三大特性,分别是可见性、有序性、原子性,今天我们从一个demo中分析可见性,以及我们如何保障可见性。

    JMM模型

    在我们分析可见性之前,我们需要了解一个概念,就是JMM模型,也就是我们常说的java memory model .

    java虚拟机规范中定义了Java内存模型,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现程序在各个平台上达到一致的并发效果,JMM规范了java虚拟机与计算机内存是如何协同工作的。规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式。

     

    上代码

    package com.tuling.juc.service;
    
    import com.tuling.concurrent.lock.UnsafeInstance;
    import com.tuling.juc.Factory.UnsafeFactory;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.LockSupport;
    
    /**
     * @Description
     * @Author zhenghao
     * @Date 2021/10/28 18:26
     **/
    
    public class VisibilityTest {
    
        //volatile  是通过内存屏障来实现 可见性 ,汇编层面 会在执行的指令前面 增加  lock关键字
        //lock 关键字 可以起到和内存屏障一样的作用,但不是内存屏障
        //lock 前缀指令 会等待它之前所有的指令都完成,并且所有缓冲的写操作写会内存
        private volatile boolean flag = true;
    
        private int count = 0;
    
        public void refresh() {
            flag = false;
            System.out.println(Thread.currentThread().getName() + "修改flag");
        }
    
        public void load() {
            System.out.println(Thread.currentThread().getName() + "开始执行.....");
            int i = 0;
            while (flag) {
                i++;
                count++;
                //TODO  业务逻辑
                //等待足够长的时间的时候,本地内存中的变量值会被主动刷新到主内存中
                //没有这句代码的时候,程序不会中断,这是因为whil true 计算机任务flag这个
                //变量随时都会被用到,所以就不会刷新到主内存中
                //shortWait(1000000);
    
                //下面这种方式,是如何解决可见性呢?线程上下文切换 加载上下文
    //            Thread.yield();//使当前线程由运行态 转换成 就绪态,让出CPU占用时间
    
                //通过内存屏障来实现 可见性
    //            UnsafeFactory.getUnsafe().storeFence();
    
                // 底层使用到了 synchronized 该关键字 最后也是调用了 storeFence方法
                //通过 内存屏障 实现 可见性
    //            System.out.println(count);
    
    //            LockSupport.unpark( Thread.currentThread());
    //
    //            try {
    //                Thread.sleep(1000);
    //            } catch (InterruptedException e) {
    //                e.printStackTrace();
    //            }
    
                //总结 java保证 可见性 分为两种方式
                // 1、 内存屏障 jvm层面的 storeLoad内存屏障  === > X86 lock 替代了 mfence
                // 2、线程上下文切换Thread.yield();
    
            }
            System.out.println(Thread.currentThread().getName() + "跳出循环: i=" + i);
        }
    
    
        public static void shortWait(long interval) {
            long start = System.nanoTime();
            long end;
            do {
                end = System.nanoTime();
            } while (start + interval >= end);
        }
    
        public static void main(String[] args) throws Exception {
            VisibilityTest visibilityTest = new VisibilityTest();
    
            Thread threadA = new Thread(() -> visibilityTest.load(), "ThreadA");
            threadA.start();
    
            Thread.sleep(1000);
    
            Thread threadB = new Thread(() -> visibilityTest.refresh(), "ThreadB");
            threadB.start();
        }
    }
    

    一、运行上述代码结果如何?

            从代码的写法上应该得到的结果是,运行一段时间后,程序会结束while 循环,并且打印出:跳出循环。但是当我们实际运行上面 结果的时候,程序会直接进入死循环,并不会结束。这就是我们常说的共享变量的可见性导致的,线程B对flag的修改,线程A并不能感知到。这样的代码在如果出现在实际业务中就会导致严重的bug。

            又JMM模型我们知道,这是因为在线程运行的时候,将flag变量都加载到了各自线程的本地内存中,而两个线程之间的通讯是通过主内存,所以我们如果想让线程B修改的变量的值,让线程A及时感知到,这就需要线程B对变量的修改及时刷新到主内存,并且其他线程本地内存对该变量的缓存失效。

    二、解决方案

    2.1 等待

    我们从常理来推测,如果我们程序在短时间内不使用这个flag变量,理论上计算机会定时将变量从缓存中移除,毕竟每个线程的本地内存空间并不是很大,并且从一些常用的内存清理中间件来分析大概都是这个原理,所以我们在whil循环中增加 等待1ms,运行结果:程序跳出while 循环,达到可见性目的,这也说明计算机会将最近不是使用的变量从本地内存清除。具体等待多久才会达到这个效果和具体的硬件有关系,没有具体详细精准的时间。

    2.2 线程上下文切换

    这钟方式典型的用法就是 Thread.yield(),让当前线程从运行态变成就绪态,交出cpu使用权。基本线程切换的时间大概为5ms-10ms之间,线程上下文切换会导致当前线程本地内存失效,当该线程再次获得CPU使用权的时候,从程序计数器中获得下一条执行的指令,并且从主内存中加载变量值,这种方式也解决了 可见性问题。

    2.3 volatile关键字

    这种方式是我们最常见的一种解决方式,那么为什么volatile关键字可以解决这个问题呢?因为这是jdk中自带的一个关键字,所以我们需要查看jdk源码才可以更好的理解

     从源码中我们可以发现,该关键字调用了storeload()方法,从这个方法命我们就可以知道,这里开始调用内存屏障的实现了。

    inline void OrderAccess::storeload()  { fence(); }
    inline void OrderAccess::fence() {
      if (os::is_MP()) {
        // always use locked addl since mfence is sometimes expensive
    #ifdef AMD64
        __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
    #else
        __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
    #endif
      }

    以上代码中在x86处理器上的实现,在该处理其中利用 lock 实现类似内存屏障的效果,这种lock的实现方式效率更高。

    lock前缀指令的作用

    1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。

    2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。

    3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。

    汇编层面的lock实现

    我们可以通过增加jvm参数来观察

    -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp

     从验证了可见性是用来了lock指令

    其余的几种方式  System.out.println(count); 底层使用了 synchronized关键字,源码最后也是调用了storeFence方法,也是利用内存屏障来实现了可见性。包括Thead.sleep()等

    2.4 将count 类型从int 更换为integer

    这种方式其实是利用了final关键字,如果我们看过intege 源码定义的话,我们最后其实获得的是value值,而在integer中value 是通过final关键字定义的,所以说final关键字也可以解决可见性问题。

    总结

    所以我们将上面的各种解决方案进行总结,

    如何保证可见性

    • 通过 volatile 关键字保证可见性。
    • 通过 内存屏障保证可见性。
    • 通过 synchronized 关键字保证可见性。
    • 通过 Lock保证可见性。
    • 通过 final 关键字保证可见性

    其实上面的这几种方式,从底层可以分文两个大的方案;

            1)线程上下文切换

            2)内存屏障   jvm层面的 storeLoad内存屏障 === > X86 lock 替代了 mfence

            

    展开全文
  • 在java并发编程中,我们一般会说需要满足以下个基本条件才能构成java并发的基础,这特性分别是:有序性、可见性、原子性。那么这特性到底是一个怎么样的概念呢,下面我们将通过一些例子来详细说明一下这几...

    在java并发编程中,我们一般会说需要满足以下三个基本条件才能构成java并发的基础,这三个特性分别是:有序性、可见性、原子性。那么这三个特性到底是一个怎么样的概念呢,下面我们将通过一些例子来详细说明一下这几个概念。

    有序性:有序性是指在java并发中,如果执行编码的两条指令按照代码的先后顺序执行。设想一下,如果代码的执行顺序和我们看到的代码顺序不一样,那很可能会导致结果错乱。可为什么会导致代码执行和我们编写看到的不一样呢?这是因为编译器,处理器为了提升执行效率会对代码编译过后的执行指令进行重新排序(指令重排序)。在as-if-serial中规定了执行重排序一定要满足执行的结果不受指令排序的影响,编译器和处理器都会遵守这个规则,但是as-if-serial是针对单线程的,它只定义了单线程下指令重排序不能影响执行结果,对于多线程的情况下,有可能会导致执行结果不可预料。看下下面的demo,一个比较经典的双重检查单例:

     public class Singleton {  
    
         private static Singleton singleton;  
    
         private Singleton() {  
    
         }  
    
       
    
         public static Singleton newInstance() {  
    
             if (singleton == null) {  
    
                 synchronized (Singleton.class) {  
    
                     if (singleton == null) {  
    
                         singleton = new Singleton();  
    
                     }  
    
                 }  
    
             }  
    
             return singleton;  
    
         }  
    
     }  

    上面这个单例写法在极端情况下可能会导致问题,singleton不可用。原因在于:假设一个线程A执行第10行的时候,初始化一个singleton对象,如果了解java对象创建的同学应该知道,一个对象的创建有几个关键的步骤:1.申请内存空间,2.初始化对象 3.将栈中singleton指向对象的地址;在这里编译器为了优化性能,可能会将2和3进行交换一下执行,这个在单线程的情况下不会有任何问题,但是在多线程的条件下,假设在线程A执行第三步的时候,线程B执行到第7行,发现这个对象不是null,就直接返回了,这个时候,该对象还没有初始化,那么久会导致线程B获取到的对象不可用。

    上面是一个比较经典的指令重排序导致多线程问题,所以在多线程的情况下,有序的按照我们写的代码的执行是一个基本的前提条件。

    可见性:在java语言中,每个线程都保留了一份用到的变量在本地缓存,而在主存中也有一份,其示意图如下:

    图片

     

    如何保证线程A修改了变量值,线程B也能立马感应到,这就是可见性要描述的问题,在多线程的环境下,如果一个线程的本地副本值修改了,另一个线程能立马感知到这个变化,并且使用的是变化后的值,那么这样就构成了多线程的第二个基本概念:可见性。

    原子性:第三个特性是指在执行一条指令的时候,对其他线程而言,要么执行成功,要么执行失败,没有中间状态,并且在执行的时候,其他线程是不能打断其执行的。这也有一个比较经典的例子:

    int i=0;//只有这个是原子操作 1) 
    
    int b=i;  // 2)
    
    i++;  // 3)
    
    i=i+1; // 4) 

    上面有四行代码,只有第一行是原子性操作,在处理器中,它们能够保证从内存中读写单个字节是原子的,换句话说当一个核cpu在处理这块内存地址的时候,其他cpu是不能访问这个地址的,对于更复杂的操作,处理器能够提供总线锁定和缓存锁定来保证原子性,像第一行的代码,i =0;处理器提供一些相关的指令来保证原子性操作,XADD,BTS,CMPXCHG等,我们只需要调用这些指令就能保证这些操作一定是原子性操作,对于java程序员而言,我们需要了解的是CAS操作就是通过CMPXCHG指令实现原子性的。从这里可以看到,如果一个线程在执行的过程中,被另一个线程打断或者修改内存中的值,那就可能导致结果不正确。这就是构成java多线程并发的第三个基础:原子性。

    当我们了解了java多线程是怎么一回事,了解了底层是怎么保证线程并发的可靠时,我们在看下这三个基本特性,就能明白理解这三个基本特性对理解java并发有多大的意义。

    想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈。

                                                                                 

    展开全文
  • 并发编程的学习中,并发三特性是我们肯定会遇到的知识点,在面试中,也会常常被问到,那么并发特性到底包括哪些呢?我们如何能够快速理解它,今天的博客将会带大家有一个彻底的了解! 1.1 串行、并行和...

    前言

    在并发编程的学习中,并发三大特性是我们肯定会遇到的知识点,在面试中,也会常常被问到,那么并发的三大特性到底包括哪些呢?我们如何能够快速理解它,今天的博客将会带大家有一个彻底的了解!

    1.1 串行、并行和并发的区别

    在了解并发三大特性之前,我们需要先了解一下串行、并行和并发之间的区别,它们之间的区别到底是什么呢?快来看看吧!

    串行:在时间上不可能发生重叠,前一个任务没搞定,下一个任务只能等着

    并行:在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行 (多个CPU同时执行)

    并发:允许两个任务彼此干扰,同一时间点,只有一个任务运行,交替执行 (时间片轮转)

    如果你觉得文字解释不够直观,没关系,画个简图来辅助理解

    串行:1 --> 2 --> 3
    并行:1 
               2
    并发:1--> 2 --> 1 --> 2    
    

    1.2 并发的三大特性

    并发的三大特性:原子性、可见性和有序性

    注意:需要保证三大特性才能够保证线程安全

    1.原子性
    1-1 原子性概念
    • 原子性是指在一个操作中,CPU不可以在中途暂停,然后再调度 (即不被中断操作,要么全部执行完成,要么都不执行)

      就好比转账的过程,从账户A向账户B转1000元,那么必然包括两个操作: 首先从账户A减去1000元,然后往账户B加上1000元,两个操作必须全部完成

    1-2 案例解释

    例如在单核CPU中有T1和T2两个线程,它们分别来执行add方法,T1线程在执行该方法时,CPU进行调度,切换T2线程来执行该方法,这就是CPU进行调度来处理并发

    private int i = 0; //i变量初始值为0
    //一个add自增方法
    public void add() {
        //i执行加1运算
        i++;
    }
    

    如果要保证该方法是原子性的,即要么都成功,要么都失败,因此不能让T1线程在执行时被CPU进行切换,而是让它直到执行完该方法或者事务时才能退出

    虽然只是执行了一个简单add方法,但其实在CPU中经历了四步操作

    • 将i从主存读取到工作内存中的副本中
    • i变量执行加1的运算
    • 将结果写入工作内存
    • 将工作内存的值刷回主存 (什时候刷入由操作系统决定,不确定的)
    1-3 具体执行步骤

    比如说有一个i变量,现在它在主存中,并且i的初值为0

    1. 如果有两个线程T1和T2,它们会分别从主存中把i变量值读取到工作内存中 (这里其实是对i变量值的一个复制,T1和T2都会有一个自己的工作内存)
    2. 然后分别开始进行加1的操作(即i值变为1),将i=1的结果写入到T1和T2的工作内存中(前三步操作都是确定的,而最后一步操作是不确定的)
    3. 最后将i=1的值再写回到主存中去(由操作系统来决定,不确定时间)

    总结:程序中的原子性是指最小的操作单元,比如自增操作,它本身其实不是原子性操作,分了三步,包括读取变量的原始值,进行加1操作,写入工作内存

    1-4 常规并发操作

    如果是常规的并发操作,其具体执行步骤如下

    1. T1线程先从主存中将i=0的复制值读取到工作内存中,但这时CPU进行调度,切换到T2线程来执行;
    2. T2线程首先将i=0的复制从主存中读取到工作内存中,然后执行加1操作,即i值变为1,将i=1存入到T2的工作内存;
    3. 再切换到T1线程,T1还没有进行加1操作,这时T1的i=0的值被写回到主存,这样肯定就导致其结果错误了
    1-5 保证add方法原子性

    如果保证add方法是原子性的,具体执行步骤如下

    1. T1线程先从主存中将count=0的复制值读取到工作内存中,然后执行加1操作,count值变为1,将count=1存入到T1的工作内存;
    2. 然后再T2线程再执行此方法,重复与T1相同的操作,T2线程在其工作内存中的count值也为1
      让前三步一块执行,这样就保证了原子性;
    3. 但还是由于最后一步何时count值从工作内存中读入主存不确定,因此仅凭原子性,还是无法保证线程安全

    总结

    • 因此在多线程中,有可能一个线程还没自增完,可能才执行到第二步,另一个线程就已经读取了值,导致结果错误;
    • 那如果保证自增操作是一个原子性操作,那么就能保证其他线程读取到的一定是自增后的数据
    • 使用synchronized同步锁可以同时保证并发的三大特性:即原子性,可见性和有序性
    2.可见性
    2-1 可见性概念

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改值

    2-2 案例解释
    • 案例解释1

      例如前面的i自增问题,但两个线程在不同的CPU,那么线程T1改变了了i的值还没有刷新到主存,线程T2又使用了i变量,那么这个值肯定还是之前的,线程T1对变量的修改线程T2没有被看到,这就存在可见性问题

    • 案例解释2

    //线程T1
    //设置标志位stop初始值为false
    private boolean stop = false;
    while(!stop) {
       doSomething();
    }
    
    //线程T2
    //T2修改标志位值为true
    stop = true;
    

    请思考一个问题:如果线程T2改变了stop中的值,那么线程T1一定会停止吗?

    答案是不一定,当线程T2更改了stop变量的值后,但是还没来得及写入主存中,线程T2就转去做其他事情了,那么线程T1由于不知道线程T2对stop变量的更改,因此还会一直循环下去

    2-3 怎样才能保证可见性呢?

    要保证可见性,就涉及操作系统中的两个协议:总线Lock (锁定) 协议 和 MESI (缓存一致性) 协议

    我们还是结合前面的i自增案例对可见性进行解释

    • 假设有T1和T2两个线程,我们保证其可见性,但不保证原子性;那么还是前三步还是会存在CPU调度和线程切换,T1线程从主存中读取到i=0值的副本;这时CPU进行调度,切换到T2线程,T2也从主存读取到的i=0的复制值 (i=0的值是在T2的工作内存中的);

    • 再次调度切换到T1线程,T1执行加1操作,这时CPU进行调度,又切换到T2线程,但是此时i=0的值会失效(只要T1做加1操作时T2中的i=0就会失效,这是由于可见性在起作用),而T2线程可能还是会执行加1的操作

    • 当再次切换到T1线程时,T1中的i=1变量值被读取到其工作内存中,然后工作内存(缓存)中的i=1又马上被刷新主存中去,但是这两步是连续进行的,即符合原子性原则 (而这是总线锁定和缓存一致性保证的)

    总结

    • 因此,如果要想保证线程安全,就需要同时保证原子性和可见性

    即在上述的T1和T2线程并发执行的过程中,我们通过原子性将前三步进行绑定,而通过可见性又将第三步和第四步进行绑定,实际上相当于将这四步操作进行绑定,整体变成了一个原子操作

    • 在Java中,为了保证 i++的线程安全问题,可以使用AtomicInteger来定义i变量

      什么是AtomicInteger

      AtomicInteger是一个支持原子操作的int封装类,提供了原子性的访问和更新操作

      为什么使用AtomicInteger可以保证线程安全

      AtomicInteger之所以可以保证线程安全性,是因为它底层是通过volatile和CAS(Compare
      And swap,即比较并交换)实现的;volatile可以保证内存可见性和有序性,但保证不了原子性,而CAS可以保证原子性

    • 使用volatile、synchronized和final关键字 (volatile除了保证有序性外,还可以保证可见性,final可以保证可见性)

    3. 有序性
    3-1 有序性概念
    • 有序性是指虚拟机在进行代编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码顺序来执行,有可能将它们重新排序;
    • 实际上,对于有些代码重新排序后,虽然对变量的值没有造成影响,但又是可能会出现线程安全问题
    3-2 案例解释

    比如本来代码执行的顺序是1234,但是经过编译重排后变成了2134,无论执行顺序是1234还是2134,在单线程下结果不会发生变化,才会进行重排,但如果发生重排后结果会发生变化,那它不会进行重排

    //变量a初始值为0
    static int a = 0; 
    //标志位初始值为false
    static boolean flag = false;
    
    //写入方法
    public void write() {
        //将变量a赋值为2
    	a = 2; // 第一步
    	//将标志位置为true
    	flag = true; //第二步
    }
    
    //乘值方法
    public void multiply() {
        //判断标志位是否为真
    	if(flag) { //第三步
    	   //如果标志位为真,进行平方运算
    	   int result = a * a; //第四步
    	}
    }
    
    

    在单线程情况下

    在单线程下,如果将第一步和第二步顺序进行交换,也就是将标志位flag先置为true,再将变量a赋值为2,与原顺序相比,其执行结果并不会受到影响,因此不会造成线程不安全问题

    在多线程情况下

    • 如果是多线程情况下,有一个T1线程和T2线程,还是将第一步和第二步的顺序进行交换,如果T1线程先执行第二步,将标志位flag置为true;

    • 如果这时CPU进行调度,切换到T2线程,T2线程可能不会去执行write方法,而是去执行multiply方法;

    • 这时T1线程已经将标志位flag改为true,所以T2就可以直接执行平方运算,然后result的值经过运算就变成了0,显然与预期的结果值4 (2*2) 不一致,因此在多线程下可能会存在线程不安全问题

    关键字:使用volatile、synchronized

    • 那么要怎样使程序编译时不会发生指令重排呢?

    为了让a=2和flag=fals按照我们所写的顺序来执行,可以使用synchronized同步锁来修饰write方法

    • 那么可以使用volatile进行修饰来解决该问题吗?

    不可以,因为这里的方法存在两个成员变量,即a和flag,而volatile只能修饰一个成员变量

    • 那么什么时候可以使用volatile呢?

    比如我们在new一个User对象的时候,首先在堆中申请一块内存,然后给内存中的属性赋值,最后赋值给栈中的变量,多线程情况下,可能User对象申请完内存后,还没来得及赋值,就被线程给调用了,这时候我们可以使用volatile来修饰这个对象

    好了,今天有关并发三大特性的学习就到此结束了,欢迎大家学习和讨论!


    参考视频链接
    https://www.bilibili.com/video/BV1Eb4y1R7zd (B站UP主程序员Mokey的Java面试100道)

    展开全文
  • JAVA并发三特性

    2021-03-28 23:23:20
    1、原子性 2、可见性 3、有序性
  • 并发三特性

    2021-02-19 14:27:12
    并发三特性: 原子性:   原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。可以通过 synchronized和Lock实现原子性,volatile不能保证原子性。 有序性:   ...
  • 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronize可以保证代码片段的原子性。 可见性 :当一个变量对共享变量...
  • 并发编程特性(原子性、可见性、有序性) 并发编程特性的定义和由来 凡事有因才有果,有果必有因,并发编程的特性也如此,人们不会莫名其妙定义出并发编程的特性。接下来我们探讨下为什么会有并发...
  • 并发编程中,我们通常会遇到以下个问题:原子性问题,可见性问题,有序性问题。 1、原子性 即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 一个很经典的例子...
  •   并发特性:原子性、可见性、有序性。 1、原子性   原子性是指子一个操作中CPU不可调度,即不可被中断,要么不执行此操作,要么全部执行完此操作;   原子性指的是最小的操作单元。比如自增操作,它...
  • Java并发编程的个重要特性分别是原子性 有序性和可见性 ,也被称为线程安全问题。非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况。 原子性 原子(Atomic)就是不可分割...
  • swoole提升性能的几个特性。提高性能1·系统常驻内存基于swoole的程序是常驻内存的程序,在启动时就将系统框架等代码读取并编译完成,不需要每次请求时都执行编译的步骤,可以大大的降低脚本的运行时间。2·连接池...
  • Java并发编程特性

    2021-01-21 13:46:43
    并发特性 可见性 当多个线程同时具有一个数值的访问权限时其中一个线程修改了其数值,其他线程要及时可见 有序性 由于程序不一定会按顺序执行,所以要求并发编程中需要有序的执行业逻辑代码 原子性 并发编程中...
  • pytest运行参数 -n用于并发运行多个测试用例,该并发运行是进程级别的并发测试,使用前,需安装pytest-xdist插件。其中 -n=auto表示使用服务器所有的CPU来运行测试用例,举例如下: import time import pytest ...
  • 多线程的特性

    2021-12-14 20:34:10
    什么是上下文切换 并行和并发的区别 并发三特性(一) 可见性 并发三特性(二) 原子性 并发三特性) 有序性 volatile介绍 jvm层面内存屏障 ​
  • 并发是什么呢? 并发指的是Java中有多线程并发运行。 为什么会有并发? 因为一个线程在执行的过程中不仅会用到cpu资源,还会用到IO,也就是输入输出。IO的速度远远比不上CPU的运算速度。...Java并发编程有特性
  • 在秒杀,抢购等并发场景下,可能会出现超卖的现象,在 PHP 语言中并没有原生提供并发的解决方案,因此就...利用数据库事务特征,做原子更新,此方法需要依赖数据库的事务特性。 借助文件排他锁,在处理下单请求的时候,
  • 串行 :在时间上不可能发生重叠,前一个任务没搞完,下一个任务就只能等着 并行: 在时间上是重叠的,两个任务在同一个时刻互不干扰的同行执行 并发: 允许两个任务彼此...并发特性 原子性、可见性、有序性 ...
  • Oracle的并发技术可以将一个大任务分解为多个小任务由多个进程共同完成。合理地使用并发可以充分利用系统资源,提高效率。一、 并发的种类Parallel queryParallel DML(PDML)Parallel DDLParallel recovery[@more@]二...
  • 一、进程与线程、 二、并发、线程间通信、 四、Java 并发 3 特性
  • Java并发编程(23):并发特性—信号量Semaphore(含代码),新特性semaphore在操作系统中,信号量是个很重要的概念,它在控制进程间的协作方面有着非常重要的作用,通过对信号量的不同操作,可以分别实现进程间的互斥...
  • 数据库事务有ACID四种特性,多线程同样有特性。 不同的数据库支持不同的事务,支持不同的特性。 同样的,多线程由使用者来保证特性。 原子性 要么一起成功,要么一起失败。 银行转账案例: 账户A向账户B转...
  • 数据库四大特性

    千次阅读 2021-03-03 23:50:02
    数据库系统必须维护事务的以下特性(简称ACID):原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability)⑴ 原子性(Atomicity)原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此...
  • 并发系统设计的大目标:高性能、高可用、可扩展 高并发,是指运用设计手段让系统能够处理更多的用户并发请求,也就是承担更大的流量。它是一切架构设计的背景和前提,脱离了它去谈性能和可用性是没有意义的。很...
  • 在计算机科学中,并发性(Concurrence)是指在一个系统中,拥有多个计算,这些计算有同时执行的特性,而且他们之间有着潜在的交互。因此系统可进行的运行路径会有相当多个,而且结果可能具有不确定性。并发计算可能会...
  • 实验 观察Linux进程线程的异步并发执行 一、实验目的 通过本实验学习如何创建Linux进程及线程,通过实验,观察Linux进程及线程的异步执行。理解进程及线程的区别及特性,进一步理解进程是资源分配单位,线程是独立...
  • volatile关键字介绍一:线程安全的特性1、原子性2、可见性3、有序性二:volatile关键字 一:线程安全的特性 1、原子性 原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须...
  • 目录前言一、并发控制概述二、封锁、封锁协议...事务是并发控制的基本单位,保证事务ACID特性是事物处理的重要任务,而事务的ACID特性可能遭到破坏的原因之一是多个事务对数据库的并发操作造成的。为保证事务的隔离性
  • mysql 并发 行锁 inoodb

    2021-01-20 21:25:40
    http://www.cnblogs.com/hitwtx/archive/2011/09/12/2174297.html1. 锁是数据库区别于文件系统的重要特性之1,锁机制用于管理对共享文件的并发访问innodb使用的是行级锁myisam,使用的是表级锁,在并发条件下,读...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 387,198
精华内容 154,879
关键字:

并发三特性