您的位置:首页 > 其它

volatile

2020-11-22 10:15 417 查看

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:

  1. 分配内存空间

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

  3. 返回地址给引用

原因:由于创建对象是一个非原子操作,编译器可能会重排序,即只是在内存中开辟一片存储空间后直接返回内存的引用。而下一个线程在判断 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又保证了可见性,所以就保证了线程安全。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: