• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

volatile

开发技术 开发技术 2周前 (11-22) 7次浏览

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;
    }
}

原因:

volatile

  • 子线程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又保证了可见性,所以就保证了线程安全。

程序员灯塔
转载请注明原文链接:https://www.wangt.cc/2020/11/volatile/
喜欢 (0)