Educoder/Touge JAVA——Características avanzadas de JAVA: Conceptos básicos de subprocesos múltiples (3) Sincronización de subprocesos

Tabla de contenido

Nivel 1: Tres conceptos de programación concurrente

detalles de la misión

información relacionada

1. atomicidad

2. Visibilidad

3. Orden

requisitos de programación

Nivel 2: use la palabra clave sincronizada para sincronizar hilos 

información relacionada

¿Cuándo la programación concurrente plantea un problema de seguridad?

¿Cómo resolver el problema de seguridad del hilo?

palabra clave sincronizada

bloque de código sincronizado

requisitos de programación

Nivel 3: use el bloqueo de subprocesos (Lock) para lograr la sincronización de subprocesos

información relacionada

Interfaz de bloqueo

Uso correcto del método lock()

requisitos de programación

Nivel 4: uso de volátiles para lograr una visibilidad variable

información relacionada

¿Qué es la palabra clave volátil?

¿Puede la volatilidad garantizar la atomicidad?

requisitos de programación


Nivel 1: Tres conceptos de programación concurrente

detalles de la misión

Cuando desarrollamos aplicaciones, muchas veces prestamos atención a la concurrencia del sitio web, si hay muchos usuarios en el sitio web, cuando estos usuarios acceden a un servicio al mismo tiempo, nuestro servidor recibirá una gran cantidad de solicitudes concurrentes. manejar bien estas solicitudes concurrentes, un trabajo que debe completar un programador calificado.

WebComprender los tres conceptos de la programación concurrente es muy útil para desarrollar mejor las aplicaciones de alta concurrencia .

La tarea de este nivel es comprender los tres conceptos importantes de la programación concurrente y completar las preguntas de opción múltiple a la derecha.

información relacionada

1. atomicidad

Atomicidad: es decir, una operación o varias operaciones se ejecutan por completo y no se interrumpirán por ningún factor durante la ejecución, o no se ejecutan.

Echemos un vistazo al siguiente código:

  1. x = 10; //语句1
  2. y = x; //语句2
  3. x++; //语句3
  4. x = x + 1; //语句4

Ahora juzgue cuál de estos códigos es una operación atómica.

Puede pensar que las cuatro declaraciones son operaciones atómicas, pero de hecho solo la declaración 1 es una operación atómica. La declaración asigna 1directamente 10el valor a x, es decir, ejecutar esta declaración escribirá directamente el valor 10en la memoria, por lo que esto es atómico. La declaración 2 es en realidad dos operaciones, primero lee xel valor y luego lo asigna a y, estas dos pasos son atómicos, pero no son operaciones atómicas juntas, y lo mismo es cierto para las dos últimas afirmaciones.

En otras palabras, solo la simple lectura y asignación (debe asignar un número a una variable, y la asignación entre variables no es una operación atómica) son operaciones atómicas.

Como se puede ver en lo anterior, Javael modelo de memoria solo garantiza que las lecturas y asignaciones básicas sean operaciones atómicas. Si desea lograr atomicidad a gran escala, puede usar y para synchronizedlograrlo lock. Se introducirán lock(bloqueo) y (sincronización). synchronizeden el siguiente nivel.

synchronizedY lockpuede garantizar que solo un subproceso ejecute el bloque de código en cualquier momento, por lo que se garantiza la atomicidad.

2. Visibilidad

La visibilidad significa que cuando varios subprocesos acceden a una variable, un subproceso cambia el valor de la variable y otros subprocesos pueden conocer el cambio de inmediato.

Por ejemplo:

  1. //线程1执行的代码
  2. int i = 0;
  3. i = 10;
  4. //线程2执行的代码
  5. j = i;

Si el hilo de ejecución 1es CPU1, el hilo de ejecución 2es CPU2. Del análisis anterior, se puede ver que cuando el subproceso 1ejecuta i =10esta oración, primero cargará iel valor inicial del CPU1caché en el caché y luego asignará un valor 10, luego el valor CPU1en el caché se convierte, pero no es inmediatamente escrito en la memoria principal entre.i10

