java并发编程之原子性、可见性、有序性

目录
  • 1原子性
    • 1.1java中的原子性操作
  • 2可见性
    • 2.1可见性问题
    • 2.2解决可见性问题
  • 3有序性
    • 3.1单个线程内程序的指令重排序
    • 3.2多线程内程序的指令重排序
    • 3.3保证有序性的解决方法
    • 3.4volatile保证有序性的原理
  • 4实例分析
    • 4.1原理分析
    • 4.2synchronized结合
    • 4.3Lock结合
    • 4.4使用AtomicInteger替换int

在java中,执行下面这个语句

int i =12;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存(物理内存)当中。

1 原子性

定义:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

int i =12;

假若一个线程执行到这个语句时,暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。
那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

1.1 java中的原子性操作

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

例如:

int x = 10;     //语句1
int y = x;     //语句2
x++;           //语句3
x = x + 1;     //语句4

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中,所以是原子性操作。

语句2实际上包含2个操作,它先要去读取x的值,再将y的值写入主存,虽然读取x的值以及 将y的值写入主存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
语句3 语句4 同理,先将x的值读取到高速缓存中,然后+1赋值后,再写入到主存中。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

2 可见性

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

2.1 可见性问题

例如:

//线程1
int i =12;
i=13;

//线程2
int j=i;

假若执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行 i =13这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为13,那么在CPU1的高速缓存当中i的值变为13了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是12,那么就会使得j的值为12,而不是13。

这就是可见性问题,也就是说 i 的值在线程一中修改了,没有通知其他线程更新而导致的数据错乱。

2.2 解决可见性问题

Java提供了volatile关键字来保证可见性。

也就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

3 有序性

定义:即程序执行的顺序按照代码的先后顺序执行。

3.1 单个线程内程序的指令重排序

例如:

int i = 0;
boolean flag = false;
i = 1;                //语句1
flag = true;          //语句2

按照我们日常的思维,程序的执行过程是从上至下一行一行执行的,就是说按照代码的顺序来执行,那么JVM在实际中一定会这样吗??? 答案是否定的,这里可能会发生指令重排序(Instruction Reorder)。

