Java并发编程实战笔记三(可见性)

    xiaoxiao2023-10-15  26

    1.1 内存可见性

          意味着当一个线程更新了共享数据时对其他线程可见。

     

    1.1.1 可见性

         可见性是一种复杂的属性。在单线程环境中,如果向某个变量写入值,然后在没有其他写入操作的情况下读取这个变量,总能得到相同的值。但是,在多线程环境中,读操作和写操作在不同的线程中执行,情况却并非如此。

    代码示例:

    public static void main(String[] args) throws InterruptedException { FlagClass fc = new FlagClass(); new Thread(() -> { while (!fc.isFlag()) { // 读取flag的值为true时结束循环 } System.out.println("---->end"); }).start(); TimeUnit.MILLISECONDS.sleep(200); fc.setFlag(true); // 修改flag的值为true } private static class FlagClass { private boolean flag = false; public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }

    启动读操作的线程可能会一直循环下去,不会执行打印语句。因为无法保证执行读操作的线程能够实时的看到其他(main)线程写入(更新)的值,因此对于读操作的线程来说是不可见的。

     

    1.1.2 失效数据

          在上面的程序中可能产生错误结果的一种情况:失效数据。当读线程查看flag变量时,可能会得到一个已经失效的值(因为flag变量已经被其他线程修改过)。除非在每次访问变量时都使用同步,否则很可能获得该变量的一个失效值,从而导致程序得到错误的值,无法正常结束。

     

    1.1.3 可见性的两种解决方式

    1. 加锁与可见性

              当线程执行同步代码块获取锁时,重新去主内存读取数据;释放锁之前更新主内存。

    简单来说:当一个线程执行由锁保护的同步代码块时,可以看到上一个线程在同一个同步代码块中的所有操作结果。

    private static class FlagClass { private boolean flag = false; public synchronized boolean isFlag() { // 加锁 return flag; } public synchronized void setFlag(boolean flag) { // 加锁 this.flag = flag; } }

    加锁(加锁机制)的含义不仅仅局限于互斥行为(原子性),还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

    2. Volatile关键字

          一种比 synchronized 关键字轻量级的同步机制,用来确保变量的更新操作通知到其他线程。

    private static class FlagClass { private volatile boolean flag = false; // 变量声明为volatile类型 public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }

    在访问 volatile 变量时不会执行加锁操作,因此在多线程修改变量时不能确保原子性。

    禁止重排序:不会将该变量上的操作与其他内存操作一起重排序。

    重排序:编译器、处理器运行时对代码的执行顺序进行优化。

     

    总结

          加锁机制可以确保原子性和可见性,而 volatile 变量只能确保可见性。

          如果只是为了多线程之间的通信效果,而不是为了互斥访问,就使用 volatile ,它更加简洁,性能也更好。

     volatile 变量使用场景:

    用于标记可变状态和结果多个线程读变量,单个线程去更新变量(不能保证原子性)不需要加锁(加锁可以确保可见性)

     

    最新回复(0)