Tabla de contenido
Nivel 1: Tres conceptos de programación concurrente
Nivel 2: use la palabra clave sincronizada para sincronizar hilos
¿Cuándo la programación concurrente plantea un problema de seguridad?
¿Cómo resolver el problema de seguridad del hilo?
Nivel 3: use el bloqueo de subprocesos (Lock) para lograr la sincronización de subprocesos
Uso correcto del método lock()
Nivel 4: uso de volátiles para lograr una visibilidad variable
¿Qué es la palabra clave volátil?
¿Puede la volatilidad garantizar la atomicidad?
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.
Web
Comprender 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:
x = 10; //语句1
y = x; //语句2
x++; //语句3
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 1
directamente 10
el valor a x
, es decir, ejecutar esta declaración escribirá directamente el valor 10
en la memoria, por lo que esto es atómico. La declaración 2 es en realidad dos operaciones, primero lee x
el 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, Java
el 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 synchronized
lograrlo lock
. Se introducirán lock
(bloqueo) y (sincronización). synchronized
en el siguiente nivel.
synchronized
Y lock
puede 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执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
Si el hilo de ejecución 1
es CPU1
, el hilo de ejecución 2
es CPU2
. Del análisis anterior, se puede ver que cuando el subproceso 1
ejecuta i =10
esta oración, primero cargará i
el valor inicial del CPU1
caché en el caché y luego asignará un valor 10
, luego el valor CPU1
en el caché se convierte, pero no es inmediatamente escrito en la memoria principal entre.i
10
En este momento, el hilo 2
se ejecuta j = i
, primero irá al i
valor leído de la memoria principal y lo cargará en CPU2
el caché. Tenga en cuenta que i
el valor en la memoria todavía está allí en este momento, luego 0
se hará j
el valor del valor 0
en lugar 10
de
Este es el problema de visibilidad.Después de que el subproceso modifica 1
la variable , el subproceso no ve inmediatamente el valor modificado por el subproceso 1.i
2
Para mayor visibilidad, Java
se proporcionan palabras clave Volatile
para garantizar que cuando se Volatile
modifica 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 Synchronized
y 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:
int a;
boolean flag;
i = 10; //语句1
flag = false; //语句2
El código anterior define una variable entera a
, una variable booleana flag
, y utiliza sentencias 1
y sentencias para asignar valores 2
a variables i
y 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 ( ).flag
1
2
1
2
Instruction 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:
int a = 3; //语句1
int b = 5; //语句2
a = a + 3; //语句3
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 4
depende del 3
resultado de la declaración, por lo que el procesador se asegurará de que la declaración se ejecute antes que 3
la declaración 4
.
Si Instruction 2
se debe utilizar el resultado de una instrucción Instruction 1
, el procesador garantiza que Instruction 1
se 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:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep();
}
doSomethingwithconfig(context);
Se puede encontrar que no hay dependencia de datos entre sentencias 1
y sentencias 2
, por lo que de acuerdo con las reglas de reordenación de instrucciones, la sentencia se puede ejecutar antes 2
que la sentencia Después de que se ejecuta la sentencia, la sentencia no 1
ha 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.2
1
2
inited
true
while
doSomethingwithconfig(context)
1
context
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 ThreadA
para ThreadB
operar 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, synchronized
y generalmente se usan Lock
para 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 synchronized
métodos o bloques de código para modificar objetos. Cuando un subproceso accede a este synchronized
mé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 synchronized
las 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 synchronized
palabras 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 1
esperará a que el subproceso 0
inserte los datos antes de ejecutar, lo que indica que el subproceso 0
y el subproceso 1
se ejecutan secuencialmente.
A partir de estos dos ejemplos, podemos saber que synchronized
las 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 synchronized
palabras clave:
-
Cuando un hilo llama a un método, no se puede acceder a
synchronized
otros métodos.La razón es muy simple, un objeto tiene solo un bloqueo;synchronized
-
Cuando un subproceso accede
synchronized
al método del objeto, otros subprocesos pueden acceder al no-synchronized
método del objeto, porque acceder a los no-métodossynchronized
no necesita adquirir un bloqueo y se puede acceder a ellos a voluntad; -
Si un subproceso A necesita acceder al método
object1
del 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.synchronized
fun1
object2
synchronized
fun1
object1
object2
bloque de código sincronizado
synchronized
Los 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:
synchronized(synObject) {
}
Cuando este fragmento de código se ejecuta en un subproceso, el subproceso adquirirá el synObject
bloqueo del objeto. En este momento, otros subprocesos no pueden acceder a este fragmento de código. synchronized
El valor del valor puede this
representar 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 synchronized
el 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 synchronized
dos formas de agregar bloqueos a los bloques de código.Se puede encontrar que agregar bloques de código es más flexible synchronized
que agregar directamente palabras clave a los métodos .synchronized
Cuando sychronized
modificamos 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, sychronized
pero usando sychronized
bloques 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 synchronized
el método de la clase a la que pertenece el objeto, la exclusión mutua no ocurrirá en este momento, porque el static synchronized
método de acceso ocupa un bloqueo de clase, y el access non- static synchronized
method 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 - End
Lea 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:
num
Só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 synchronized
las palabras clave en el nivel anterior. synchronized
Las 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 synchronized
modificado 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:
-
Después de que el subproceso ejecuta el bloque de código, el bloqueo se libera automáticamente;
-
El programa informa de un error y
jvm
permite que el subproceso libere automáticamente el bloqueo.
sleep
Puede 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.
synchronized
Es 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 synchronized
usamos 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 Lock
pueda realizar pasando.
En general , hay más funciones Lock
que synchronized
las proporcionadas y el grado de personalización es mayor. Lock
No 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:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Se puede encontrar que Lock
es 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 Lock
debe try{}catch{}
realizarse en un bloque, y la operación de liberación de la cerradura se finally
realiza en el bloque para garantizar que la cerradura debe liberarse para evitar bloqueos mutuos.
Un Lock
ejemplo de uso:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
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 true
puede false
encontrar 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 false
dentro del tiempo de espera , volverá true
.
Así que a menudo lo usamos así:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
Uso correcto del método lock()
Debido a Lock
que es una interfaz, generalmente usamos su clase de implementación cuando programamos. ReentrantLock
Es Lock
una 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 lock
es una variable local. THread-0
Es Thread-1
un 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 - End
Lea 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 volatile
las 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?
-
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. -
volatile
El "Reordenamiento de instrucciones" está deshabilitado cuando se decora una variable compartida .
Veamos primero un ejemplo:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
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 1
cuando el subproceso se ejecuta, copiará stop
el valor de la variable y lo colocará en su propia memoria de trabajo.
Luego, cuando el subproceso 2
cambia stop
el 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 stop
continuará en bucle porque no conoce el cambio de la variable por hilo 2
¿Cómo evitar esta situación?
Es muy simple, solo stop
agregue palabras clave a las variables .volatile
volatile
¿Qué efecto tienen las palabras clave?
-
El uso
volatile
de la palabra clave obligará a que el valor modificado de la variable se escriba en la memoria principal inmediatamente; -
Usando
volatile
la palabra clave, cuando el subproceso 2stop
modifica la variable, invalidará por la fuerza las líneas de cachéstop
en el caché correspondiente a todos los subprocesos que usan la variable .stop
-
Dado que la línea de caché del subproceso 1
stop
no es válida, el subproceso 1 leerá el valor de la variable en la memoria principal en tiempo de ejecuciónstop
.
Así que el último hilo 1
lee stop
el 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 volatile
se 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 copy
ejecú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 10000
y podemos tener dudas, no, lo anterior es realizar inc
una operación de autoincremento en la variable. Como volatile
la visibilidad está garantizada, entonces en cada subproceso inc
Una vez que se completa el autoincremento, el valor modificado se puede ver en otros subprocesos, por lo que un 10
subproceso ha realizado 1000
dos operaciones por separado, luego el inc
valor final debe ser 1000*10=10000
.
Mencionamos anteriormente volatile
que se puede garantizar la visibilidad, pero el error del programa anterior es que volatile
no 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 1
y escríbalo en la memoria de trabajo.
Imaginamos tal situación: el subproceso 1 inc
puede encontrarse con esta situación cuando opera el autoincremento de la variable, lea el valor de la variable, el valor en inc
este 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.inc
10
inc
inc
10
inc
inc
11
inc
inc
inc
10
inc
1
inc
11
Encontramos que ambos subprocesos realizan inc
una ronda de operaciones en , pero inc
el valor de 1
.
Tal vez todavía tengamos dudas, no, ¿no está garantizado que volatile
cuando 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-before
Esta 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 inc
valor no se modifica. Entonces, aunque volatile
se puede garantizar que el subproceso lee el valor de 2
la variable de la memoria, el subproceso no la ha modificado, por lo que el subproceso no verá el valor modificado en absoluto.inc
1
2
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 - End
Lea 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);
}
}