Conceptos básicos de subprocesos múltiples (4): problemas de volatilidad y visibilidad y el principio de que sucede antes

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

@[toc] Conozca el uso de sincronizado en el artículo anterior, y la composición básica del modelo de memoria java JMM. Así que ahora, echemos un vistazo a un nuevo problema.

1. Problemas de visibilidad

Echemos un vistazo al siguiente código:

package com.dhb.concurrent.test;

import java.util.concurrent.TimeUnit;

public class VolatileTest {

	private static int INIT = 0;
	private static int MAX = 10;

	public static void main(String[] args) {

		new Thread(() -> {
			int local = INIT;
			while (local < MAX) {
				if (local != INIT) {
					System.out.println("Get change for INIT [" + INIT + "] local is [" + local + "]");
					local = INIT;
				}
			}
		}, "Reader Thread").start();

		new Thread(() -> {
			int local = INIT;
			while (local < MAX) {
				System.out.println("Update value [" + local + "] to [" + (local++) + "]");
				INIT = local;
				try {
					TimeUnit.MILLISECONDS.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "Writer Thread").start();
	}

}
复制代码

Hay dos subprocesos, un subproceso escribe y el otro subproceso lee, por lo que el resultado del subproceso de escritura puede ser leído por el subproceso de lectura cada vez. Ejecutamos el código anterior:

Update value [0] to [0]
Get change for INIT [1] local is [0]
Update value [1] to [1]
Update value [2] to [2]
Update value [3] to [3]
Update value [4] to [4]
Update value [5] to [5]
Update value [6] to [6]
Update value [7] to [7]
Update value [8] to [8]
Update value [9] to [9]
复制代码

Se puede ver que mientras el hilo de escritura incrementa el resultado INIT, el hilo de lectura solo lee el primer valor. Después de eso, no importa cómo el hilo de escritura cambie el valor de INIT, en el hilo de lectura, este valor sigue siendo el resultado anterior, es decir, el valor agregado por el hilo de escritura, que en realidad es invisible para el hilo de lectura. Ese es el punto que debemos cubrir hoy, el tema de la visibilidad.leer invisible

Como se muestra en la figura anterior, cada hilo es en realidad una copia de la variable que opera en su propia memoria de trabajo.Si se modifica esta copia, se sincronizará de nuevo con la memoria principal. Entonces, el escritor escribe el resultado en INIT en la memoria principal cada vez. Sin embargo, para el hilo Reader, solo se leerá una vez al comparar después de leer. El valor en este momento es 1. Después de eso, el valor no se modificará, pero el valor siempre se colocará en el hilo en la memoria de trabajo. . Aquí es donde surge el problema de la visibilidad. De hecho, dado que el subproceso Reader solo lee, solo hay una operación de uso dentro, por lo que no se asignará, por lo que no es necesario cargar esta variable desde la memoria principal cada vez. La intención original de este diseño es aumentar la eficiencia del cálculo de la memoria JVM, porque la parte real de la memoria de trabajo se puede calcular en la memoria caché de la CPU. Si se carga desde la memoria principal cada vez, la velocidad de la memoria caché y la memoria principal es muy diferente, entonces causará una sobrecarga innecesaria del sistema. Entonces, ¿cómo deberíamos resolver este problema? Esta es la palabra clave clave que este artículo necesita presentar, volátil. Esta palabra clave tiene dos efectos:

  • mantener la visibilidad de la memoria
  • Reordenación de instrucciones en reposo

我们先来说内存的可见性。volatile是如何保持内存的可见性呢?很简单,实际上如果某个变量被volatile修饰的话,那么在工作内存中,就不再走工作内存的缓存了,而是每次都去主内存去加载。这样一来,虽然带来了一些性能的损耗,但是这样可以更好的解决系统的一致性。 leer visible

如上图,这样一来,Reader线程的工作内存中在对INIT的读取的时候,每次都会从主内存中去同步。这样Writer线程对主内存的修改,对Reader线程来说,就是可见的了。我们修改代码如下:

public class VolatileTest {

	private volatile static int INIT = 0;
	private static int MAX = 10;

	public static void main(String[] args) {

		new Thread(() -> {
			int local = INIT;
			while (local < MAX) {
				if (local != INIT) {
					System.out.println("Get change for INIT [" + INIT + "] local is [" + local + "]");
					local = INIT;
				}
			}
		}, "Reader Thread").start();

		new Thread(() -> {
			int local = INIT;
			while (local < MAX) {
				System.out.println("Update value [" + local + "] to [" + (local++) + "]");
				INIT = local;
				try {
					TimeUnit.MILLISECONDS.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "Writer Thread").start();
	}

}
复制代码

执行结果:

Update value [0] to [0]
Get change for INIT [1] local is [0]
Update value [1] to [1]
Get change for INIT [2] local is [1]
Update value [2] to [2]
Get change for INIT [3] local is [2]
Update value [3] to [3]
Get change for INIT [4] local is [3]
Update value [4] to [4]
Get change for INIT [5] local is [4]
Update value [5] to [5]
Get change for INIT [6] local is [5]
Update value [6] to [6]
Get change for INIT [7] local is [6]
Update value [7] to [7]
Get change for INIT [8] local is [7]
Update value [8] to [8]
Get change for INIT [9] local is [8]
Update value [9] to [9]
Get change for INIT [10] local is [9]
复制代码

这样reader线程每次都能读到最新的INIT的值了。

2.指令重排序

我们来看下面这个例子:

package com.dhb.concurrent.test;

public class VolatileTest2 {

	static int a,b,x,y;

	public static void main(String[] args) {
		long time = 0;
		while (true) {
			time ++;
			a = 0;
			b = 0;
			x = 0;
			y = 0;
			Thread t1 = new Thread(() -> {
				a = 1;
				x = b;
			});
			Thread t2 = new Thread(() -> {
				b = 1;
				y = a;
			});
			t1.start();
			t2.start();
			try {
				t1.join();
				t2.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			if( x==0 && y==0) {
				break;
			}
		}
		System.out.println("time:["+time+"] x:["+x+"] y:["+y+"]");
	}
}
复制代码

对于指令重排序,如果在一个线程中,两条语句之间彼此没有任何关系,那么在jvm内部对指令进行优化的时候,就可以出现,写在后面的语句被优化到前面执行的情况。在这个例子中, a=1、x=b 这是两条没有任何关系的语句,同样,在另外一个线程中b=1、y=a也如此。这些语句彼此间没有先后关系。而在传统的逻辑中,这两种组合,无论哪个线程先执行或者同时执行,都不会出现x=0,y=0的情况。

执行情况 结果
t1先于t2执行 x=0,y=1
t2先于t1执行 x=1,y=0
t1、t2同时执行 x=1,y=1
只有当出现指令重排序,x=a,y=b先于a=1,b=1执行,那么才可能出现x=0,y=0的情况。
我们执行上述代码:
```
time:[102946] x:[0] y:[0]
```
这个指令重排序不是一个必然事件,因此这个代码每次执行的结果都不一样:
```
time:[8943] x:[0] y:[0]
```
如果运气好可能很快就会出现这个结果。
这就是指令重排序问题。那么volatile的另外一个作用就是能够禁止指令重排序,这种情况就不会产生。这也是一个比较常见的面试问题,DCL的单例模式需要加volatile的的原因。

3.synchronized与可见性

在JMM中,关于synchronized有两条规定:

  • 线程解锁之前,必须把共享变量的最新值刷新到主内存中。
  • 线程加锁的时候,将清空工作内存中的共享变量的值,从而使共享变量需要从主内存中重新获取最新的值。(加锁与解锁是同一个锁)

由此可见,synchronized,实际上也能实现可见性。此外,synchronized使用的是同步锁,还具有原子性。 再回到前文的例子中,我们将这个程序改造为如下方式:

package com.dhb.concurrent.test;

import java.util.concurrent.TimeUnit;

public class SyncTest {

	private static Count count = new Count();
	private static int MAX = 10;

	public static void main(String[] args) {

		new Thread(() -> {
			int local = count.getCount();
			while (local < MAX) {
				synchronized (count) {
					if (local != count.getCount()) {
						System.out.println("Get change for INIT [" + count.getCount() + "] local is [" + local + "]");
						local = count.getCount();
					}
				}
			}
		}, "Reader Thread").start();

		new Thread(() -> {

			int local = count.getCount();
			while (local < MAX) {
				synchronized (count) {
					System.out.println("Update value [" + local + "] to [" + (++local) + "]");
					count.setCount(local);
				}
				try {
					TimeUnit.MILLISECONDS.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

		}, "Writer Thread").start();


	}

	private static class Count {
		int count = 0;

		public int add() {
			return ++count;
		}

		public int getCount() {
			return count;
		}

		public void setCount(int count) {
			this.count = count;
		}
	}

}
复制代码

之后我们再来看执行结果:

Update value [0] to [1]
Get change for INIT [1] local is [0]
Update value [1] to [2]
Get change for INIT [2] local is [1]
Update value [2] to [3]
Get change for INIT [3] local is [2]
Update value [3] to [4]
Get change for INIT [4] local is [3]
Update value [4] to [5]
Get change for INIT [5] local is [4]
Update value [5] to [6]
Get change for INIT [6] local is [5]
Update value [6] to [7]
Get change for INIT [7] local is [6]
Update value [7] to [8]
Get change for INIT [8] local is [7]
Update value [8] to [9]
Get change for INIT [9] local is [8]
Update value [9] to [10]
Get change for INIT [10] local is [9]
复制代码

这样通过synchronized也能很好的解决可见性问题。由于synchronized在1.8中已经做了很多优化,其性能与ReentrantLock的性能无差别,因此,只要不涉及到指令的重排序,通过synchronized也能很好的完成可见性的效果。 candado visible

如上图所示,在加锁之后,每次读写的时候都需要从主内存中刷新同步。

4.Happens-Before规则

在《深入理解Java虚拟机》一书中,对Happens-Before原则进行了归纳,主要是以下8个:

  • 1.程序次序规则:在同一线程内部,按照代码顺序,关联代码书写在前面的操作优先发生于书写在后面的操作。
  • 2.管程锁定规则:一个unlock操作优先发生于此后对同一个锁的lock操作。
  • 3.volatile变量规则:对一个变量的写操作优先发生于后面对这个变量的读操作。(时间先后)
  • 4.线程启动规则:Thread的start方法优先发生于此线程的每一个动作。
  • 5.线程终结规则:线程中所有操作都优先发生于线程的终止检测,我们可以通过Thread.join()、Thread.isAlive()返回值等手段进行检测。
  • 6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到事件的发生。
  • 7.对象终结规则:一个对象的初始化完成先于发生他的finalize()方法的开始。
  • 8.传递性规则:如果操作A对操作B可见,而操作B又对操作C可见,则可以得出操作A对操作C也可见。

¿Cómo entender estas reglas de Happens-Before? Ocurre antes no significa que la operación anterior ocurra antes de la operación posterior. Más bien, pretende expresar que el resultado de una operación anterior es visible para las operaciones posteriores. De hecho, Happens-Before restringe el comportamiento del compilador. El compilador puede optimizar el orden de ejecución de múltiples códigos según sea necesario. Sin embargo, esta optimización del compilador debe cumplir con la regla Happens-Before.

5. Comprensión de lo que sucede antes

5.1 Reglas de orden del programa

Cabe señalar que para esta regla, cuando una jvm está optimizando, si no hay dependencia entre el código escrito, no cumple con esta regla. El siguiente código:

int a = 3;     //代码1
int b = a + 1; //代码2
复制代码

Debido a las dependencias, el resultado del código 1 siempre es visible para el código 2.

int a = 3; //代码1
int b = 2; //代码2
复制代码

Entonces, el código anterior no tendrá una relación de orden y las instrucciones de JVM se optimizarán según sea necesario. El orden después de la optimización no es necesariamente el mismo. Puede ser el código 1 primero o el código 2 primero.

5.2 Reglas de bloqueo del monitor

Esta regla se entiende bien, es decir, cuando se necesita bloquear una cerradura, primero se debe ejecutar la operación de desbloqueo sobre ella.

5.3 reglas de variables volátiles

Si la variable se modifica por volatile, la operación de escritura en esta variable será visible para todas las operaciones de lectura posteriores. Esto también se demuestra en los ejemplos anteriores de este artículo.

public class VolatileTest {

	private volatile static int INIT = 0;
	private static int MAX = 10;

	public static void main(String[] args) {

		new Thread(() -> {
			int local = INIT;
			while (local < MAX) {
				if (local != INIT) {
					System.out.println("Get change for INIT [" + INIT + "] local is [" + local + "]");
					local = INIT;
				}
			}
		}, "Reader Thread").start();

		new Thread(() -> {
			int local = INIT;
			while (local < MAX) {
				System.out.println("Update value [" + local + "] to [" + (local++) + "]");
				INIT = local;
				try {
					TimeUnit.MILLISECONDS.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}, "Writer Thread").start();
	}

}
复制代码

5.4 Reglas transitivas

Si la operación A ocurre antes que la operación B, y la operación B ocurre antes que la operación C, se puede concluir que la operación A ocurre antes que la operación C. Esto es algo similar a la regla transitiva en matemáticas. Si A>B, B>C, entonces A>C.

package com.dhb.concurrent.test;

public class VolatileExample {

	static  int x = 0;
	static volatile boolean v = false;

	public static void main(String[] args) {
		Thread t1 = new Thread(() -> {
			x = 42;
			v = true;
		});

		Thread t2 = new Thread(() -> {
			if(v == true){
				System.out.println(x);
			}else {
				System.out.println(x);
			}
		});

		t1.start();
		t2.start();
		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
复制代码

En el ejemplo anterior, si t1 debe ejecutarse primero, de acuerdo con la regla transitiva, la salida resultante debe ser 42.

Supongo que te gusta

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