指令重排序(Instruction Reorder 是指: 处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

需要注意的是:处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

3.2 多线程内程序的指令重排序

重排序不会影响单个线程内程序执行的结果,但是多线程就不一定了。

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

3.3 保证有序性的解决方法

在Java里面,可以通过volatile关键字来保证一定的“有序性”。
当然可以通过synchronizedLock来保证有序性,很显然,synchronizedLock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

3.4 volatile 保证有序性的原理

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性,也就是说:

当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

4 实例分析

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

一般说来 有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。但实际中并不是这样,进行过测试后会发现,每次执行结束后,得到的都是一个比10000要小的值。

4.1 原理分析

自增操作是不具备原子性的,它包括读取变量的原始值到高速缓存中、进行加1操作、写入主存中这三个过程。
也就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,
此时 变量inc的值还没有任何改变,此时线程2拿到的值也为10,然后进行加1操作,然后将值11写入到主存中,
然后线程1继续进行加1操作 这里线程1中 inc的值依然为10,进行加1操作,然后将值11写入到主存中

那么两个线程分别进行了一次自增操作后,inc只增加了1。

4.2 synchronized 结合

public class Test {
    public  int inc = 0;

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

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

4.3 Lock 结合

public class Test {
    public  int inc = 0;
    Lock lock = new ReentrantLock();

    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

4.4 使用AtomicInteger替换int

public class Test {
    public  AtomicInteger inc = new AtomicInteger();

    public  void increase() {
        inc.getAndIncrement();
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

到此这篇关于java并发编程之原子性、可见性、有序性 的文章就介绍到这了,更多相关java并发编程之原子性、可见性、有序性 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 了解Java多线程的可见性与有序性

    多线程的可见性 一个线程对共享变量值的修改,能够及时的被其他线程看到. 共享变量 如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量. Java内存模型 JMM(Java Memory Model,简称JMM)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节.它遵循四个原则: 所有的变量都存储在主内存中 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝

  • Java多线程的原子性,可见性,有序性你都了解吗

    目录 1.原子性问题 2.可见性问题 3.有序性问题 总结 问题: 1.什么是原子性.可见性.有序性? 1. 原子性问题 原子性.可见性.有序性是并发编程所面临的三大问题. 所谓原子操作,就是“不可中断的一个或一系列操作”,是指不会被线程调度机制打断的操作.这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换. 例如对于 i++ 而言,实际会产生如下的 JVM 字节码指令: getstatic i // 获取静态变量i的值(内存取值) iconst_1 // 准备常量1 iadd //

  • Java并发之原子性 有序性 可见性及Happen Before原则

    1.原子性(Atomicity) 原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响.由Java内存模型来直接保证的原子性变量操作包括read.load.assign.use.store和write这六个,我们大致可以认为,基本数据类型的访问.读写都是具备原子性的(例外就是long和double的非原子性协定).如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock

  • Java 并发编程的可见性、有序性和原子性

    并发编程无论在哪门语言里,都属于高级篇,面试中也尝尝会被问到.想要深入理解并发编程机制确实不是一件容易的事,因为它涉及到计算机底层和操作系统的相关知识,如果对这部分知识不是很清楚可能会导致理解困难. 在这个专栏里,王子会尽量以白话和图片的方式剖析并发编程本质,希望可以让大家更容易理解. 今天我们就来谈一谈可见性.有序性和原子性都是什么东西. 并发编程的幕后 进入主题之前,我们先来了解一下并发编程的幕后. 随着CPU.内存和I/O设备的不断升级,它们之间一直存在着一个矛盾,就是速度不一致问题.CP

  • Java多线程 原子性操作类的使用

    目录 1. 基本类型的使用 2. 数组类型的使用 3. 引用类型的使用 4.字段类型的使用 前言: 在java5以后,我们接触到了线程原子性操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,本文总结一下Atomic系列的类的使用方法,其中包含: 1. 基本类型的使用 public class AtomicTest { /** * 常见的方法列表 * * @see AtomicInteger#get() 直接返回值 * @see AtomicIn

  • java并发编程之原子性、可见性、有序性

    目录 1原子性 1.1java中的原子性操作 2可见性 2.1可见性问题 2.2解决可见性问题 3有序性 3.1单个线程内程序的指令重排序 3.2多线程内程序的指令重排序 3.3保证有序性的解决方法 3.4volatile保证有序性的原理 4实例分析 4.1原理分析 4.2synchronized结合 4.3Lock结合 4.4使用AtomicInteger替换int 在java中,执行下面这个语句 int i =12; 执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写

  • Java并发编程之原子性-Atomic的使用

    目录 线程安全 线程安全主要体现在以下三个方面 JUC中的Atomic包详解 总结 线程安全 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类时线程安全的. 线程安全主要体现在以下三个方面 原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作 可见性:一个线程对主内存的修改可以及时的被其他线程观察到 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观

  • java并发编程关键字volatile保证可见性不保证原子性详解

    目录 关于可见性 关于指令重排 volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解 相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点 先说它的两个作用: 保证变量在内存中对线程的可见性禁用指令重排 每个字都认识,凑在一起就麻了 这两个作用通常很不容易被我们Java开发人员正确.完整地理解,以至于许多同学不能正确地使用volatile 关于可见性 不多bb,码来 public

  • Java并发编程-volatile可见性详解

    前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 <br>本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文. 什么是可见性? 把JAVA内存模型(JMM)展示得很详细了,简单概括一下 1.每个Thread有一个属于自己的工作内存(可以理解为每个

  • Java 并发编程:volatile的使用及其原理解析

    Java并发编程系列[未完]: •Java 并发编程:核心理论 •Java并发编程:Synchronized及其实现原理 •Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) •Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) •Java 并发编程:volatile的使用及其原理 一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchroniz

  • Java并发编程volatile关键字的作用

    日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再深入一点也许是听闻 volatile 只能保证可见性而不能保证原子性,无法有效保证线程安全,于是更加避免使用 volatile ,简简单单加上synchronize关键字就完事了.本文稍微深入探讨 volatile 关键字,分析其作用及对应的使用场景. 并发编程的几个概念简述 首先简单介绍几个与并发编程相关的概念: 可见性 可见性是指变量在线程之间是否可见,JVM 中默认情况下线程之间不具备可见

  • Java并发编程——volatile关键字

    一.volatile是什么 volatile是Java并发编程中重要的一个关键字,被比喻为"轻量级的synchronized",与synchronized不同的是,volatile只能修饰变量,无法修饰方法及代码块等. 下面是使用volatile关键字实现的单例模式: public class Singleton implements Serializable { private static volatile Singleton singleton; private Singleto

  • Java并发编程之线程之间的共享和协作

    一.线程间的共享 1.1 ynchronized内置锁 用处 Java支持多个线程同时访问一个对象或者对象的成员变量 关键字synchronized可以修饰方法或者以同步块的形式来进行使用 它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中 它保证了线程对变量访问的可见性和排他性(原子性.可见性.有序性),又称为内置锁机制. 对象锁和类锁 对象锁是用于对象实例方法,或者一个对象实例上的 类锁是用于类的静态方法或者一个类的class对象上的 类的对象实例可以有很多个,但是每个类只有

  • Java并发编程之关键字volatile知识总结

    一.作用 被 volatile 修饰的变量 1.保证了不同线程对该变量操作的内存可见性 2.禁止指令重排序 二.可见性 Java 内存模型(Java Memory Model) 是 Java 虚拟机定义的一种规范,即每个线程都有自己的工作空间,线程对变量的操作都在线程的工作内存中完成,再同步到主存中,这样可能会导致不同的线程对共享变量的操作,在各自线程工作空间内不一样的问题. 而用 volatile 修饰的变量,线程对该变量的修改,会立刻刷新到主存,其它线程读取该变量时,会重新去主存读取新值.

随机推荐