El modelo de memoria de Java entra en juego

¡Acostúmbrate a escribir juntos! Este es el día 17 de mi participación en el "Desafío de actualización de abril del nuevo plan diario de Nuggets", haga clic para ver los detalles del evento

Esta serie de columnas  Columna de programación concurrente de Java - Columna de Yuanhao 875 - Nuggets (juejin.cn)

prefacio

Antes de hablar sobre el modelo de memoria de Java, hablemos de algo que es fácil de confundir, el modelo de memoria de Java aquí es el modelo de memoria de Java, que es un conjunto complejo de especificaciones, que se utiliza para resolver algunos problemas en la programación concurrente.

Y mucha gente también llama a la estructura de memoria de tiempo de ejecución de JVM también llamada modelo de memoria de Java, lo cual es muy incorrecto, es decir, la siguiente imagen:

imagen.png

Estos son los diversos espacios abiertos por la JVM cuando se ejecutan programas Java, incluidas las áreas de método y montón compartidas por subprocesos (área de metadatos o generación permanente, que se manejan de manera diferente según las diferentes versiones), así como pilas de métodos y contadores de programas que son no compartido por hilos. Esta es la estructura de memoria JVM, no la llame modelo de memoria Java . En este capítulo llegamos a entender realmente qué es el modelo de memoria de Java.

1650097935(1).jpg

texto

En el último artículo, hablamos sobre los tres problemas principales que causan errores de programación concurrentes: visibilidad, atomicidad y orden, y estos tres problemas son los problemas que han evolucionado durante décadas de desarrollo informático, mientras que Java es un lenguaje de programación avanzado. El lenguaje admite la concurrencia, por lo que para resolver los problemas causados ​​por la visibilidad y el orden, el lenguaje Java presenta el famoso modelo de memoria Java .

¿Qué es el modelo de memoria de Java?

En el artículo anterior, dijimos que la fuente del problema de visibilidad es la memoria caché de la CPU, y la fuente del orden es la optimización del compilador, así que puedo deshabilitar la memoria caché de la CPU y la optimización del compilador, pero aunque el problema está resuelto, la gente además sin él, el rendimiento del programa será preocupante.

Una solución razonable es deshabilitar el almacenamiento en caché y la optimización de compilación a pedido. ¿Cuándo se necesita? Este es el programador que escribe el código, por lo que sería bueno proporcionar a los programadores una forma de deshabilitar el almacenamiento en caché y la optimización de compilación según sea necesario. , y el El modelo de memoria de Java hace precisamente eso.

Java内存模型定义了一套规范,能使JVM按需禁用CPU缓存和编译优化;而对于程序员来说,就是提供了一些方法可以让JVM按需禁用缓存和编译优化,这些方法包括了volatile、synchronized和final三个关键字,以及六项Happen-Before规则。

使用volatile的困惑

volatile在古老的C语言中就有,最原始的意义就是禁用CPU缓存,例如我们声明一个volatile变量volatile int x = 0,这句话的意思对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入。

那如果只有这个功能的话也只能说明这个变量是线程间可见的,但是还不够完全解决问题,我们来看下面代码:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}
复制代码

假如线程A执行writer()方法,会把变量v的值为true写入内存,线程B执行reader()方法,按照volatile语义,线程B会从内存中获取变量v,如果线程B看到的是"v == true",这里x值可能是42也可能是0,这里要分版本:低于1.5版本上,x值可能是42,也可能是0;如果1.5以上的版本,x只能是42。

产生这个原因也非常简单,变量x可能被CPU缓存导致可见性问题,也有可能是指令重排导致的,但是在1.5以上对volatile语义进行了增强,如何增强呢 就是Happens-Before规则。

Happens-Befroe规则

Happen-Before规则是Java内存模型制定的规则,用来处理线程间可见性问题,至于如何去处理,我们先不做讨论细节。

这个Happen-Before可以说是Java内存模型中最难懂的地方,理解起来非常绕;首先这个词的翻译就比较难,Happen-Before并不是说前面一个操作发生在后续操作之前,它要真正表达的意思是:前面一个操作的结果对于后续操作是可见的

就像有心灵感应的2个人,虽然远隔千里,一个人所想,另一个人能看得见,而Happens-Before规则就是要线程之间保持这种"心灵感应",所以比较正式的说法是:Happens-Berfore约束了编译器的优化行为,允许编译器优化,但是优化后必须遵守Happens-Before规则

