volatile和synchronized关键字介绍

荀攸
• 阅读 662

背景

上篇文章介绍了java的53个关键字,其中个人感觉volatile和synchronized两个java关键字可以重点详细介绍下.这两个关键字都是作用在多线程并发环境下,其中volatile能保证操作对象的可见性和有序性,synchronized能保证操作对象的原子性和可见性.

JMM

多线程并发环境有必要先了解JMM(java memory model),在了解JMM前我们需要知道PC物理机的内存模型,如图:
volatile和synchronized关键字介绍
CPU处理指令的性能很高,而CPU直接从内存中读取数据的性能相对来说就很慢了,所以如果直接都从内存中读取数据,会严重拖慢PC的处理速度.所以才会有CPU缓存,一般都是3级缓存,级别越高CPU读取性能越高,CPU内存也有寄存器,它的读取性能是最高的.
JMM内存模型如图:
volatile和synchronized关键字介绍
注意JMM不是真实物理的内存结构,它是java虚拟机栈工作内存的规范.每个线程都有自己的工作内存,线程对所有变量的操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存.

可见性

当一个共享变量被修改时,它的值会立即更新到主内存,当有其他线程读取时,都会去主存中读取最新值.说明该变量是对所有线程是具有可见性.

有序性

java虚拟机的编译器和处理器会对指令进行重排序优化,来提升代码的执行效率.重排序会依据happens-before原则,保证指令代码的有序性.重排序不会影响单线程情况下的执行结果,但多线程并发的情况下可能会影响到它的正确性.所以并发情况下需要防止虚拟机对一定代码的重排序.

原子性

多个代码执行,要么同时都执行,要么都不执行,像原子一样不能被分割,即这些操作不可被中断,我们就说这些操作是具备原子性.

volatile

可见性代码例子

public class VisibilityDemo {
    static boolean flag = true;
    public static void main(String[] args) throws Exception {
        new Thread(()->{
            System.out.println("开始循环啦~~~");
            while(flag){

            }
            System.out.println("循环退出了~~~");
        }, "t1").start();

        Thread.sleep(2000);

        new Thread(()->{
            System.out.println("flag的值修改为false");
            flag=false;
        }, "t2").start();
    }
}

t1线程中的flag一直获取不到t2线程修改flag变量后的值所以一直在循环中,运行结果如下图:
volatile和synchronized关键字介绍

对变量使用volatile修饰就可以退出循环了.

public class VisibilityVolatileDemo {
    static volatile boolean flag = true;
    public static void main(String[] args) throws Exception {
        new Thread(()->{
            System.out.println("开始循环啦~~~");
            while(flag){

            }
            System.out.println("循环退出了~~~");
        }, "t1").start();

        Thread.sleep(2000);

        new Thread(()->{
            System.out.println("flag的值修改为false");
            flag=false;
        }, "t2").start();
    }
}

t1线程中的flag能获取到t2线程修改flag变量后的值所以就退出循环了,运行结果如下图:
volatile和synchronized关键字介绍

有序性代码例子

public class OrderlinessDemo {
    public static int a=0,b=0,i=0,j=0;

    public static void main(String[] args) throws Exception {
        int count = 0;
        while(true){
            a=0;
            b=0;
            i=0;
            j=0;
            Thread t1 = new Thread(() -> {
                a = 1;
                i = b;
            }, "t1");

            Thread t2 = new Thread(() -> {
                b = 1;
                j = a;
            }, "t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            count++;
            System.out.println("第"+ count +"次输出结果, i = "+i+", j ="+j);
            if(i==0 && j==0){
                break;
            }

        }
    }
}

理论上来说,并发情况下只可能输出i=0,j=1;i=1,j=0;i=1,j=1的情况.只有当发生指令重排序,才能输出i=0,j=0的情况,运行结果如下图:
volatile和synchronized关键字介绍

public class OrderlinessVolatileDemo {
    public static volatile int a=0,b=0,i=0,j=0;

    public static void main(String[] args) throws Exception {
        int count = 0;
        while(true){
            a=0;
            b=0;
            i=0;
            j=0;
            Thread t1 = new Thread(() -> {
                a = 1;
                i = b;
            }, "t1");

            Thread t2 = new Thread(() -> {
                b = 1;
                j = a;
            }, "t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            count++;
            System.out.println("第"+ count +"次输出结果, i = "+i+", j ="+j);
            if(i==0 && j==0){
                break;
            }

        }
    }
}

添加volatile修饰变量后,一直没有出现i=0,j=0的输出情况,运行结果如下图:
volatile和synchronized关键字介绍

原子性代码例子

public class AtomicityDemo {
    static int count = 0;

    public static void main(String[] args) {
        for(int i=0; i<100;i++){
            new Thread(()->{
                for(int j=0;j<10000;j++){
                    count++;
                    System.out.println("输出结果:"+count);
                }
            }).start();
        }
    }
}

100个线程进行累加1W次,输出的结果始终小于100W且每次运行的结果大概率会都不一样,运行结果如下图:
第一次:
volatile和synchronized关键字介绍
第二次:
volatile和synchronized关键字介绍
第三次:
volatile和synchronized关键字介绍

public class AtomicityVolatileDemo {
    volatile static int count = 0;

