Java并发编程实践笔记之—可见性(Visibility)

时间:2022-05-09 15:25:47

同步的重要性有两个方面:

  • 实现原子性:防止某个线程正在使用对象状态而另一个线程同时在修改改状态
  • 内存可见性:确保一个线程修改了对象状态后,其他线程能够看到发生的状态变化

失效数据

  • 缺乏同步的程序可能会产生的一种错误情况就是——失效数据
  • 失效数据举例
    //在没有同步的情况下共享数据
    public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
    public void run() {
    while (!ready)
    Thread.yield();
    System.out.println(number);
    }
    }

    public static void main(String[] args) {
    new ReaderThread().start();
    number = 42;
    ready = true;
    }
    }

引发的问题有

  1. NoVisibility 可能会持续循环下去,因为ReaderThread可能永远看不到ready写入的值
  2. NoVisibility 可能会输出0,因为ReaderThread可能看到了ready写入的值,但是没有看到写入的number的值(这种现象叫重排序)

非原子的64位操作

  • 非volatile类型的64位数值变量(double和long)在多线程程序中使用也是不安全的
  • 如果对该变量的读操作和写操作在不同的线程中执行,那么可能会读取某个值的高32位和另一个值的低32位
  • 用关键字volatile来声明,或者用锁保护

加锁与可见性

  • 为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步

volatile变量

  • 稍弱的同步机制
  • 编译器和runtime都会注意到这个变量是共享的,所以不会将该变量重排序,不会被缓存在寄存器或者其他处理器不可见的地方
  • 行为类似于以下程序清单,但是使用volatile时不会执行加锁操作,因此也不会阻塞线程,比synchronized更轻量级
    @ThreadSafe
    public class SynchronizedInteger {
    @GuardedBy("this") private int value;

    public synchronized int get() { return value; }
    public synchronized void set(int value) { this.value = value; }
    }

  • 仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们,不建议过度依赖volatile变量提供的可见性
  • 通常用作某个操作完成、发生中断或者状态的标志
  • 典型用法:检查某个状态标记以判断是否退出循环。以下示例中asleep必须为volatile变量,否则当asleep被另一个线程修改时,执行判断的线程却发现不了。
  • 加锁机制可以确保可见性和原子性,而volatile变量只能确保可见性
    //数绵羊
    volatile boolean asleep;
    ...
    while (!asleep)
    countSomeSheep();