En este momento, el hilo 2se ejecuta j = i, primero irá al ivalor leído de la memoria principal y lo cargará en CPU2el caché. Tenga en cuenta que iel valor en la memoria todavía está allí en este momento, luego 0se hará jel valor del valor 0en lugar 10de

Este es el problema de visibilidad.Después de que el subproceso modifica 1la variable , el subproceso no ve inmediatamente el valor modificado por el subproceso 1.i2

Para mayor visibilidad, Javase proporcionan palabras clave Volatilepara garantizar que cuando se Volatilemodifica una variable, se asegurará de que el valor modificado se reescriba en la memoria principal de inmediato, y cuando otros subprocesos quieran llamar a la variable compartida, irá a la memoria principal para reescribir leer.

Pero las variables compartidas ordinarias no pueden garantizar la visibilidad, porque las variables ordinarias se leerán en la propia memoria del subproceso. Cuando un subproceso se modifica, es posible que no tenga tiempo de actualizar a la memoria principal, y otros subprocesos leerán de la memoria principal la variable. Por lo tanto, otros subprocesos aún pueden tener el valor original al leerlo, por lo que las variables compartidas ordinarias no pueden garantizar la visibilidad.

En cuanto a la garantía de visibilidad, también se puede conseguir mediante Synchronizedy lock.

3. Orden

Ordenamiento: Es decir, la ejecución del programa se ejecuta en el orden en que se escribe el código, como el siguiente ejemplo:

  1. int a;
  2. boolean flag;
  3. i = 10; //语句1
  4. flag = false; //语句2

El código anterior define una variable entera a, una variable booleana flag, y utiliza sentencias 1y sentencias para asignar valores 2a variables iy sentencias. Parece que la sentencia está antes que la sentencia , pero ¿ se ejecutará la sentencia antes que la sentencia cuando el programa esté corriendo ? No necesariamente, porque aquí puede ocurrir el reordenamiento de instrucciones ( ).flag1212Instruction Reorder

¿ Qué es el reordenamiento de instrucciones ?

En términos generales, para mejorar la eficiencia de ejecución, el procesador optimizará el código de entrada. No garantiza que el orden en que se ejecuta el código sea coherente con el orden en que se escribe el código, pero se asegura de que el la salida del programa es consistente con el orden en que se ejecuta el código .

Por ejemplo, en el código anterior, quién ejecuta primero la declaración 1 o la declaración 2 no tiene efecto en el resultado final del programa, por lo que es posible que la declaración 2 se ejecute primero y la declaración 1 se ejecute más tarde durante la ejecución.

Pero cabe señalar que aunque el procesador reordenará las instrucciones, garantizará que el resultado final del programa será el mismo que el resultado de la ejecución secuencial del código, entonces, ¿en qué garantía se basa? Considere el siguiente ejemplo:

  1. int a = 3; //语句1
  2. int b = 5; //语句2
  3. a = a + 3; //语句3
  4. b = b + a + 4; //语句4

El resultado de la ejecución del programa anterior puede ser: 语句2 => 语句1 => 语句3 => 语句4.

¿Es posible que sea: 语句2 => 语句1 => 语句4 => 语句3 qué?

Imposible, porque el procesador considerará las dependencias entre los datos al ejecutar la declaración. La declaración del código anterior 4depende del 3resultado de la declaración, por lo que el procesador se asegurará de que la declaración se ejecute antes que 3la declaración 4.

Si Instruction 2se debe utilizar el resultado de una instrucción Instruction 1, el procesador garantiza que Instruction 1se ejecutará antes Instruction 2.

Aunque el reordenamiento de instrucciones no afectará el resultado de ejecución final de un solo subproceso, ¿lo afectará en el caso de subprocesos múltiples? Veamos un ejemplo:

  1. //线程1:
  2. context = loadContext(); //语句1
  3. inited = true; //语句2
  4. //线程2:
  5. while(!inited ){
  6. sleep();
  7. }
  8. doSomethingwithconfig(context);

