关于java:聊聊volatile

46次阅读

共计 2035 个字符,预计需要花费 6 分钟才能阅读完成。

一、作用

1、保障线程可见性

main 线程和 T 线程共享堆内存的数据;main 和 T 也有本人的工作空间,当要拜访共享内存的数据 flag,会把共享内存的 flag 复制一份到本人的工作空间中。

例如 main 线程对 flag 进行了扭转,首先是在本人的空间进行扭转,批改后的值会马上批改到共享内存;然而 T 线程何时查看共享内存的值有没有被扭转不好管制。

即 main 线程的批改并没有及时的反馈到 T 线程,也就是线程之间不可见。对这个变量加了 volatile 后,可能保障一个线程对这个变量批改后,另一个线程可能马上晓得。

底层是通过 CPU 的缓存一致性协定(MESI)保障的

2、禁止指令重排序

指令重排序:CPU 原来执行一条指令时,是一步一步的程序执行;当初的 CPU 为了提高效率,会并发的执行指令,就是第一个指令执行到一半时,第二个指令可能曾经开始执行了,也就是流水线似的执行。这时就要求编译器,要可能对指令重排序产生影响的状况进行相应的解决,此时 volatile 就派上用场了。

DCL 单例 (Double Check Lock)来解释

/**
 * @author Java 和算法学习:周一
 */
public class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {if (INSTANCE == null) {
            // 双重查看
            synchronized (Singleton.class) {if (INSTANCE == null) {
                    try {Thread.sleep(1);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    INSTANCE = new Singleton();}
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(Singleton.getInstance().hashCode());
            }).start();}
    }
    
}

INSTANCE = new Singleton();(如果外面有个变量 int a = 100)new 对象的过程分为 3 步
1)申请内存(赋默认值);a=0
2)初始化;a=100
3)赋值,把值给变量;把 a 的值赋值给 INSTANCE,即让 INSTANCE 指向变量 a 的地址

如果外面存在指令重排序的话,就存在还未初始化的变量就进行了赋值操作,即 2、3 两步地位替换了;也就是对象处于半初始化状态就进行了赋值操作。当第一个线程(只管加了锁),执行到 new Singleton() 时,new 了一半;此时第二个线程来了,首先判断 INSTANCE 是否为空,因为 INSTANCE 曾经是半初始化状态,外面曾经有值了,不再是空值了,也就是第二个线程曾经拿到了这个对象了,这个线程就能够间接应用该对象了,很可能就会应用外面的这个值。原本冀望这个值是 100,然而这个值却是 0,如果这个值是订单数的值这时就存在问题。 加了 volatile 后,对这个对象的指令重排序就不容许存在,即必须是在初始化实现之后才会进行赋值操作

3、volatile 不能保障原子性

/**
 * @author Java 和算法学习:周一
 */
public class T {
    public volatile int count = 0;

    public synchronized void m() {for (int i = 0; i < 1000; i++) {count++;}
    }

    public static void main(String[] args) {T t = new T();
        List<Thread> threadList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {threadList.add(new Thread(t::m));
        }

        threadList.forEach((o)->{o.start();
        });
        threadList.forEach((o)->{
            try {o.join();
            } catch (InterruptedException e) {e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }

}

m 办法,如果不加 synchronized,count 始终不会到 10000。起因是,当一个线程把 count 值批改为 1 后,此时进来了第二、第三个线程,都读到 count 为 1,批改后把 count 加 1(即 2)写回去,这时两个线程对 count 批改后,count 的值只从 1 变到了 2,所以最初的后果总是小于 10000。归根结底就是 count 的值是保障了可见性,然而 count++ 自身不是原子性的操作(底层分为几个步骤)。

volatile 能保障线程的可见性,然而并不能代替 synchronized 保障原子性。

正文完
 0