    public static void main(String[] args) {
        for(int i=0; i<100;i++){
            new Thread(()->{
                for(int j=0;j<10000;j++){
                    count++;
                    System.out.println("输出结果:"+count);
                }
            }).start();
        }
    }
}

使用volatile修饰变量后,结果也是小于100W,说明volatile是无法保证原子性的,但最后输出的数值会比上面未使用volatile修饰的运行结果更接近100W的结果,运行结果如下图:
第一次:
volatile和synchronized关键字介绍
第二次:
volatile和synchronized关键字介绍
第三次:
volatile和synchronized关键字介绍

synchronized

原子性和可见性代码例子

public class AtomicitySynDemo {
    static int count = 0;

    public static void main(String[] args) {
        for(int i=0; i<100;i++){
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    synchronized (AtomicitySynDemo.class) {
                        count++;
                        System.out.println("输出结果:" + count);
                    }
                }
            }).start();
        }
    }
}

使用synchronized的代码块,能保证代码块里面的操作具备的原子性和变量的可见性,所以每次运行结果都是100W.

总结

特点

volatile关键字解决的是内存可见性和有序性的问题.可见性是因为变量的写操作都会直接刷新到主存,读操作都会去主存中同步.有序性是变量通过内存屏障(是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束)来禁止指令重排序.
synchronized关键字通过锁机制来保证代码块的同步顺序执行,从而解决操作原子性的问题.同时synchronized代码块的每次执行开始和结束,都会分别将变量读取主存和写入主存中,从而解决变量可见性的问题.synchronized不能防止同步代码块里面的代码进行重排序,所以不能解决代码块的有序性.

[下一篇 介绍synchronized对象锁]

点赞
收藏
评论区
推荐文章
浩浩 浩浩
5年前
JVM--指令重排序+volatile关键字
volatile关键字1、volatile翻译为不稳定的,容易改变的。意思很明确,如果使用volatile定义一个变量,意思就是可能该变量改变频繁,并且设计到多线程访问问题。2、不过现在jdk的synchronized关键字性能已经足够出色,也提供了多种Lock类,因此volatile关键字能实现的功能jdk的同步方法都能够实
Easter79 Easter79
4年前
synchronized底层原理
前言一、synchronized的特性1.1原子性1.2可见性1.3有序性1.4可重入性二、synchronized的用法三、synchronized锁的实现3.1同步方法3.2同步代码块四、
Wesley13 Wesley13
4年前
volatile实现可见性但不保证原子性
   volatile关键字:能够保证volatile变量的可见性不能保证volatile变量复合操作的原子性         volatile如何实现内存可见性:        深入来说:通过加入内存屏障和禁止重排序优化来实现的。对volatile变量执行写操作时,会在写操作后加入一条store屏
Wesley13 Wesley13
4年前
Volatile关键字
Volatile关键字①volatile的两个特点1保证线程(CPU)之间的可见性;(也就是保证数据一致性)简单解释一下:一个线程将一个值的数值改变时,另一个使用该数值的线程能看到这种改变;2禁止指令重排序(禁止乱序执行);这个和单例
Wesley13 Wesley13
4年前
Java多线程之volatile详解
目录:什么是volatile?JMM内存模型之可见性volatile三大特性之一:保证可见性volatile三大特性之二:不保证原子性volatile三大特性之三:禁止指令重排小结1.什么是volatile?答:volatile是java虚拟机提供的轻量级的同步机制(
Wesley13 Wesley13
4年前
JAVA 并发包
Java.Utril.ConcurrentVolatile关键字避免java虚拟机指令重排序,保证共享数据修改同步,数据可见性。volatile相较于synchronized是一种比较轻量级地同步策略,但不具备互斥性,不能成为synchronized的替代,不能保证原子性。
Wesley13 Wesley13
4年前
Java并发系列2
上一节讲到Java线程和synchronized关键字的使用。下面就开始介绍JDK中的一些好用的并发控制工具。先来看ReetrantLock类,他可用来替换synchronized关键字,而且比synchronized关键字更为强大和灵活。一、ReetrantLock简单示例先看代码:publi
Wesley13 Wesley13
4年前
Java多线程(二)
\恢复内容开始一,volatile关键字当多个线程操作共享数据时,可以保证内存中的数据可见性相较于synchronized关键字:1,不具备“互斥性”2,不能保证变量的原子性二,原子变量volatile保证内存可见性CAS(CompareAndSwap)算法保证数据的原子性内存值V预估值A更新值
Wesley13 Wesley13
4年前
Java多线程之volatile关键字
volatile关键字1importjava.util.concurrent.TimeUnit;23/4volatile5volatile比synchronized效率高很多6能用volatile就不要用synchronized,很多并
Wesley13 Wesley13
4年前
Java 并发编程之 JMM & volatile 详解
本文从计算机模型开始,以及CPU与内存、IO总线之间的交互关系到CPU缓存一致性协议的逻辑进行了阐述,并对JMM的思想与作用进行了详细的说明。针对volatile关键字从字节码以及汇编指令层面解释了它是如何保证可见性与有序性的,最后对volatile进行了拓展,从实战的角度更了解关键字的运用。一、现代计算机理论模型与工作原理
Wesley13 Wesley13
4年前
Java 开发, volatile 你必须了解一下
并发的三个特性首先说我们如果要使用volatile了,那肯定是在多线程并发的环境下。我们常说的并发场景下有三个重要特性:原子性、可见性、有序性。只有在满足了这三个特性,才能保证并发程序正确执行,否则就会出现各种各样的问题。原子性,上篇文章说到的CAS和Atomic\类,可以保证简单操作的原子性,对