Se puede encontrar que no hay dependencia de datos entre sentencias 1y sentencias 2, por lo que de acuerdo con las reglas de reordenación de instrucciones, la sentencia se puede ejecutar antes 2que la sentencia Después de que se ejecuta la sentencia, la sentencia no 1ha comenzado a ejecutarse y el hilo puede comenzar a ejecutarse. En este momento , saltará. El bucle se vuelve a ejecutar y la declaración no se ha ejecutado en este momento, y no se ha inicializado, lo que hará que el programa informe un error.212initedtruewhiledoSomethingwithconfig(context)1context

Se puede ver en los ejemplos anteriores que el reordenamiento de instrucciones no afectará la ejecución de un solo hilo, pero afectará la ejecución de múltiples hilos.

En otras palabras, para asegurar la corrección de la ejecución de un programa de subprocesos múltiples, se debe garantizar la atomicidad, la visibilidad y el orden. Mientras uno no esté garantizado, puede causar que el programa se ejecute incorrectamente.

requisitos de programación

levemente

Nivel 2: use la palabra clave sincronizada para sincronizar hilos 

información relacionada

¿Cuándo la programación concurrente plantea un problema de seguridad?

No habrá problemas de seguridad en un solo subproceso, pero es muy probable que ocurra en una situación de varios subprocesos, por ejemplo: varios subprocesos acceden al mismo recurso compartido al mismo tiempo y varios subprocesos insertan datos en la base de datos en el mismo momento. mismo tiempo Si no hacemos nada, es muy probable que los resultados reales de los datos no coincidan con nuestros resultados esperados.

Ejemplo: ahora hay dos subprocesos para obtener los datos ingresados ​​por el usuario al mismo tiempo y luego insertar los datos en la misma tabla, sin requerir datos duplicados.

Debemos realizar las siguientes operaciones a la hora de insertar datos:

  • Compruebe si los datos existen en la base de datos;

  • No inserte si existe, de lo contrario inserte.

Ahora hay dos hilos ThreadApara ThreadBoperar en la base de datos, cuando en un momento determinado los hilos A y B leen datos X al mismo tiempo, en ese momento ambos van a la base de datos para verificar si existe X, y los resultados obtenidos son todos inexistente Entonces A, Thread B ha insertado datos X en la base de datos En este momento, hay dos datos X en la base de datos, pero todavía hay duplicación de datos.

Este es un problema de seguridad de subprocesos. Cuando varios subprocesos acceden a un recurso al mismo tiempo, el resultado de la ejecución del programa no es el que desea ver .

Aquí, este recurso se llama: recurso crítico (también llamado recurso compartido).

Pueden surgir problemas de seguridad de subprocesos cuando varios subprocesos acceden a recursos críticos (un objeto, propiedades dentro de un objeto, un archivo, una base de datos, etc.) al mismo tiempo.

Cuando varios subprocesos ejecutan un método, las variables locales dentro del método no son recursos críticos, porque el método se ejecuta en la pila y la pila de Java es privada para el subproceso, por lo que no habrá problemas de seguridad de subprocesos.

Cómo resolver el problema de seguridad del hilo

¿Cómo resolver el problema de seguridad del hilo?

Básicamente, todas las soluciones a los problemas de seguridad de subprocesos utilizan el método de "acceso a recursos críticos serializados", es decir, solo un subproceso opera recursos críticos al mismo tiempo, y otros subprocesos solo pueden operar después de que se completa la operación, lo que también se denomina sincrónico. acceso de exclusión mutua.

En Java, synchronizedy generalmente se usan Lockpara lograr el acceso de exclusión mutua síncrono.

palabra clave sincronizada

En primer lugar, echemos un vistazo a los mutexes , mutexes: bloqueos que pueden lograr el acceso de exclusión mutua.

Si se agrega un mutex a una variable, solo un subproceso puede acceder a la variable al mismo tiempo, es decir, cuando un subproceso accede a un recurso crítico, otros subprocesos solo pueden esperar.

En Java, cada objeto tiene una marca de bloqueo (monitor), también conocida como monitor.Cuando varios subprocesos acceden a un objeto, solo se puede acceder al bloqueo que adquiere el objeto.

Cuando escribimos código, podemos usar synchronizedmétodos o bloques de código para modificar objetos. Cuando un subproceso accede a este synchronizedmétodo de objeto o bloque de código, adquiere el bloqueo de este objeto. En este momento, no se puede acceder a otros objetos y solo pueden esperar. El método del objeto solo se puede ejecutar después de que el subproceso que ha adquirido el bloqueo termine de ejecutar el método o el bloque de código.

