【Java并发编程学习笔记】volatile关键字

volatile关键字

      volatile关键字是JVM提供的一个轻量级的同步机制,可以保证有序性和可见性,不能保证原子性。

volatile原理

    volatile修饰的变量带有内存屏障(写屏障/读屏障),Mermory Barrier(Mermory Fence)

volatile保证可见性和有序性

可见性

    volatile修饰的变量是带有写屏障和读屏障的,写屏障之前对于共享变量的修改都刷新到主存中。在读屏障之后,读取的数据都是从主存中加载进来的最新数据,每次读取的时候都从主存读取出来生成一个新的副本。

public class Main {
    static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                // 读屏障,在此之后从主存之中读取共享变量的最新值
                while (flag) {
                    
                }
            }
        });
        Thread.sleep(2000);
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                flag = false;
                // 写屏障,在此之前将共享变量的值刷新到主存中
            }
        });
        t2.start();
    }
}

有序性

    volatile遵循happened-befores规则:多线程情况下,即便发生了指令重排序也不影响最终的结果。

synchronized&volatile

  • volatile关键字是线程同步的轻量级实现,性能要比synchronized
  • volatile只能修饰变量;synchronized可以修饰方法和同步代码块
  • JDK新版本的发布优化了synchronized,优化后的synchronized执行效率有了很大的提升,一般在开发中使用synchronized
  • 多线程反问volatile不会阻塞,synchronized会阻塞
  • volatile不能保证原子性,可以保证可见性和有序性;synchronized三个核心问题都可以解决
  • volatile解决的是多个线程之间共享数据的可见性;synchronized解决的是多线程之间访问共享资源的同步性

DCL+volatile

// Double Check Lock
public class Singleton {
    //private static volatile Singleton instance;
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    // new不是原子操作,所以有可能引发多线程问题
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
    }
}
  • new操作不是原子操作,这一点我们可以通过查看字节码文件来得知
  • 查看字节码文件:idea安装jclasslib插件,view->show Bytecode with jclasslib
// 指令含义查看字节码手册
17 new #3 <Singleton> 
// 1. 在堆区分配内存,生成一个不完全的对象,
// 将不完全对象的引入压入栈顶
20 dup 
// 1. 复制栈顶元素, 将复制数组压入栈顶
21 invokespecial #4 <Singleton.<init> : ()V>
// 堆区中的对象就是一个完整的对象了(执行了默认构造方法)
24 putstatic #2 <Singleton.instance : LSingleton;>
// 将完整对象的引用赋值给方法区的共享变量

正常流程(上面的流程) // 1.生成一个不完全对象(创建一个对象空间) // 2.初始化对象 // 3.instance指向初始化对象 // 4.单线程访问对象没有问题
如果发生指令重排 // 1.首先创建一个不完全对象(创建一个对象空间) // 2.instance指向这个对象空间 // 3.初始化对象 // 4.单线程访问对象的时候不会影响结果 // 问题:多线程情况下,可能在当前线程在创建对象的过程中, // 由于指令重排序使得其他的线程获取到instance==null,此时instance只对自己的线程可见, // 那么竞争到锁以后,instance依然为null,进而无法保证得到的对象是单例的,此时就需要添加volatile

CAS

  • Unsafe实现了CAS操作(Java的原子操作类中使用了Unsafe)。
  • Java代码中,Unsafe类只能通过反射来获取。
  • CAS可以将read-modify-write这类的操作转化为原子操作。
  • 悲观锁:synchronized可以称为悲观锁,每次只允许一个线程执行同步代码块,其他线程只能阻塞。
  • 乐观锁:CAS可以称为乐观锁,允许多个线程同步操作共享资源。

CAS+volatile

  • CAS+volatile可以实现无锁化编程
    • CAS在操作共享变量的时候,如果使用volatile修饰共享变量,可以保证共享变量的可见性。
    • 无锁化编程适用于竞争不激烈,多核CPU的情况之下:
    • 线程可以并发,不会进入阻塞或者等待状态,可以提高执行的效率;
    • 当时当竞争激烈的时候,那么比较的次数就会变多,重试的次数也会变多,CPU占用也就越多,反而影响效率。

CAS解决ABA问题

  • 添加版本号,把数据更新到主存的时候,再次读取主存中的变量的版本号,如果现在变量的版本号与主存中一致则更新。
  • 添加时间戳。
posted @ 2021-10-02 20:03  菜鸭丶  阅读(58)  评论(0编辑  收藏  举报