11-可见性

nobility 发布于 2021-05-17 1494 次阅读


可见性

高速缓存容量比主存内存小,但是速度快,所以为了提高执行效率,CPU与主存之间就多了缓存层(不同的CPU有不同的缓存层数),由于CPU有多层缓存,导致读到数据可能会过期,线程之间的共享变量的可见性问题就是由于缓存引起的,Java内存模型标准就屏蔽了底层的多层缓存,只抽象成了两层:主内存和工作内存

Java中所有的共享的变量都存储在主内存中,每个线程都有自己的工作内存,工作内存中保存该线程使用到的变量的主内存副本拷贝;线程对变量的读写操作都应该在工作内存中完成;不同线程之间不能相互访问工作内存,交互数据需要通过主内存

下面代码验证可见性

public class Main {
  private static int a = 1;
  private static int b = 2;

  public static void main(String[] args) {
    while (true) {
      a = 1;
      b = 2;
      new Thread(() -> {
        try {
          Thread.sleep(100);  //休眠更容易出现该效果
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        a = 3;
        b = a;
      }).start();
      /**
       * thread1先运行完毕,thread2再运行,                                a=3,b=3
       * thread2先运行完毕,thread1再运行,                                a=1,b=2
       * thread1和2交叉运行,                                              a=3,b=2
       * thread1先运行完毕,但thread2只看到了对b的修改却未看到对a的修改    a=1,b=3
       */
      new Thread(() -> {
        try {
          Thread.sleep(100);  //休眠更容易出现该效果
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        if (a == 1 && b == 3) {  //条件是a=1,b=3
          System.out.println("a=" + a + ",b=" + b);  //输出结果却可能是其他值
        }
      }).start();
    }
  }
}

Happens-Before

由于操作的有执行顺序,由于操作可以在一个线程之内,也可以是在不同线程之间,所以Java内存模型通过happens-before规则解决跨线程的内存可见性问题,简单的说就是:前一个操作的结果可以被后续操作获取

volatile、synchronized、lock、并非容器、join等待、线程启动等都可以保证可见性,具体Happens-Before规则如下:

  • 单线程原则:又由于传递性,一个线程内,后面语句一定能看到前面语句
  • 锁操作:加锁之后一定能看到解锁之前的指令,以及解锁之前的其他操作
  • volatile变量:使用volatile修饰的变量,所有线程都能立即看到该变量的修改,以及之前的其他操作
  • 线程启动:父线程启动子线程,子线程一定能看到启动前父线程的所有指令
  • 线程join:线程join等待结束后,一定能看到被等待线程的所有指令
  • 中断:线程被其他线程中断,中断检测或中断异常一定能被其他线程看到
  • 构造方法:finalize()方法一定能看到构造方法中的最后一条指令
  • 工具类的Happens-Before原则
    • 线程安全容器get一定能看到在此之前的put等存入动作
    • CountDownLatch、CyclicBarrier、Semaphore
    • Future
    • 线程池

volatile关键字

volatile的作用

volatile只能作用于属性字段上

volatile是一种轻量级的同步机制,因为并不会发生上下文切换等行为开销(无锁机制),相应的能力也小,无法做到synchronized那样的原子保护,只有在很有限的场景下才能发挥作用

  • 可见性:读取一个volatile变量之前,需要先适相应的本地缓存失效,这样就一定能从主内存中读取到最新的值;修改一个volatile变量会立即刷新到主内存中
  • 禁止指令重排序优化:比如解决单例双重锁乱序问题

不适用的场合

非原子性的数据争用

public class Main {
  private volatile static int a;

  public static void main(String[] args) throws InterruptedException {
    Runnable runnable = () -> {
      for (int i = 0; i < 10000; i++) a++;
    };
    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    //保证线程1和线程2先执行结束
    System.out.println("最终的a=" + a);  //结果不一定是20000
  }
}

适用场合

纯赋值的原子操作,由于Happens-Before规则,volatile可作为刷新之前变量的触发器

public class Main {
  private volatile static boolean flag;
  private static String options;

  public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new Thread(() -> {
      System.out.println("读取配置文件");
      options = "已赋值配置";
      System.out.println("读取配置成功");
      flag = true;  //由于Happens-Before规则,另一个线程也一定会看到options的修改,从而保证线程安全
    });
    Thread thread2 = new Thread(() -> {
      while (!flag) {  //未配置好就陷入休眠1秒
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println("配置项为:" + options);
    });
    thread1.start();
    thread2.start();
  }
}

对比synchronized

volatile相对于synchronized更轻量,如果一个共享变量至始至终只被各个线程赋值,而没有其他操作,那么就可以使用volatile来代替synchronized以及原子变量,因为赋值本身就具有原子性,而volatile又保证了可见性,所以就保证了线程安全

此作者没有提供个人介绍
最后更新于 2021-05-17