Veamos un ejemplo para comprender mejor synchronizedlas palabras clave:

public class Example {
    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
        new Thread() {
            public void run() {
                insertData.insert(Thread.currentThread());
            };
        }.start();
    }  
}
class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public void insert(Thread thread){
        for(int i=0;i<5;i++){
            System.out.println(thread.getName()+"在插入数据"+i);
            arrayList.add(i);
        }
    }
}

这段代码的执行是随机的(每次结果都不一样):

Thread-0在插入数据0
Thread-1在插入数据0
Thread-1在插入数据1
Thread-1在插入数据2
Thread-1在插入数据3
Thread-1在插入数据4
Thread-0在插入数据1
Thread-0在插入数据2
Thread-0在插入数据3
Thread-0在插入数据4

Ahora agreguemos synchronizedpalabras clave para ver los resultados de la ejecución:

public synchronized void insert(Thread thread){
     for(int i=0;i<5;i++){
        System.out.println(thread.getName()+"在插入数据"+i);
        arrayList.add(i);
    }
}
输出:

Thread-0在插入数据0
Thread-0在插入数据1
Thread-0在插入数据2
Thread-0在插入数据3
Thread-0在插入数据4
Thread-1在插入数据0
Thread-1在插入数据1
Thread-1在插入数据2
Thread-1在插入数据3
Thread-1在插入数据4

Se puede encontrar que el subproceso 1esperará a que el subproceso 0inserte los datos antes de ejecutar, lo que indica que el subproceso 0y el subproceso 1se ejecutan secuencialmente.

A partir de estos dos ejemplos, podemos saber que synchronizedlas palabras clave pueden realizar la sincronización de métodos y el acceso de exclusión mutua.

Hay varias cuestiones que necesitan nuestra atención al usar synchronizedpalabras clave:

  1. Cuando un hilo llama a un método, no se puede acceder a synchronizedotros métodos.La razón es muy simple, un objeto tiene solo un bloqueo;synchronized

  2. Cuando un subproceso accede synchronizedal método del objeto, otros subprocesos pueden acceder al no- synchronizedmétodo del objeto, porque acceder a los no-métodos synchronizedno necesita adquirir un bloqueo y se puede acceder a ellos a voluntad;

  3. Si un subproceso A necesita acceder al método object1del objeto , otro subproceso B necesita acceder al método del objeto , incluso si es del mismo tipo), no habrá ningún problema de seguridad del subproceso, porque acceden a diferentes objetos, por lo que No hay problema de exclusión mutua.synchronizedfun1object2synchronizedfun1object1object2

bloque de código sincronizado

synchronizedLos bloques de código son muy útiles para nosotros para optimizar el código de subprocesos múltiples. Primero, echemos un vistazo a cómo se ve:

  1. synchronized(synObject) {
  2. }

Cuando este fragmento de código se ejecuta en un subproceso, el subproceso adquirirá el synObjectbloqueo del objeto. En este momento, otros subprocesos no pueden acceder a este fragmento de código. synchronizedEl valor del valor puede thisrepresentar el objeto actual o un atributo del objeto. Uso de objeto Cuando la propiedad del objeto, significa el bloqueo de la propiedad del objeto.

Con synchronizedel bloque de código, podemos modificar el ejemplo anterior de agregar datos en las siguientes dos formas:

class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public void insert(Thread thread){
        synchronized (this) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
}
class InsertData {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Object object = new Object();
    public void insert(Thread thread){
        synchronized (object) {
            for(int i=0;i<100;i++){
                System.out.println(thread.getName()+"在插入数据"+i);
                arrayList.add(i);
            }
        }
    }
}

Los códigos anteriores son synchronizeddos formas de agregar bloqueos a los bloques de código.Se puede encontrar que agregar bloques de código es más flexible synchronizedque agregar directamente palabras clave a los métodos .synchronized

Cuando sychronizedmodificamos un método con palabras clave, este método solo puede ser accedido por un hilo al mismo tiempo, pero a veces solo se necesita sincronizar una parte del código, y en este momento es imposible modificar el método con palabras clave, sychronizedpero usando sychronizedbloques de código Esta función se puede realizar.

