Java的原子性&&可见性&&有序性

Java的原子性&&可见性&&有序性

原子性

定义:

原子性:是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。

原子操作原理(处理器是如何实现原子操作的)

处理器实现原子操作有3种方式:

1. 处理器自动保证基本内存操作的原子性

首先说明,处理器会自动保证基本的内存操作是原子性的。处理器保证从系统内存中读取或写入一个字节是原子的。意思是,当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。

当然,longdouble类型在32位操作系统中的读写操作不是原子的,因为longdouble占64位,需要分成2个步骤来处理,在读写时分别拆成2个字节进行读写。因此longdouble类型的数据在进行计算时需要注意这个问题。

2. 使用总线锁保证原子性

如果多个处理器同时对共享变量进行读改写操作(如:i++),共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的。如图所示:

image

当多个处理器调度线程时,同时读取主内存中的共享变量,就会造成上面的问题。如果想上述操作是原子的,那么必须保证CPU1读改写共享变量时,CPU2不能做缓存该共享变量的操作。

处理器使用总线锁就是解决这个问题的。总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

java锁机制其实是锁总线。

3. 使用缓存锁保证原子性

总线锁巴CPU和内存之间通信锁住,使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁的开销比较大。所以在某些场合使用缓存锁替代总线锁。

缓存锁是指通过锁住CPU缓存,在CPU缓存区实现共享变量的原子性操作。

如果缓存在处理器的缓存行中,内存区域在LOCK操作期间被锁定,当它执行锁操作,回写主内存时,处理器不在总线锁上声言LOCK#信号,而是修改内部内存地址,并允许它的缓存一致性机制来保证操作的原子性。因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。

缓存锁使用的是比较并交换策略(Compare And Swap简称CAS),CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

java并发包中的原子类就是使用缓存锁机制。

在两种情况下处理器不会使用缓存锁。

  • 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。

  • 有些处理器不支持缓存锁定。


可见性

定义:

可见性是指:当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此。

普通变量与volatile变量的区别 
是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。

volatile关键字介绍

在java中提供了volatile关键字,通过volatile关键字修饰内存中的变量,该变量在线程之间共享。

volatile关键字是轻量级的锁(synchronized)。在使用的时候,消耗的成本比synchronized小很多。volatile用于修饰变量。

volatile实现原理

volatile修饰的变量,在翻译成汇编语言的时候,会有一个LOCK前缀的指令。

LOCK前缀的指令在多核处理器下会引发两件事情。

  1. 将当期处理器缓存行的数据会写回到系统内存。
  2. 这个写回内存的操作会引起在其他CPU里缓存该内存地址的数据无效。

如果对声明了volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

volatile的适用范围

volatile变量固然方便,但是存在着限制,volatile修饰的变量,并不能保证是原子操作的,所以多处理器操作数据时,会导致数据重复。所以volatile关键字通常被当作完成、中断的状态的标识使用

final关键字的可见性

与volatile相比较,final域的读和写更像是普通的变量访问。

对于final域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

    • 写final域的重排序规则
    • JMM禁止编译器把final域的写重排序到构造函数之外。

    • 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

    • 读final域的重排序规则

    • 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。


有序性

定义

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

Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

有序性的语意有几层, 
1. 最常见的就是保证多线程运行的串行顺序 
2. 防止重排序引起的问题 
3. 程序运行的先后顺序。比方JMM定义的一些Happens-before规则

synchronized是如何保证有序性的(synchronized同步原理)

JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

当使用synchronized关键字是,只能有一个线程执行直到执行完成后或异常,才会释放锁。所以可以保证synchronized代码块或方法只会有一个线程执行,保障了程序的有序性。


原子性&&可见性&&有序性实例

public class MyClass {

    private static volatile int count = 0;
    private static AtomicInteger atomicCount = new AtomicInteger(0);
    private static int synchronizedCount = 0;

    public static void main(String[] args) {
        //  分别调用,打印结果
        // volatileCount();
        //  atomicCount();
        synchronizedCount();
    }

    private static void volatileCount() {
        for (int i = 0; i < 10; i++) {
            Executors.newFixedThreadPool(3).execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 虽然使用volatile关键字修饰int变量,但是对于多线程的环境下,也很难保证没问题,所以一般用来修饰标志位
                    System.out.println("volatile count: " + ++count);
                }
            });
        }
    }
    //    打印结果:有重复数据,且顺序错乱。表示数据操作不是原子的,线程之间也不是有序的
    //    volatile count: 1
    //    volatile count: 5
    //    volatile count: 4
    //    volatile count: 3
    //    volatile count: 1
    //    volatile count: 2
    //    volatile count: 6
    //    volatile count: 7
    //    volatile count: 8
    //    volatile count: 9


    private static void atomicCount() {
        for (int i = 0; i < 10; i++) {
            Executors.newFixedThreadPool(3).execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 通过使用Atomic包中的原子类保证数据操作是原子的(数据没有重复,表示是原子操作),但是不能保障有序性
                    System.out.println("atomic count: " + atomicCount.incrementAndGet());
                }
            });
        }
    }
    //    打印结果:虽然顺序错乱,但是数据没有重复,也就说明保障了数据的操作是原子的,但是线程间不是有序的
    //    atomic count: 1
    //    atomic count: 2
    //    atomic count: 3
    //    atomic count: 5
    //    atomic count: 8
    //    atomic count: 9
    //    atomic count: 10
    //    atomic count: 4
    //    atomic count: 7
    //    atomic count: 6


    private static void synchronizedCount() {
        for (int i = 0; i < 10; i++) {
            Executors.newFixedThreadPool(3).execute(new Runnable() {
                @Override
                public void run() {
                    synchronized (MyClass.class) {  // 通过synchronized关键字来保证线程之间的有序性
                        System.out.println("synchronized count: " + ++synchronizedCount);
                    }
                }
            });
        }
    }
    //    打印结果:没有重复数据,也没有错乱现象,说明数据操作是原子的,同时线程操作也是顺序的。同时也说明,有序性可以保障数据操作的原子性
    //    synchronized count: 1
    //    synchronized count: 2
    //    synchronized count: 3
    //    synchronized count: 4
    //    synchronized count: 5
    //    synchronized count: 6
    //    synchronized count: 7
    //    synchronized count: 8
    //    synchronized count: 9
    //    synchronized count: 10
}

猜你喜欢

转载自blog.csdn.net/qq_33260318/article/details/81144333