Conceptos básicos de subprocesos múltiples (3): Introducción a la palabra clave sincronizada y al modelo de memoria Java

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

@ [toc]

1. Temas de seguridad de subprocesos

Después de comprender algunos conceptos básicos de subprocesos múltiples de Java, ahora usamos subprocesos múltiples para resolver un problema práctico. Suponiendo que cada subproceso puede sumar un número a 100000, ahora usamos diez subprocesos, los agregamos al mismo tiempo y vemos si el resultado es 1000000. El código es el siguiente:

ackage com.dhb.concurrent.test;

import java.util.concurrent.CountDownLatch;

public class SyncDemo implements Runnable{

	private static int count = 0;
	static CountDownLatch countDownLatch = new CountDownLatch(10);

	public static void main(String[] args) throws InterruptedException{
		for(int i=0;i<10; i++) {
			Thread t = new Thread(new SyncDemo());
			t.start();
		}
		countDownLatch.await();
		System.out.println(count);

	}

	 private void add(){
		for (int i = 0; i < 100000; i++) {
			count++;
		}
		countDownLatch.countDown();
	}

	@Override
	public void run() {
		add();
	}
}
复制代码

En el código anterior, se inician 10 subprocesos respectivamente, ahora vea si el resultado de salida es 1000000.

361177
复制代码

El resultado no es el mismo que se esperaba, volvamos a hacerlo y vemos?

294781
复制代码

Cada vez es diferente. Esto significa que debe haber un problema de seguridad de subprocesos. Es decir, la variable miembro de cuenta que definimos, en el proceso de acceso simultáneo por 10 subprocesos, puede haber lecturas sucias, es decir, un subproceso no ha terminado de escribir y otro subproceso ha leído el resultado inconcluso, lo que lleva a The el resultado final no es 1000000. Por eso, para solucionar este problema, tenemos que pensar en una palabra clave, sincronizada para solucionarlo.

2. Instrucciones de uso de sincronizado

Los problemas de concurrencia generalmente necesitan resolver dos tipos de problemas. Uno es la exclusión mutua, es decir, los recursos solo pueden ser accedidos por un subproceso al mismo tiempo. Cuando este subproceso está en proceso de acceso, otros subprocesos no pueden acceder a esta variable. Esto es exclusión mutua. Otro problema es la sincronización, la sincronización es principalmente para resolver el problema de la comunicación entre subprocesos. Es decir, debido a que el subproceso no puede obtener el recurso de bloqueo requerido antes de acceder a esta variable, ingresará al estado de bloqueo y renunciará a la autoridad de ejecución de la CPU. Luego, cuando se puede volver a ejecutar, el subproceso que accede al recurso debe recibir una notificación después de que se complete la ejecución.Los métodos de espera y notificación son buenos métodos de sincronización de subprocesos. De hecho, sincronizado en inglés significa sincronización, pero lo que es más interesante es que sincronizado resuelve principalmente el problema de exclusión mutua. es decir, bloquear. Modificamos el código de la siguiente manera:

 private void add(){
	synchronized (SyncDemo.class) {
		for (int i = 0; i < 100000; i++) {
			count++;
		}
		countDownLatch.countDown();
	}
}
复制代码

Vuelva a comprobar el resultado de la ejecución:

1000000
复制代码

Efectivamente, la salida es el resultado deseado. Pero, ¿y si modificamos el bloque sunchronizado de la siguiente manera?

 private void add(){
	synchronized (this) {
		for (int i = 0; i < 100000; i++) {
			count++;
		}
		countDownLatch.countDown();
	}
}
复制代码

El resultado es el siguiente:

205975
复制代码

又不能满足了。这说明,synchronized代码块,括号中锁定的对象,是有讲究的,前面的SyncDemo.class,由于SyncDemo.class是个特殊的对象,只有一个对象。因此多线程访问的时候就会形成互斥。而改成this之后,由于这个类在使用的时候通过new,导致了多个实例,实例与实例之间加索就不能构成互斥关系。 另外,上述代码块也可以与如下情况等价:

 private synchronized void add(){
	for (int i = 0; i < 100000; i++) {
		count++;
	}
	countDownLatch.countDown();
}
复制代码

如果方法中除了代码块没有任何内容,那么这种方式与前面的synchronized(this)等价。 此外synchronized(SyncDemo.class)也与如下等价:

 private static synchronized void add(){
	for (int i = 0; i < 100000; i++) {
		count++;
	}
	countDownLatch.countDown();
}
复制代码

对,就是将方法改为静态方法,这样锁住的就是类了。我们总结一下:

分类 详细分类 被锁的对象 代码示例
方法 实例方法 类的实例对象 public synchronized void method(){ ... ... }
方法 实例方法 类对象 public static synchronized void method() { ... ... }
代码块 实例对象 类的实例对象 synchronized(this) { ... ... }
代码块 class对象 类对象 synchronized(SyncDemo.class){ ... ... }
代码块 任意实例对象Object 实例对象Object Object lock = new Object(); synchronized(lock){ ... ... }