Y si un subproceso ejecuta un no-método de un objeto static synchronized, y otro subproceso necesita ejecutar static synchronizedel método de la clase a la que pertenece el objeto, la exclusión mutua no ocurrirá en este momento, porque el static synchronizedmétodo de acceso ocupa un bloqueo de clase, y el access non- static synchronizedmethod ocupa un objeto lock. , por lo que no hay exclusión mutua.

Veamos un trozo de código:

public class Test {
    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            public void run() {
                insertData.insert();
            }
        }.start(); 
        new Thread(){
            public void run() {
                insertData.insert1();
            }
        }.start();
    }  
}
class InsertData { 
    public synchronized void insert(){
        System.out.println("执行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackT\frace();
        }
        System.out.println("执行insert完毕");
    }
    public synchronized static void insert1() {
        System.out.println("执行insert1");
        System.out.println("执行insert1完毕");
    }
}
执行结果:

执行insert
执行insert1
执行insert1完毕
执行insert完毕

requisitos de programación

Begin - EndLea atentamente el código de la derecha y complemente el código en el área de acuerdo con las indicaciones del método Las tareas específicas son las siguientes:

  • numSólo un subproceso puede acceder a la variable a la vez .
package step2;

public class Task {

	public static void main(String[] args) {
		
		final insertData insert = new insertData();
		
		for (int i = 0; i < 3; i++) {
			new Thread(new Runnable() {
				public void run() {
					insert.insert(Thread.currentThread());
				}
			}).start();
		}				
	}
}
class insertData{
	
	public static int num =0;
	
	/********* Begin *********/
	public synchronized void insert(Thread thread){
		for (int i = 0; i <= 5; i++) {
			num++;
			System.out.println(num);
		}
	}

	/********* End *********/
}

Nivel 3: use el bloqueo de subprocesos (Lock) para lograr la sincronización de subprocesos

información relacionada

Hablamos de synchronizedlas palabras clave en el nivel anterior. synchronizedLas palabras clave se utilizan principalmente para sincronizar el código y realizar un acceso de exclusión mutua síncrono, es decir, solo un subproceso puede acceder a los recursos críticos al mismo tiempo. Para resolver el problema de seguridad del hilo.

Si un método o bloque de código es synchronizedmodificado por palabras clave, cuando un subproceso adquiere el bloqueo del método o bloque de código, otros subprocesos no pueden continuar accediendo al método o bloque de código.

Si otros subprocesos quieren poder acceder al método o al bloque de código, deben esperar a que el subproceso que adquirió el bloqueo lo libere, y aquí solo hay dos casos para liberar el bloqueo:

  1. Después de que el subproceso ejecuta el bloque de código, el bloqueo se libera automáticamente;

  2. El programa informa de un error y jvmpermite que el subproceso libere automáticamente el bloqueo.

sleepPuede haber una situación en la que un subproceso adquiera el bloqueo del objeto y se bloquee por alguna razón (esperando IO, llamando a métodos) durante la ejecución. En este momento, el bloqueo aún está en manos del subproceso bloqueado, mientras que otros subprocesos están bloqueados en este momento. No hay otra manera que esperar. Pensemos en cómo afectará esto a la eficiencia del programa.

synchronizedEs una palabra clave proporcionada por Java, que es muy conveniente de usar, pero en algunos casos tiene muchas limitaciones. Por lo tanto, debe haber un mecanismo para evitar que el subproceso en espera espere indefinidamente (por ejemplo, solo esperar un cierto período de tiempo o poder responder a las interrupciones), lo que se puede hacer pasando Lock.

Por ejemplo, cuando varios subprocesos operan en el mismo archivo, leer y escribir al mismo tiempo entrarán en conflicto, y escribir al mismo tiempo también entrará en conflicto, pero leer al mismo tiempo no entrará en conflicto, y si lo synchronizedusamos una pregunta:

Si varios subprocesos solo realizan operaciones de lectura, entonces cuando un subproceso realiza operaciones de lectura, otros subprocesos solo pueden esperar y no pueden realizar operaciones de lectura.