说到这里或许就明白了,虽然volatile变量禁用了CPU缓存,但是没有禁止编译器优化啊,编译器依旧可以优化,但是像前面说的把"x = 42和v = true"给调换位置的优化就不会,而且x可见性也能得到保证,那这个强大的Happen-Before规则是什么样的呢。

和程序员相关的规则有6个,且都关于可见性的。

(1) 程序的顺序性规则

指的是在一个线程中按照程序顺序,前面的操作Happens-Berfore于后续的任意操作。比如前面的代码中,第6行代码"x = 42" Happens-Before于第7行代码"v = true",这比较符合单线程的思维:程序前面对某个变量的修改,一定是对后续操作可见的。

注意哦,这个规则是单线程下的规则,比如上面代码如果没有volatile修饰的话,x和v的赋值之间是没有依赖的,所以这2个赋值操作可以重排,但是x的赋值结果却对v的赋值这条语句来说是可见的,虽然这个可见性没啥用(因为v的赋值不依赖x的值)。

(2) volatile变量规则

这条规则是指一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作,单独看这一条规则,这不就是禁用缓存的意思吗,别急,我们看第三条。

(3) 传递性规则

这条规则是指如果A Happens-Berfore B,且B Happens-Before C,那么A Happens-Before C,那我们将规则3的传递性应用到我们的例子中是:

imagen.png

可以看到:

  • "x = 42" Happens-Before 写变量"v = true",这是规则1;

  • 写变量"v = true" Happens-Before 读变量"v = true",这是规则2;

再根据传递性规则,我们得到结果"x = 42" Happens-Before读变量"v = true",这意味什么呢 那就是线程A设置的"x = 42"是对线程B可见的,也就是线程B能看到"x == 42",这就是1.5版本对volatile语义的增强,这个意义重大,Java并发工具包就是靠volatile语义来搞定可见性的。

而这里对这个可见性的实现是禁止这2段语句的重排,这个也是volatile的通俗功能说法会禁止指令重排序。

(4) 管程中锁的规则

这条规则是指一个锁的解锁Happens-Before于后续对这个锁的加锁

管程是一种通用的同步原语,在Java中是指synchronized,synchronized是Java里对管程的实现,管程中的锁在Java里隐式实现的,比如下面代码在进入同步代码块之前,会自动加锁,而在代码块执行完后会自动释放锁,加锁以及释放锁都是编译器帮我实现的:

synchronized (this) { //此处自动加锁
  // x是共享变量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此处自动解锁
复制代码

可以这样理解:假设x初始值为10,线程A执行完代码块后,x值为12,自动释放锁,线程B进入代码块时,能够看见线程A对x的写操作,即线程B能够看到x==12,这也是符合我们对synchronized的用法。

(5) 线程start()规则

Esta regla trata sobre el inicio del subproceso, es decir, después de que el subproceso principal A inicia el subproceso B, el subproceso B puede ver el funcionamiento del subproceso principal antes de iniciar el subproceso B , como el siguiente código:

Thread B = new Thread(()->{
  // 主线程调用B.start()之前
  // 所有对共享变量的修改,此处皆可见
  // 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();
复制代码

Esto también está en línea con nuestro sentido común, es decir, la operación start() de Happens-Before en cualquier operación del subproceso B.

(6) Reglas de combinación de subprocesos ()

Esta regla trata sobre la espera de subprocesos, es decir, el subproceso A espera a que se complete el subproceso B, es decir, el subproceso principal A llama al método join() del subproceso B. Cuando el subproceso B se completa, el subproceso principal puede ver la operación del subproceso, que se llama aquí Ver también se refiere a operaciones en variables compartidas .

Por ejemplo el siguiente código:


Thread B = new Thread(()->{
  // 此处对共享变量var修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66
复制代码

Este es el retorno de cualquier operación que suceda antes en el subproceso B a la operación join().

Resumir

El contenido de este capítulo es relativamente complicado, principalmente introduce el modelo de memoria de Java para resolver los problemas de visibilidad y orden antes mencionados, entre ellos, la regla Happens-Before es relativamente clara, que es esencialmente una regla de visibilidad, que es proporcionada por el modelo de memoria. La regla Happens-Before y las palabras clave volátiles y sincronizadas pueden resolver los problemas de visibilidad y orden. En el próximo artículo, veremos cómo resolver el problema atómico.

Supongo que te gusta

Origin juejin.im/post/7087488770420768782
Recomendado
Clasificación