volatile

JMM(java内存模型)

  • JMM屏蔽了底层不同计算机的区别,描述了Java程序中线程共享变量的访问规则,以及在jvm中将变量存储到内存和从内存中读取变量这样的底层细节。

  • JMM有以下规定:

    • 所有的共享变量都存储与主内存中,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

    • 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

    • 线程对变量的所有操作(读和写)都必须在工作内存中完成,而不能直接读写主内存中的变量。

    • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值传递需要通过主内存中转来完成。

多线程下变量的不可见性:

public class test7 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        while (true) {
            if (t.isFlag()) {
                System.out.println("停不下来了"); // 不会执行到这里
            }
        }
    }
}
class MyThread extends Thread {
    private boolean flag = false;
    // private volatile boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag被修改了");
    }

    public boolean isFlag() {
        return flag;
    }
}

原因:

  • 子线程t从主内存读取到数据放入其对应的工作内存
  • 将flag的值更改为true,但flag的值还没有写回主内存
  • 此时main方法读取到了flag的值为false
  • 当子线程t将flag的值写回主内存后,主线程没有再去读取主内存中的值,所以while(true)读取到的值一直是false。

volatile 的特性

  • volite 可以实现并发下共享变量的可见性;

  • volite 不保证原子性;

  • volite 可以防止指令重排序的操作。

    使用原子类来保证原子性:

    public AtomicInteger(): 初始化一个默认值为0的原子型Integer 
    public AtomicInteger(int initialValue): 初始化一个指定值的原子型
    Integer int get(): 获取值 
    int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。 
    int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。 
    int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的 value)相加,并返回结果。 
    int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值
    
    private static AtomicInteger atomicInteger = new AtomicInteger();
    Runnable r = () -> {
         for (int i = 0; i < 100; i++) {
               atomicInteger.incrementAndGet();
         }
    };
    

    有时为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。重排序可以提高处理的速度。

volatile写读建立的happens-before关系

happens-before :前一个操作的结果可以被后续的操作获取。

happens-before规则:

  1. 程序顺序规则(单线程规则)

    同一个线程中前面的所有写操作对后面的操作可见

  2. 锁规则(Synchronized,Lock等)

    如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程

    1和线程2可以是同一个线程)

  3. volatile变量规则:

    如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都

    对线程2可见(线程1和线程2可以是同一个线程)

  4. 传递性

    A h-b B , B h-b C 那么可以得到 A h-b C

  5. join()规则:

    线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。

  6. start()规则:

    假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来

    线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。

public class VisibilityHP { 
    int a = 1; 
    int b = 2; 
    private void write() { 
        a = 3;
        b = a; 
    }
    private void read() { 
        System.out.println("b=" + b + ";a=" + a); 
    }
    public static void main(String[] args) { 
        while (true) { 
            VisibilityHP test = new VisibilityHP(); 
            new Thread(new Runnable() { 
                @Override 
                public void run() { 
                    test.write(); 
                } 
            }).start(); 
            new Thread(new Runnable() { 
                @Override 
                public void run() { 
                    test.read(); 
                } 
            }).start(); 
        } 
    } 
}

没给b加volatile,那么有可能出现a=1 , b = 3 。因为a虽然被修改了,但是其他线程不可见,而b恰好其他线程可见,造成了b=3 , a=1。

如果使用volatile修饰long和double,那么其读写都是原子操作

volatile在双重检查加锁的单例中的应用

饿汉式(静态常量)

public class Singleton01 {
    private static final Singleton01 Intance = new Singleton01();

    private Singleton01() {}

    public static Singleton01 getIntance() {
        return Intance;
    }
}

饿汉式(静态代码块)

public class Singleton02 {
    private final static Singleton02 Intance;

    static {
        Intance = new Singleton02();
    }

    private Singleton02() {}

    public static Singleton02 getInstance() {
        return Intance;
    }
}

懒汉式(线程安全,性能差)

public class Singleton03 {
    private static Singleton03 Instance;

    private Singleton03() {}

    public static synchronized Singleton03 getInstance() {
        if (Instance == null) {
            Instance = new Singleton03();
        }
        return Instance;
    }
}

懒汉式(volatile双重检查模式,推荐)

public class Singleton04 {
    private static volatile Singleton04 Instance = null;

    private Singleton04() {}

    public static Singleton04 getInstance() {
        if (Instance == null) {
            synchronized (Singleton04.class) {
                if (Instance == null) {
                    //创建对象的过程是非原子操作
                    Instance = new Singleton04();
                }
            }
        }
        return Instance;
    }
}

此处加上volatile 的作用:

① 禁止指令重排序。

创建对象的过程要经过以下几个步骤s:

a. 分配内存空间

b. 调用构造器,初始化实例

c. 返回地址给引用

原因:由于创建对象是一个非原子操作,编译器可能会重排序,即只是在内存中开辟一片存储空间后直接返回内存的引用。而下一个线程在判断 instance 时就不为null 了,但此时该线程只是拿到了没有初始化完成的对象,该线程可能会继续拿着这个没有初始化的对象继续进行操作,容易触发“NPE 异常”。

② 保证可见性

静态内部类单例方式

public class Singleton05 {
    private Singleton05() {}
    private static class SingletonInstance {
        private static final Singleton05 INSTANCE = new Singleton05();
    }
    
    public static Singleton05 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
  1. 静态内部类只有在调用时才会被加载,jvm在底层会保证只有一个线程去初始化实例,下一个线程获取实例时就直接返回。
  2. 相比于双重检查,静态内部类的代码更简洁。但基于volatile的双重检查有一个额外的优势:除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化。

volatile使用场景

  1. volatile适合做多线程中的纯赋值操作:如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么可以用volatile来代替synchronized,因为赋值操作本身是原子性的,而volatile又保证了可见性,所以足以保证线程安全。

  2. volatile可以作为刷新之前变量的触发器,可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到该变量之前的操作都将是最新可见的。

    public class test8 {
        int a = 1;
        int b = 2;
        int c = 3;
        volatile boolean flag = false;
        public void write() {
            a = 100;
            b = 200;
            c = 300;
            flag = true;
        }
        public void read() {
            while (flag) {
                System.out.println("a=" + a + " " + "b=" +  b + " " + "c=" + c);
                break;
            }
        }
    
        public static void main(String[] args) {
            test8 test8 = new test8();
            new Thread(() -> {
                test8.write();
            }).start();
            new Thread(() -> {
                test8.read();
            }).start();
        }
    }
    

volatile 和synchronized的区别

  1. volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
  2. volatile保证数据的可见性,但是不保证原子性,不保证线程安全。
  3. volatile可以禁止指令重排序,解决单例双重检查对象初始化代码执行乱序问题。
  4. volatile可以看做轻量版synchronized,volatile不保证原子性,但是如果对一个共享变量只进行纯赋值操作,而没有其他操作,那么可以使用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就保证了线程安全。