Por lo tanto, se necesita un mecanismo para que cuando varios subprocesos solo realicen operaciones de lectura, no haya conflictos entre los subprocesos y se Lockpueda realizar pasando.

En general , hay más funciones Lockque synchronizedlas proporcionadas y el grado de personalización es mayor. LockNo está integrado en el lenguaje Java, sino en una clase.

Interfaz de bloqueo

Echemos un vistazo a lo que se ha mencionado en repetidas ocasiones.Primero Lock, veamos su código fuente:

  1. public interface Lock {
  2. void lock();
  3. void lockInterruptibly() throws InterruptedException;
  4. boolean tryLock();
  5. boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  6. void unlock();
  7. Condition newCondition();
  8. }

Se puede encontrar que Lockes una interfaz, en la cual: lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()el método se usa para adquirir el bloqueo, y unlock()el método se usa para liberar el bloqueo.

El primer lock()método es el método más utilizado, que se utiliza para adquirir bloqueos. Espere si el bloqueo ya ha sido adquirido por otro subproceso.

Como se mencionó anteriormente, si lo usa Lock, debe liberar activamente el bloqueo, y cuando ocurre una excepción, el bloqueo no se liberará automáticamente. Por lo tanto, en términos generales, el uso Lockdebe try{}catch{}realizarse en un bloque, y la operación de liberación de la cerradura se finallyrealiza en el bloque para garantizar que la cerradura debe liberarse para evitar bloqueos mutuos.

Un Lockejemplo de uso:

  1. Lock lock = ...;
  2. lock.lock();
  3. try{
  4. //处理任务
  5. }catch(Exception ex){
  6. }finally{
  7. lock.unlock(); //释放锁
  8. }

tryLock()Como su nombre lo indica, se usa para intentar adquirir el bloqueo, y el método tiene un valor de retorno, que indica si la adquisición es exitosa o no, la adquisición regresa con éxito y la falla regresa. Desde el método, se truepuede falseencontrar que si el método no adquiere el bloqueo, no seguirá esperando, y devolverá el valor directamente.

tryLock()La función del método sobrecargado tryLock(long time, TimeUnit unit)es similar, excepto que este método esperará un período de tiempo para adquirir el bloqueo, si no se adquiere el bloqueo después del tiempo de espera, regresará, y si se adquiere falsedentro del tiempo de espera , volverá true.

Así que a menudo lo usamos así:

  1. Lock lock = ...;
  2. if(lock.tryLock()) {
  3. try{
  4. //处理任务
  5. }catch(Exception ex){
  6. }finally{
  7. lock.unlock(); //释放锁
  8. }
  9. }else {
  10. //如果不能获取锁,则直接做其他事情
  11. }

Uso correcto del método lock()

Debido a Lockque es una interfaz, generalmente usamos su clase de implementación cuando programamos. ReentrantLockEs Lockuna clase de implementación de la interfaz, lo que significa "bloqueo de reentrada". A continuación, usaremos un ejemplo para aprender la lock()forma correcta de usar el método.

Ejemplo 1:

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public static void main(String[] args)  {
        final Test test = new Test();
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();    //注意这个地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}
输出:

Thread-1得到了锁
Thread-0得到了锁
Thread-0释放了锁
Thread-1释放了锁

El resultado puede estar más allá de sus expectativas. No, debería ser que un subproceso obtenga el bloqueo y otros subprocesos no puedan obtener el bloqueo. ¿Por qué sucede esto? Porque la variable insert()en el método lockes una variable local. THread-0Es Thread-1un bloqueo diferente al adquirido, por lo que el hilo no esperará.

Entonces, ¿cómo podemos usarlo para lock()lograr la sincronización? Creo que ya lo ha pensado, simplemente defina Lock como una variable global.

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}
结果:

Thread-0得到了锁
Thread-0释放了锁
Thread-1得到了锁
Thread-1释放了锁

Este es el resultado que esperábamos.

Muchas veces, para mejorar la eficiencia del programa, no queremos que el hilo esté bloqueado todo el tiempo para esperar el bloqueo, en este momento podemos usarlo para lograr el objetivo tryLock().