理论上来说,synchronized()的括号中可以是任意对象。但是,需要注意的是, 一般我们最好不要用String和包装类做为被锁定的对象。 这是因为,在jvm中,对这些类进行特殊处理,String类,尤其是G1中要是开启了字符串去重,那么全部jvm中都只有这一个对象。这样会导致许多系统其他的功能受到影响。包装类由于有常量池,也会导致同样的问题,这样你会莫名其妙的感觉系统卡顿。

3.java的内存模型JMM

在前面学习伪共享的时候了解过,操作系统中,实际上CPU与主内存之间存在多级缓存架构。而这些多级高速缓存的速度远远高于主内存的读取速度。其结构如下: modelo de caché

高速缓存和主内存以及CPU的同步关系,需要通过缓存一致性协议来确保数据的一致性。如MESI、MSI等协议。通过这些协议,才能保证各内存高速缓存与主内存的数据一致性。这个模型如下图所示 modelo de caché

除了高速缓存之外,为了使CPU运算单元尽可能的充分利用,还会对输入的代码进行优化,其先后顺序会被改变。因此,在实际代码的执行过程中,其先后顺序不一定按照代码顺序来执行。这就是指令的重排序。关于这一点的细节再后续volatile关键字部分进行详细介绍。 那么JAVA实际上也是与这个模型类似,再java虚拟机中,虚拟机做为最外层的容器,其执行的逻辑与这个模型也非常相似。实际上,线程是CPU的最小执行单位,Java的内存模型实际上是对这个模型的抽象。在java中,也分为主内存和工作内存:

  • 主内存:java虚拟机规定,所有变量必须在主内存上产生,主内存也等价于是堆区。与前面的模型相比,这里的主内存可能是前面内存的一部分。
  • 工作内存:java虚拟机中的每个线程都有自己的工作内存,也就是线程的栈区。与前面的高速缓存相比,线程的工作过程中需要使用高速缓存。线程的工作内存实际上大部分内容在内存中,分配到CPU执行的时候,就会将需要执行的部分放入高速缓存。

那么java主内存和工作内存之间,也需要通过jvm的一些规则来保证数据的一致性。 需要说明的是,这两个模型只用于对比记忆,实际上二者并无直接关系。因为中间还有操作系统层的映射。而对于操作系统是如何在这两个模型之间转换的,还有很多内容本文并未涉及。 java内存模型如下: JMM

在java中,工作内存与主内存的交互,主要通过如下8种活动来进行,每个活动都是原子性的。

  • lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
  • unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
  • read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  • load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
  • use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
  • assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
  • store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

可以看到,上述图种绿色部分就是在工作内存种执行的活动。其他活动则是在主内存种执行。其过程详细如下图: proceso JMM

在每个线程中,其执行的时候的变量,实际上是其主内存中变量的副本。那么如果采用了synchronized,则会用lock操作锁定该变量,之后其他线程并无法访问。之后再进行read、load过程,之后使用或者赋值。 对于两个线程,分别从主内存中读取变量a和b的值,并不一样要read a; load a; read b; load b; 也会出现如下执行顺序:read a; read b; load b; load a; volatile修饰的变量则除外。 上述这些操作,JSR133规定,需要满足如下规则:

  • 1. Una de las operaciones de lectura y carga, almacenamiento y escritura no puede aparecer sola. Estos dos conjuntos de operaciones deben aparecer en pares. Después de leer, debe cargar, y después de almacenar, debe escribir. No está permitido que la memoria de trabajo no reciba después de leer, o que la memoria principal no reciba después de almacenar.
  • No se permite que un subproceso descarte la operación de asignación más reciente. Su valor modificado debe sincronizarse con la memoria principal.
  • Las variables solo se pueden crear en la memoria principal. No se permite el uso directo de una variable no inicializada en la memoria de trabajo. La carga debe ejecutarse antes de que se realicen las operaciones de uso y asignación.
  • Una variable solo puede ser bloqueada por un subproceso a la vez, lo que logra la exclusión mutua. También es la esencia de nuestro uso de sincronizado.
  • No está permitido realizar la operación de desbloqueo en variables sin bloqueos. Si un subproceso no realiza bloqueos, definitivamente no está permitido realizar desbloqueos. Por supuesto, tampoco está permitido realizar desbloqueos en variables que están bloqueadas por otros hilos.
  • Antes de desbloquear una variable, la variable debe sincronizarse nuevamente en la memoria principal. Es decir, después de ejecutar write.

4. Resumen

Este artículo presenta el uso de sincronización de problemas de seguridad de subprocesos. Y una breve introducción al modelo de memoria java. Por supuesto, sincronizado y reentrante, y el conocimiento de las prácticas y optimizaciones específicas subyacentes también son partes muy importantes. Esto se describirá en detalle más adelante.

Supongo que te gusta

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