Java线程模型定义了程序中各种变量的访问规则,即虚拟机中把变量存储到内存和从内存中取出变量值这样的底层细节。此处的变量包括实例对象、静态字段和数组对象,其实也就是堆上的数据。线程私有的变量不会被共享,也就是不存在变量共享的问题。

线程的工作内存中包含线程私有变量和部分主内存变量的副本。
线程不能直接操作主内存的数据,必须先拷贝到工作内存。线程之间的工作内存不能互操作,必须通过主内存。
image.png

Java内存模型规则

程序的顺序性规则

一个线程中,前面的操作Happens-Before于后续的任意操作。

Volatile变量规则

对volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。

传递性

A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。


管程中的规则

对一个锁的解锁Happens-Before于后续对这个锁的加锁。管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现。

  1. int x = 0;
  2. //线程A
  3. synchroized(objectA){
  4. x = 10;
  5. }
  6. //线程B
  7. synchroized(objectB){
  8. //此时读取到的x为10
  9. }

线程join原则

主线程A等待子线程B完成,当子线程B完成后,主线程能够看到子线程的操作。

  1. int x =0;
  2. Thread threadA = new Thread(()->{
  3. x = 10;
  4. });
  5. threadA.join();
  6. //此时主线程读取的x为10

Java内存模型实现原理

主要是通过内存屏障禁止重排序。编译器根据具体的CPU体系架构,将这些内存屏障转换成具体的CPU指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新。

具体来讲,volatile修饰的变量在汇编后有一个lock指令。这个指令会做两件事情:

  • Lock指令前的指令不能排到Lock指令后,
  • 修改后,对其它CPU立即可见;

可见性@待整理

Java语言层面支持的可见性实现方式有两种:

  • Synchronized
  • Volatile

Synchronized不仅能通过互斥锁来实现同步,而且还能够实现可见性。Java内存模型关于Synchronized有两条规定:

  • 线程释放锁之前,JMM会将工作内存中的共享变量刷新到主内存中;
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

线程执行互斥代码的过程:

  • 获取监视器锁
  • 清空工作内存
  • 从主内存中拷贝变量的最新副本到工作内存
  • 执行代码
  • 将更改后的共享变量的值刷新到主内存
  • 释放监视器锁

如果某个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其它所有要调用类中任何标记为synchronized方法的线程都会被阻塞。

volatile 待整理

被volatile修饰的变量实现了内存可见性禁止指令重排序的两大语义。

内存可见性

  1. 如果一个线程对共享变量的修改,能够被其它线程看到,那么就能说明共享变量在线程之间是可见的。如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。所有的变量都储存在主内存中。每个线程都有自己独立的工作内存,里面保存了该线程使用到的变量的副本(主内存中该变量的一份拷贝),如下图所示。<br />![image.png](https://cdn.nlark.com/yuque/0/2019/png/182772/1566134801337-a582341d-e5a3-44d2-b85f-1acd86303015.png#height=251&id=aU1le&name=image.png&originHeight=910&originWidth=1730&originalType=binary&ratio=1&size=491205&status=done&style=none&width=478)<br />为什么会出现共享变量可见性的问题,这是因为线程对共享变量的所有操作都必须在自己的工作内存中进行,不能从主内存中读写;而且不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。线程1对共享变量的修改要想被线程2及时看到,必须要经过如下两个步骤:
  • 把工作内存1中更新过的共享变量刷新到主内存中;
  • 把内存中最新的共享变量的值更新到工作内存2中

JVM提出了对volatile变量的要求:

  • 当对volatile变量执行写操作后,JMM会把工作内存中最新的变量值刷新到主内存;
  • 写操作会导致其它线程的缓存失效;

禁止指令重排序

通过内存屏障实现指令的禁止重排序。

底层实现

它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其它处理器对应的缓存失效。另外还提供了有序的指令无法越过这个内存屏障的作用。

volatile通过加入内存屏障禁止指令重排序优化来实现的:

  • 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,这样就会把读写时的数据缓存加载到主内存中;
  • 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令,这样就会从主内存中加载变量;

所以说,volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,就会强迫线程将最新的值刷新到主内存,这样任何时刻,不同的线程总能看到该变量的最新值。
线程写volatile变量的过程:

  • 改变线程工作内存中volatile变量副本的值;
  • 将改变后的副本的值从工作内存刷新到主内存中

线程读volatile变量的过程:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中;
  • 从工作内存中读取volatile变量的副本

image.png
保证number自增操作的原子性,可以通过以下方法:

  • 使用synchronized关键字
  • 使用ReentrantLock

原子性和易变性

原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前执行完毕。原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。当定义long或double变量时,如果使用volatile关键字,就会获得原子性。如果一个域被声明为volatile,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到整个修改。如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile。例如下面这段代码:

  1. class MyThread2 implements Runnable{
  2. private int tickCount = 5;
  3. @Override
  4. public void run() {
  5. while(tickCount >0){
  6. tickCount--;
  7. System.out.println(Thread.currentThread().getName()+"卖出一张票,还剩下"+tickCount+"张票");
  8. }
  9. }
  10. }
  11. public class RunnableTicket {
  12. public static void main(String[] args) {
  13. MyThread2 thread =new MyThread2();
  14. Thread th1 = new Thread(thread,"线程1");
  15. Thread th2 = new Thread(thread,"线程2");
  16. Thread th3 = new Thread(thread,"线程3");
  17. th1.start();
  18. th2.start();
  19. th3.start();
  20. }
  21. }

类MyThread2实现了Runnable接口,对于共享统一资源的情况,应该实现Runnable接口而不是继承Thread类,上面的run()方法输出结果如下:

线程3卖出一张票,还剩下2张票 线程2卖出一张票,还剩下2张票 线程1卖出一张票,还剩下2张票 线程2卖出一张票,还剩下0张票 线程3卖出一张票,还剩下1张票

可以看到虽然总的票数没有错,但是输出结果并不是我们想要的。Java提供了关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。也就是将上面的run()方法改为public synchronized void run(),即可输出正确的结果。

线程安全

线程安全,指的是在多线程环境下,一个类执行某个方法时,对类的内部实例变量的访问是安全的。对于下面的两类变量,不存在线程安全问题

  • 方法签名中的任何参数变量
  • 处于方法内部的局部变量