Ejemplo, modifique el insert()método anterior para tryLock()implementar:

 public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"得到了锁");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
}
输出:

Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁

requisitos de programación

Begin - EndLea atentamente el código de la derecha y complemente el código en el área de acuerdo con las indicaciones del método .

package step3;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Task {

	public static void main(String[] args) {
		final Insert insert = new Insert();
		Thread t1 = new Thread(new Runnable() {
			public void run() {
				insert.insert(Thread.currentThread());
			}
		});
		Thread t2 = new Thread(new Runnable() {
			public void run() {
				insert.insert(Thread.currentThread());
			}
		});
		Thread t3 = new Thread(new Runnable() {
			public void run() {
				insert.insert(Thread.currentThread());
			}
		});
		// 设置线程优先级
		t1.setPriority(Thread.MAX_PRIORITY);
		t2.setPriority(Thread.NORM_PRIORITY);
		t3.setPriority(Thread.MIN_PRIORITY);
		t1.start();
		t2.start();
		t3.start();
	}
}
class Insert {
	public static int num;
	// 在这里定义Lock
    private Lock lock = new ReentrantLock(); 

	public void insert(Thread thread) {
		/********* Begin *********/
		if(lock.tryLock()){
			try{ //处理任务
				System.out.println(thread.getName()+"得到了锁");
				for (int i = 0; i < 5; i++) {
					num++;
					System.out.println(num);
				}
			}finally{
				System.out.println(thread.getName()+"释放了锁");
				lock.unlock();   //释放锁
			}
		}else{ //如果不能获取锁,则直接做其他事情
			System.out.println(thread.getName()+"获取锁失败");
		}
		/********* End *********/
	}
}

Nivel 4: uso de volátiles para lograr una visibilidad variable

información relacionada

En la programación concurrente volatilelas palabras clave juegan un papel muy importante, así que vayamos directo al tema.

¿Qué es la palabra clave volátil?

volatile¿Para qué sirve, cuáles son sus significados y características?

  1. Cuando se modifica una variable compartida volatile, tiene "visibilidad", es decir, cuando la variable es modificada por un subproceso, el cambio será conocido inmediatamente por otros subprocesos.

  2. volatileEl "Reordenamiento de instrucciones" está deshabilitado cuando se decora una variable compartida .

Veamos primero un ejemplo:

  1. //线程1
  2. boolean stop = false;
  3. while(!stop){
  4. doSomething();
  5. }
  6. //线程2
  7. stop = true;

Debido a que el hilo no proporciona directamente un método de parada, a menudo usamos el código anterior cuando queremos interrumpir el hilo.

Sin embargo, hay un problema con este código: cuando se ejecuta el subproceso 2, ¿puede este código garantizar que el subproceso 1 se interrumpirá? En la mayoría de los casos sí, pero es posible que el hilo 1 no se pueda interrumpir.

¿Por qué es posible que el hilo no se pueda interrumpir? Cada subproceso tiene su propia memoria de trabajo durante la ejecución, por lo que 1cuando el subproceso se ejecuta, copiará stopel valor de la variable y lo colocará en su propia memoria de trabajo.

Luego, cuando el subproceso 2cambia stopel valor de la variable, pero no ha tenido tiempo de escribirlo en la memoria principal, el subproceso 2 pasa a hacer otras cosas, luego el subproceso 1 stopcontinuará en bucle porque no conoce el cambio de la variable por hilo 2

¿Cómo evitar esta situación?

Es muy simple, solo stopagregue palabras clave a las variables .volatile

volatile¿Qué efecto tienen las palabras clave?

  1. El uso volatilede la palabra clave obligará a que el valor modificado de la variable se escriba en la memoria principal inmediatamente;

  2. Usando volatilela palabra clave, cuando el subproceso 2 stopmodifica la variable, invalidará por la fuerza las líneas de caché stopen el caché correspondiente a todos los subprocesos que usan la variable .stop

  3. Dado que la línea de caché del subproceso 1 stopno es válida, el subproceso 1 leerá el valor de la variable en la memoria principal en tiempo de ejecución stop.

Así que el último hilo 1lee stopel valor más reciente.

