天天看点

Java并发编程知识点总结(七)——原子性、有序性、可见性

文章目录

    • (一)、原子性
        • 1.1 原子性的概念
        • 1.2 原子性操作
        • 1.3 synchronized和volatile对比
    • (二)、有序性
    • (三)、可见性

(一)、原子性

1.1 原子性的概念

原子性指的是一个操作是不可中断的,要么全部执行成功,要么全部执行失败。

在多线程环境中,如果一个线程开始了操作,那么其他线程就不能对这个线程有干扰。

int a = 10;//1
    a++;//2
    int b = a;//3
    a = a + 10;//4
           

分析上面的四行代码,其实只有第一行是具有原子性的,直接将10赋值给a。剩下的三行都不具有原子性。例如第二行的a++操作,其实是分为3步的。1)、将a的值读取到工作内存中 2)、将a执行+1操作 3)、将计算的值写回a中。这三个操作就无法构成原子性了。3和4也是同样的道理。

1.2 原子性操作

Java内存模型中定义了一下8个操作是原子性的。

  1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
  2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
  4. load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
  5. use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  6. assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
  8. write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

    注意:java内存模型只要求上述操作是顺序执行的,而不一定是连续执行的

    long和double不是原子性操作

1.3 synchronized和volatile对比

synchronized
           

上面的8种基本操作包括了6种满足基本读写的原子性操作,还剩下lock和unlock两个原子操作。synchronized则使用更高层次的指令monitorenter和monitorexit进行加锁和解锁,因此synchronized是满足原子性的。

volatile
           
public class Thread15 {

    static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 10; i++) {
            Thread thread = new Thread(){
                @Override
                public void run() {
                    for(int i = 0; i < 1000; i++) {
                        count++;
                    }
                }
            };
            thread.start();
        }
        Thread.sleep(5000);
        System.out.println(count);
    }
}

           
Java并发编程知识点总结(七)——原子性、有序性、可见性

我们根据上面的代码来看,虽然共享变量count被修饰为volatile,但结果却不是预计的10000。这其实就可以说明volatile是无法保证原子性的。 因为count++并不是原子性的。如果A线程读取count到工作内存,B线程已经完成了自增的操作,那么A线程读取的值就是过期的值。所以volatile是无法保证原子性的。

如果想让volatile拥有原子性:

  1. 运算结果的值不依赖于变量的当前值或者只有一个线程修改该变量的值
  2. 变量不需要与其他的状态变量参与不变约束。例如low<up这样的场景就不行,例如
volatile boolean judge = false;
    
    public void a(){
        judge = true;
    }
    
    public void b() {
        while(judge == false) {
            //do something
        }
    } 
           

(二)、有序性

synchronized
           

Synchronized关键字只允许一个线程访问同步代码块,其他线程只能阻塞等待,也就是说每个线程只能串行执行,因此Synchronized是具有原子性的。

volatile
           
private   Thread16() {}
    private volatile static Thread16 thread16;

    public Thread16 getThread16() {
        if(thread16 == null) {
            synchronized (Thread16.class) {
                if (thread16 == null) {
                    thread16 = new Thread16();
                }
            }
        }
        return thread16;
    }
           

上面是我们常见的双检验单例模式。

上面的代码其实分为3步操作:1)、分配内存空间 2)、初始化对象 3)、thread16指向内存空间

但是有可能出线重排序问题。

Java并发编程知识点总结(七)——原子性、有序性、可见性

如果如上图中,如果还没有初始化对象的时候就已经判断了是否thread16为null,也就是3操作在2操作之前执行,那么单例模式就会失败,如果不是使用双重检验的话。但是使用volatile修饰之后,可以禁止2和3的重排序,从而避免这种情况。由此可见,volatile是具有可见性的。

(三)、可见性

synchronized
           

通过之前的文章:Java并发编程知识点总结(四)——Synchronized实现原理以及优化,我们可以知道Synchonized在执行读写操作的时候会强制从主内存中读取数据,并且也会强制将写操作后的数据更新到主内存中。因此具有可见性。

volatile
           

通过之前的文章:Java并发编程知识点总结(五)——volatile实现原理,我们可以知道对于volatile的写操作,会导致其他处理器的缓存失效,从而强制从主内存中读取数据来更新数据,因此volatile是具有可见性的。

参考了这篇文章:三大性质总结:原子性,有序性,可见性

继续阅读