¿Puede la volatilidad garantizar la atomicidad?

Antes aprendimos sobre las tres características de los hilos: atomicidad, visibilidad y orden.

En el ejemplo anterior, sabemos que volatilese puede garantizar la visibilidad de las variables compartidas, pero volatile¿se puede garantizar la atomicidad?

Vamos a ver:

public class Test {
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

Pensemos en la salida de este programa y luego copyejecútelo localmente para ver el efecto.

Tal vez el resultado que queremos debería ser: 10000, pero el resultado final de ejecución muchas veces no se alcanza 10000y podemos tener dudas, no, lo anterior es realizar incuna operación de autoincremento en la variable. Como volatilela visibilidad está garantizada, entonces en cada subproceso incUna vez que se completa el autoincremento, el valor modificado se puede ver en otros subprocesos, por lo que un 10subproceso ha realizado 1000dos operaciones por separado, luego el incvalor final debe ser 1000*10=10000.

Mencionamos anteriormente volatileque se puede garantizar la visibilidad, pero el error del programa anterior es que volatileno se puede garantizar la atomicidad del programa.

Sabemos que los incrementos variables no son atómicos. Consiste en dos pasos:

1. Leer el valor de la variable;

2. Sume el valor de la variable 1y escríbalo en la memoria de trabajo.

Imaginamos tal situación: el subproceso 1 incpuede encontrarse con esta situación cuando opera el autoincremento de la variable, lea el valor de la variable, el valor en inceste momento es , el subproceso 1 está bloqueado cuando la operación de autoincremento aún no se ha realizado Entonces el subproceso 2 opera sobre la variable. Tenga en cuenta que el valor en este momento sigue siendo el mismo. El subproceso 2 ha realizado una operación de autoincremento sobre la variable. No va al valor leído en la memoria principal, porque ya está en su caché, así que continúe con la operación anterior. Tenga en cuenta que el valor en el caché del subproceso 1 es en este momento, y se agrega el valor del subproceso 1. Igual a , luego escriba en la memoria principal.inc10incinc10incinc11incincinc10inc1inc11

Encontramos que ambos subprocesos realizan incuna ronda de operaciones en , pero incel valor de 1.

Tal vez todavía tengamos dudas, no, ¿no está garantizado que volatilecuando se modifica una variable se invalida la línea de caché? Luego, otros subprocesos leerán el nuevo valor cuando lo lean, sí, esto es correcto. happens-beforeEsta es la regla de la variable en las reglas anteriores volatile, pero debe tenerse en cuenta que después de que el subproceso 1 lea la variable, si está bloqueada, el incvalor no se modifica. Entonces, aunque volatilese puede garantizar que el subproceso lee el valor de 2la variable de la memoria, el subproceso no la ha modificado, por lo que el subproceso no verá el valor modificado en absoluto.inc12

Después de entender esto, podemos entender que la raíz de este problema es que la operación de autoincremento no es atómica.

Es muy simple resolver este problema, simplemente haga que la operación de autoincremento sea atómica.

¿Cómo garantizar la atomicidad, cómo hacer que el código anterior resulte 10000?

Simplemente use el conocimiento que aprendimos en los dos primeros niveles. Piense en el código específico usted mismo. Después de todo, lo que sale de su propia mente es suyo.

requisitos de programación

Begin - EndLea atentamente el código de la derecha y complemente el código en el área de acuerdo con las indicaciones del método . ####Instrucción de prueba

Salida esperada: 10000.

Consejo: hay dos formas de lograr la atomicidad, por lo que hay muchas formas de pasar este nivel.

package step4;

public class Task {
	public volatile int inc = 0;
//请在此添加实现代码
/********** Begin **********/
	public synchronized void increase() {
		inc++;
}
/********** End **********/
	public static void main(String[] args) {
		final Task test = new Task();
		for (int i = 0; i < 10; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 1000; j++)
						test.increase();
				};
			}.start();
		}
		while (Thread.activeCount() > 1) // 保证前面的线程都执行完
			Thread.yield();
		System.out.println(test.inc);
	}
}

Supongo que te gusta

Origin blog.csdn.net/zhou2622/article/details/128382708
Recomendado
Clasificación