La historia de la programación concurrente: el modelo compartido de concurrencia

Modelo compartido de concurrencia


1. Problemas de uso compartido causados ​​por subprocesos múltiples

Por ejemplo, cuando dos subprocesos utilizan un recurso compartido. El subproceso 1 realiza una operación +1 en la variable i = 0, pero cambia al subproceso 2 en el camino a +1. El subproceso 2 realiza una operación -1 en la variable i y luego devuelve -1 a la variable compartida. Pero volver al subproceso 1 ya es +1, es decir, la variable local ya es 1, y reasignar a la variable compartida causará problemas de concurrencia. La segunda es que si el recurso compartido siempre es utilizado por un subproceso, el subproceso
puede se desperdicia debido al tiempo de suspensión, espera, io y otras operaciones. Tiempo de uso de la CPU, luego puedes darle este tiempo a otros.

Análisis del problema
El código de bytes de i++ es
getstatic i para obtener la variable estática i, que es la i de la memoria principal.

iconst_1 preparar constante 1

iadd realiza la suma

yo pustático

Entonces, el i ++ que vemos no es una instrucción atómica. Dado que no es una instrucción atómica, el hilo naturalmente puede hacer que estas instrucciones se intercalen durante el proceso de conmutación. Al final, el recurso compartido es un problema de datos sucios.

Una sección crítica
es en realidad un recurso compartido en el código al que acceden varios subprocesos, por lo que este código es una sección crítica.

Condición de carrera
Si en la sección crítica, varios subprocesos ejecutan diferentes secuencias de instrucciones, lo que da como resultado resultados impredecibles, es una condición de carrera.

2. Solución

① Después de bloquear el hilo de sincronización
1, el hilo 2 no puede obtener el bloqueo y no puede ejecutar la sección crítica. El hilo 2 se bloquea y espera a que el hilo 1 complete la liberación del bloqueo antes de poder usarlo. Sincronizar se puede comparar con una habitación: cada vez que una persona con una cerradura puede ingresar a la habitación para hacer cosas, incluso si se agota el tiempo de la CPU, otros subprocesos no pueden ingresar a la habitación sin la llave de la cerradura. Cuando termine, se liberará el bloqueo y se despertarán los hilos bloqueados.
Insertar descripción de la imagen aquí

static int count=0;
    static Object lock=new Object();
    public static void main(String[] args) throws InterruptedException {
    
    
        Room room = new Room();
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 5000; i++) {
    
    
//                room.increment();
                synchronized (lock){
    
    
                    count++;
                }

            }
        }, "t1");

        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 5000; i++) {
    
    
//                room.decrement();
                synchronized (lock){
    
    
                    count--;
                }

            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
//        log.debug("{}", room.getCounter());
        log.debug("{}",count);
    }

Pensando
① ¿Qué pasa si sincronizar se coloca afuera? De hecho, el bloqueo se liberará solo después de que se ejecute todo el for: atomicidad
② ¿Qué sucede si t1 y t2 usan obj diferentes? Es equivalente a ingresar a diferentes habitaciones, entonces el bloqueo no tiene ningún efecto y los dos subprocesos aún ejecutarán los bloques de código en la sección crítica, lo que dará como resultado diferentes secuencias de ejecución.
③¿Qué pasa si t1 está bloqueado pero t2 no? Eso equivale a que t2 pueda ejecutar la sección crítica en cualquier momento.

La mejora orientada a objetos
consiste en encapsular todas las secciones críticas y recursos compartidos que deben bloquearse en una clase. Agregar sincronización al método equivale a bloquear esto. Si es un método estático, equivale a bloquear la clase object.class

class Room {
    
    
    private int counter = 0;

    public synchronized void increment() {
    
    
        counter++;
    }

    public synchronized void decrement() {
    
    
        counter--;
    }

    public synchronized int getCounter() {
    
    
        return counter;
    }
}

Tres, sincronizar en el método.

Ocho bloqueos de subprocesos
Caso 1: cuando no hay suspensión, n1 bloquea su propio objeto al ejecutar los métodos a y b. Entonces son mutuamente excluyentes.
Situación 2: Con el sueño, sigue siendo lo mismo: los dos subprocesos que ejecutan los métodos a y b verán quién obtiene el bloqueo primero. Entonces quien lo ejecute primero. Independientemente de si hay sueño en él, los subprocesos que no han adquirido el bloqueo tienen que esperar.

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    
    
    public static void main(String[] args) {
    
    
        Number n1 = new Number();
//        Number n2 = new Number();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.b();
        }).start();
    }
}
@Slf4j(topic = "c.Number")
class Number{
    
    
    public synchronized void a() {
    
    
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
    
    
        log.debug("2");
    }
}

Caso 3: En este caso c no está bloqueado, es decir, se puede ejecutar a voluntad. Los resultados posibles son 3 12, 32 1 o 23 1.

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    
    
    public static void main(String[] args) {
    
    
        Number n1 = new Number();
//        Number n2 = new Number();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.b();
        }).start();

        new Thread(() -> {
    
    
            log.debug("begin");
            n1.c();
        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{
    
    
    public synchronized void a() {
    
    
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
    
    
        log.debug("2");
    }

    public void c(){
    
    
        log.debug("3");
    }
}

Caso 4: Los candados que unen son todos diferentes, lo que equivale a no tener candado. El resultado final debe ser 21. Porque 1 hilo está durmiendo

@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
    
    
    public static void main(String[] args) {
    
    
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(() -> {
    
    
            log.debug("begin");
            n1.a();
        }).start();
        new Thread(() -> {
    
    
            log.debug("begin");
            n2.b();
        }).start();

//        new Thread(() -> {
    
    
//            log.debug("begin");
//            n1.c();
//        }).start();

    }
}
@Slf4j(topic = "c.Number")
class Number{
    
    
    public synchronized void a() {
    
    
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
    
    
        log.debug("2");
    }
//
//    public void c(){
    
    
//        log.debug("3");
//    }
}

En los casos 5 a 8, se agrega estática, así que simplemente juzgue si tienen el mismo bloqueo en función de los objetos que bloquean.

4. Análisis de variables de seguridad de subprocesos.

¿Existen problemas de seguridad de subprocesos con variables estáticas y variables miembro?
Si es solo lectura, entonces no hay nada, si es lectura y escritura, debes prestar atención a la sección crítica.

¿Variables locales?
Si es un tipo de referencia, entonces sí.
El valor de las variables locales se almacena en el marco de pila del hilo, que es privado. En lugar de sacar la variable del área del método como una variable estática y luego modificarla en consecuencia.
Insertar descripción de la imagen aquí
Situación 1: la lista ThreadUnsafe se crea en la clase, lo que provocará problemas de seguridad del subproceso causados ​​por el subproceso que procesa la misma lista en el montón.

Caso 2: coloque la lista en el método1, luego es una referencia a una variable local y cada hilo tiene su propia lista después de llamar al método. Entonces no causará problemas de seguridad del hilo.

Situación 3: si una subclase anula este método, también puede obtener la lista, lo que provoca que varios subprocesos operen la lista. La solución es agregar final al método para evitar que las subclases lo anulen.

public class TestThreadSafe {
    
    

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
    
    
//        ThreadSafeSubClass test = new ThreadSafeSubClass();
        ThreadUnsafe test=new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
    
    
            new Thread(() -> {
    
    
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}
class ThreadUnsafe {
    
    
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
    
    
        for (int i = 0; i < loopNumber; i++) {
    
    
            method2();
            method3();
        }
    }

    private void method2() {
    
    
        list.add("1");
    }

    private void method3() {
    
    
        list.remove(0);
    }
}

class ThreadSafe {
    
    
    public final void method1(int loopNumber) {
    
    
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
    
    
            method2(list);
            method3(list);
        }
    }

    public void method2(ArrayList<String> list) {
    
    
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
    
    
        System.out.println(1);
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    
    
//    @Override
    public void method3(ArrayList<String> list) {
    
    
        System.out.println(2);
        new Thread(() -> {
    
    
            list.remove(0);
        }).start();
    }
}

Insertar descripción de la imagen aquí
Clase segura para subprocesos
Integer
HashTable
String Clase de vector
aleatorio en JUC

Sus métodos individuales son seguros para subprocesos, pero es diferente cuando se ejecutan varios métodos.
El problema con el siguiente código es que el hilo 1 cambia después de juzgar con éxito, simplemente libera el bloqueo, y luego el hilo 2 adquiere el bloqueo para juzgar, luego cambia el hilo 1 nuevamente para adquirir el bloqueo y procesa la colocación, y el hilo de cambio 2 también puede adquirir La cerradura y el proceso se ponen. Porque el bloqueo se liberará después de que se ejecute un único método. Por lo tanto, esto aún debe bloquearse en su totalidad para continuar con el procesamiento.
Insertar descripción de la imagen aquí
String inmutable y seguro para subprocesos
String
y Integer son inmutables, y String es esencialmente una matriz char[]. Si es el método de subcadena, en realidad copia una nueva matriz y luego asigna un valor a la matriz de caracteres de String. Reemplazar en realidad crea una matriz y luego compara el valor anterior de la matriz deseada. Si es el valor anterior, asigna directamente un nuevo valor a la posición de la nueva matriz.

public String replace(char oldChar, char newChar) {
    
    
        if (oldChar != newChar) {
    
    
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
    
    
                if (val[i] == oldChar) {
    
    
                    break;
                }
            }
            if (i < len) {
    
    
                //创建新数组
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
    
    
                    buf[j] = val[j];
                }
                while (i < len) {
    
    
                    char c = val[i];
                    //根据原来的数组是旧值的位置改变成新值
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
 public String substring(int beginIndex) {
    
    
        if (beginIndex < 0) {
    
    
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
    
    
            throw new StringIndexOutOfBoundsException(subLen);
        }
     //实际上就是创建了一个新的String,而不是修改了值
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}


  public String(char value[], int offset, int count) {
    
    
        if (offset < 0) {
    
    
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
    
    
            if (count < 0) {
    
    
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
    
    
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
    
    
            throw new StringIndexOutOfBoundsException(offset + count);
        }
      //实际上就是创建新数组并且进行复制
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

Análisis de ejemplo:
este tipo de hilo no es seguro porque MyServlet se comparte y UserService está en el montón. Varios subprocesos pueden llamar a sus métodos para modificar el recuento, lo que puede provocar problemas de concurrencia.
Insertar descripción de la imagen aquí
Esto también tiene problemas de concurrencia: el uso compartido singleton puede ser invocado por múltiples subprocesos, cubriendo variables como el inicio.
Insertar descripción de la imagen aquí
Esto no causa problemas de seguridad de subprocesos porque no se pueden modificar variables.
Insertar descripción de la imagen aquí
Aquí se producen problemas de seguridad de subprocesos porque la conexión está expuesta y puede ser procesada por varios subprocesos. Si el subproceso 1 cambia al subproceso 2 al procesar getCon y se cierra, entonces la conexión del subproceso 1 no puede continuar ejecutándose porque ha sido modificada.
Insertar descripción de la imagen aquí
Aquí no hay ningún problema de seguridad de subprocesos porque un UserDao creado cada vez es equivalente a una variable local. No es un recurso compartido.
Insertar descripción de la imagen aquí
Pueden surgir problemas de seguridad de subprocesos aquí porque las variables locales están expuestas a través de métodos de subclase y pueden ser modificadas por objetos de subclase a través de subprocesos. Resumen: si la
Insertar descripción de la imagen aquí
seguridad de los subprocesos depende de si se pueden modificar. Recursos compartidos

5. Ejercicios

El problema de seguridad del subproceso ocurre cuando varios subprocesos pueden estar vendiendo boletos y el recuento de la variable compartida se elimina al mismo tiempo. El número de recuento final es el valor procesado por el último subproceso. En lugar de valores procesados ​​conjuntamente, debido a que se produjo un error en su secuencia de ejecución, otro subproceso leyó el recuento sin esperar el procesamiento.

La solución es bloquear la sección crítica, que en realidad es la venta de la ventana. Entonces, ¿por qué no bloquear la lista de cantidades y la ventana? La razón es que no operan el mismo recurso compartido y lo manejan de manera diferente, por lo que no es necesario bloquearlo. Sin embargo, las dos operaciones anteriores de HashTable, get y put, estaban dirigidas al mismo recurso compartido, lo que provocó que el último hilo sobrescribiera el último valor. Debido a que el hilo 1 no se ha completado, el juicio vacío es exitoso. Además, las dos operaciones no están bloqueadas, sino que una se realiza y la otra se realiza.

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);

        // 所有线程的集合
        List<Thread> threadList = new ArrayList<>();
        // 卖出的票数统计
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                // 买票

                try {
    
    
                    Thread.sleep(random(10));
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                int amount = window.sell(random(5));
                // 统计买票数
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }

        for (Thread thread : threadList) {
    
    
            thread.join();
        }

        // 统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int random(int amount) {
    
    
        return random.nextInt(amount) + 1;
    }
}

// 售票窗口
class TicketWindow {
    
    
    private int count;

    public TicketWindow(int count) {
    
    
        this.count = count;
    }

    // 获取余票数量
    public int getCount() {
    
    
        return count;
    }

    // 售票 synchronized
    public  int sell(int amount) {
    
    
        if (this.count >= amount) {
    
    
            this.count -= amount;
            return amount;
        } else {
    
    
            return 0;
        }
    }
}

Vector自己的方法就已经带锁

public synchronized boolean add(E e) {
    
    
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

Problema de transferencia
Los problemas de seguridad de subprocesos realmente ocurren aquí. Lo principal es que cuando a transfiere dinero, b también transfiere dinero, entonces lo que obtiene b definitivamente es a sin transferencia, y lo mismo ocurre con a. Entonces, ¿está bien agregar sincronización directamente al método? Si vinculas un objeto de este tipo, obviamente no funcionará, porque hay dos cuentas con cerraduras diferentes y entran en habitaciones diferentes. Entonces la solución es usar la única clase Account.class, luego se puede bloquear

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 1000; i++) {
    
    
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
    
    
        return random.nextInt(100) + 1;
    }
}

// 账户
class Account {
    
    
    private int money;

    public Account(int money) {
    
    
        this.money = money;
    }

    public int getMoney() {
    
    
        return money;
    }

    public void setMoney(int money) {
    
    
        this.money = money;
    }

    // 转账
    public void transfer(Account target, int amount) {
    
    
        synchronized(Account.class) {
    
    
            if (this.money >= amount) {
    
    
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

6. Monitorear

El encabezado del objeto Java
incluye markword, que almacena principalmente código hash, edad (valor de vida de gc), si sesgo_lock es un bloqueo sesgado, 01 situación de bloqueo
y klassword, que apunta principalmente a objetos de clase (información de clase).
Si es una matriz, también incluye la longitud de la matriz
Insertar descripción de la imagen aquí
. Bloqueo del monitor El
bloqueo del monitor lo proporciona el sistema operativo y es muy costoso.
El principio de funcionamiento
es en realidad registrar la dirección del monitor en los primeros 30 bits de la palabra de marca. de obj y apunte al monitor. Luego, si un hilo quiere ejecutar el código de la sección crítica, indique al propietario del monitor el hilo correspondiente. Si entra otro hilo, vea si obj está asociado con un candado y luego vea si hay un propietario. Si es así, ingrese EntryList para bloquear y esperar. Después de esperar a que el hilo libere el bloqueo, active EntryList y comience la competencia nuevamente.
Insertar descripción de la imagen aquí
Desde la perspectiva del código de bytes,
la perspectiva del código de bytes consiste en copiar primero la referencia del candado a la ranura 1, luego monitorear la entrada, apuntar la palabra de marca del candado al monitor y almacenar el código hash, etc. en el monitor. Luego se ejecuta el código comercial y finalmente se retira la ranura de referencia 1 y luego se desbloquea monitorexit. Además, el código comercial, es decir, el bloque de sincronización, se monitorea en busca de excepciones. Si ocurre una excepción, la operación de desbloqueo aún se realizará.
Insertar descripción de la imagen aquí

7. Sincronizar la optimización

Breve historia:
si no hay competencia entre dos hilos, se puede usar un candado liviano, que equivale a colgar una mochila escolar, si se descubre que es la mochila escolar de la otra parte, espere afuera. Más tarde, si el otro hilo ya no es necesario, entonces el otro hilo puede grabar el nombre en el exterior de la puerta, lo que equivale a una cerradura sesgada. Si alguien viene a competir en este momento, será ascendido a escuela. bolsa, que es un candado liviano. Más tarde, otro hilo regresó y descubrió que el hilo grababa nombres en muchas puertas, por lo que fue al sistema operativo para grabar esos nombres en lotes. Eso es para modificar el bloqueo de polarización. Al final, se grabaron demasiados nombres y se canceló la preferencia.

Los bloqueos sesgados son exclusivos de un solo hilo. Si un solo hilo procesa un determinado código sin competencia, entonces se pueden usar bloqueos sesgados. Si hay competencia, se puede actualizar a un bloqueo liviano.
1. Bloqueo ligero
La esencia es que el hilo llama al registro de bloqueo del marco de la pila del método de área temporal para guardar la referencia del objeto y la información de la marca del objeto. El siguiente paso es intercambiar la información de bloqueo del registro de bloqueo correspondiente con obj. Por ejemplo, cambiar de 01 a 00 le dice a obj que se trata de un candado liviano y le dice a obj la dirección del registro de bloqueo, lo que equivale a etiquetar a obj quién. Es. Etiqueta de bloqueo. Si se trata de un bloqueo reentrante, entonces la parte de marca del registro de bloqueo es nula, lo que indica que es reentrante y utiliza el mismo bloqueo.
Insertar descripción de la imagen aquí
2. La expansión de la cerradura
se produce en realidad cuando se compite por cerraduras livianas y no hay lugar para que los subprocesos de la competencia las almacenen. En este momento, es necesario convertir las cerraduras livianas en monitores de cerraduras pesados. De hecho, es señalar la marca de objeto al monitor. Luego, el propietario del monitor señala el registro de bloqueo del hilo actual. Coloque el hilo bloqueado en la cola de espera.
Durante la recuperación, CAS intentó restaurar el registro de bloqueo del hilo al pasado, pero descubrió que fallaba. En este momento, el método de recuperación se cambia al método de recuperación de bloqueo pesado, la lista se activa, luego el propietario se establece en nulo y el hilo compite por el monitor nuevamente. De lo contrario, restaure la información del código hash guardada por el monitor.
Insertar descripción de la imagen aquí
3. La optimización del giro
equivale en realidad a esperar un semáforo: si llega pronto al semáforo en verde, espere un rato, y si tarda mucho, tire del freno de mano. Girar es girar por un tiempo y esperar a que otros suelten el bloqueo de peso pesado. Si tiene éxito una vez, la próxima vez seguramente aumentará la probabilidad de éxito y aumentará el número de giros. Si no espera, bloquea.
El motivo del giro: el bloqueo hará que el cambio de contexto del hilo consuma tiempo y recursos de la CPU. La velocidad es relativamente lenta.

4. Bloqueo sesgado
La razón por la que se utiliza el bloqueo sesgado es porque el bloqueo liviano llama a CAS para comparar cada vez que el bloqueo vuelve a entrar. CAS es una operación de instrucción del sistema operativo, por lo que es muy lento. Por lo tanto, el bloqueo sesgado consiste en asignar directamente el ThreadId a la palabra de marca, de modo que la palabra de marca se pueda comparar directamente en Java la próxima vez.
Los bloqueos sesgados se retrasan y, por lo general, no se generarán hasta un tiempo después de que se crea el objeto. Genere
primero los bloqueos sesgados: "bloqueos ligeros -" bloqueos pesados
. Si se utilizan bloqueos sesgados para secciones críticas, entonces la identificación del hilo de ejecución correspondiente es asignado a markword.
Si se usa Si falta el código hash del bloqueo, entonces el bloqueo sesgado se desactivará porque el código hash ocupa demasiados bits. El
ligero registra el código hash en el registro de bloqueo y el pesado lo registra en el monitor. .
Si dos subprocesos usan el mismo bloqueo de nivel de polarización, entonces el bloqueo se volverá no sesgable y se actualizará a un bloqueo liviano. La
Insertar descripción de la imagen aquí
repolarización por lotes
en realidad consiste en múltiples subprocesos sin competencia, que utilizan el mismo bloqueo. Si la jvm encuentra que el Si el número de desviaciones de bloqueo canceladas excede 20 veces, automáticamente se desviará a otro hilo. Por ejemplo, el hilo t1 usa un montón de bloqueos y los bloqueos están sesgados hacia t1. Pero si t2 usa estos bloqueos y necesita revocar el bloqueo pero lo desvía más de 20 veces, entonces todos estos bloqueos se desviarán hacia t2.

Deshacer por lotes:
si el deshacer supera las 40 veces, jvm deshará el sesgo de todos los objetos.

5. Eliminación del bloqueo:
De hecho, JIT descubre que no hay recursos compartidos en la sección crítica del bloqueo, por lo que cancela el bloqueo.
Insertar descripción de la imagen aquí

8. espera y notifica

Breve historia:
Xiaonan necesita cigarrillos para trabajar, pero también tiene que ocupar la cerradura para evitar que otros entren. Luego, abrir un juego de espera en este momento equivale a dejar que Xiaonan vaya al salón para relajarse y desbloquear la cerradura. Si llega humo, notifique a Xiaonan que puede continuar trabajando.

La diferencia entre bloqueado y en espera
es en realidad que esperar libera el bloqueo, mientras que bloqueado significa que no hay bloqueo.
Después de que se notifica la espera, aún necesita ingresar a la lista de entrada para esperar
Insertar descripción de la imagen aquí
las reglas de espera y notificar.
Cuando el hilo llama al El objeto espera y notifica, debe usar este bloqueo para convertirse en propietario del monitor. De esta forma, podrás esperar a liberar el candado y ser avisado por otras personas que adquieran el candado.

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    
    
    final static Object obj = new Object();

    public static void main(String[] args) {
    
    

        new Thread(() -> {
    
    
            synchronized (obj) {
    
    
                log.debug("执行....");
                try {
    
    
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
    
    
            synchronized (obj) {
    
    
                log.debug("执行....");
                try {
    
    
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
    
    
//            obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

El método wait() puede limitar el tiempo de espera, esperar(parámetro)

9. dormir y esperar

La diferencia es
dormir: llamada de subproceso, método estático y no liberará el bloqueo.
esperar: todos los obj, pero debe usarse con sincronizado para liberar el bloqueo.
Extensión
. Generalmente, el bloqueo se agregará con final para evitar que se modificado.

Uso correcto
Xiaonan necesita humo para funcionar. Si usa el sueño sin soltar la cerradura, otras personas que necesiten esperar para trabajar esperarán. Pero esperar puede permitir que Xiaonan libere el bloqueo, dejar que otros subprocesos funcionen y despertar a Xiaonan.
Hay un problema
. ¿Hay otros subprocesos esperando el bloqueo? Si es así, ¿se despertará el hilo equivocado?

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
    
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                log.debug("有烟没?[{}]", hasCigarette);
                if (!hasCigarette) {
    
    
                    log.debug("没烟,先歇会!");
                    try {
    
    
                        room.wait(2000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
    
    
                    log.debug("可以开始干活了");
                }
            }
        }, "小南").start();

        for (int i = 0; i < 5; i++) {
    
    
            new Thread(() -> {
    
    
                synchronized (room) {
    
    
                    log.debug("可以开始干活了");
                }
            }, "其它人").start();
        }

        sleep(1);
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                hasCigarette = true;
                log.debug("烟到了噢!");
                room.notify();
            }
        }, "送烟的").start();
    }

}

Solución:
puede usar while para determinar si la condición es verdadera varias veces y usar directamente notifyAll para activar todos los subprocesos. Luego, una vez que se despierta el hilo, primero puede juzgar si la condición es verdadera. Si es verdadera, ejecute lo siguiente. Si no es verdadera, continúe esperando.
Insertar descripción de la imagen aquí

10. aparcar y desaparcar

La diferencia entre esperar y notificar
es que no es necesario usarlo con un monitor
y puede activar y bloquear hilos con precisión
. Puede desaparcar primero, pero no puede notificar primero. Pero después de desaparcar, aparcar no funciona.

Principio de funcionamiento
①park, primero vaya al contador para determinar si es 0, si es así, deje que el hilo ingrese a la cola y configure el contador en 0 nuevamente
②unpark, si el hilo está bloqueado, primero configure el contador en 1, luego despierte suba el hilo y reanude Ejecutar y establezca el contador en 0
③ Desaparque primero y luego estacione, luego desaparque y agregue el contador a 1. Estacionar determina que el contador es 1. Si cree que todavía tiene energía, continúe la ejecución.

11. Volver a comprender el estado del hilo


Caso 1: nuevo-> inicio de subproceso ejecutable
Caso 2: ejecutable->esperando
notificar y esperar. esperar ingresa al bloqueo, notificar les permite competir por el bloqueo nuevamente y ingresar a ejecutable. Otros aún ingresan a la
situación de bloqueo 3. situación
de estacionamiento y desestacionamiento
4.
El hilo que lo llama por t.join entrará en espera, esperando que t complete la tarea o ser interrumpido.
Las situaciones 5-8
en realidad son esperar, unirse, dormir más tiempo, todo desde
situación ejecutable->bloqueada 9
Si la sincronización no logra adquirir el bloqueo, entrará en
la situación bloqueada 10
Después de ejecutar todo el código, será terminado
Insertar descripción de la imagen aquí

12. Múltiples cerraduras

Una habitación para dormir y estudiar. Pero cuando solo un bloqueo está inactivo, no puede aprender y la concurrencia es muy baja. Luego, en este momento, puede refinar la granularidad de la cerradura y dividirla en dos cerraduras, una para la sala de estudio y otra para el dormitorio, de modo que las dos funciones se puedan ejecutar al mismo tiempo.

Problema:
si hay demasiados bloqueos y un subproceso necesita varios bloqueos, se producirá un punto muerto.

public class TestMultiLock {
    
    
    public static void main(String[] args) {
    
    
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
    
    
            bigRoom.study();
        },"小南").start();
        new Thread(() -> {
    
    
            bigRoom.sleep();
        },"小女").start();
    }
}

@Slf4j(topic = "c.BigRoom")
class BigRoom {
    
    

    private final Object studyRoom = new Object();

    private final Object bedRoom = new Object();

    public void sleep() {
    
    
        synchronized (this) {
    
    
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
    
    
        synchronized (this) {
    
    
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }

}

Caso de punto muerto
t1 tiene A pero quiere B, t2 tiene B pero quiere A

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    
    
    public static void main(String[] args) {
    
    
        test1();
    }

    private static void test1() {
    
    
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
    
    
            synchronized (A) {
    
    
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
    
    
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
    
    
            synchronized (B) {
    
    
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
    
    
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

Método de verificación de interbloqueo
1. jps localiza la identificación del proceso y la identificación de jstack para ver la información del programa
2. jconsole ve directamente la información del proceso

Livelock
en realidad significa que ambos subprocesos están cambiando las condiciones de desbloqueo de cada uno, lo que resulta en que no se libere el bloqueo, pero tampoco se bloquee. Punto muerto significa que el bloqueo que contiene a la otra parte no se libera, lo que provoca que el hilo se bloquee.
Solución:
puede cambiar el tiempo de ejecución de los subprocesos y dejar que intercalen la ejecución para ejecutar rápidamente la condición de desbloqueo.
Insertar descripción de la imagen aquí
El problema de la inanición
es en realidad que el hilo no ha podido adquirir (competir por) el bloqueo, lo que no ha provocado ninguna ejecución.

13. Bloqueo reentrante

En comparación con la sincronización
, se puede interrumpir
. Puede establecer el tiempo de espera de adquisición. Después del tiempo de espera, automáticamente dejará de adquirir el bloqueo.
Bloqueo justo para evitar problemas de inanición.
Hay muchas variables de condición.

Reentrante
Siempre que el mismo hilo adquiera el mismo bloqueo, se puede utilizar una segunda vez. (Se puede usar una segunda vez cuando no está desbloqueado)

@Slf4j(topic = "c.test22")
public class MyTest22 {
    
    
    public static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
    
    
        lock.lock();
        try{
    
    
            log.debug("开始进入m1");
            m1();
        }finally {
    
    
            lock.unlock();
        }
    }

    public static void m1(){
    
    
        lock.lock();
        try {
    
    
            log.debug("m1进入");
            m2();
        }finally {
    
    
            lock.unlock();
        }
    }

    public static void m2(){
    
    
        lock.lock();
        try{
    
    
            log.debug("m2进入");
        }finally {
    
    
            lock.unlock();
        }
    }
}

LockInterrupt puede ser interrumpido
. Este método puede ser interrumpido por otros subprocesos que esperan el bloqueo. Si es un bloqueo, incluso si se interrumpe, no tendrá ningún efecto. Esta interrumpibilidad puede reducir la aparición de interbloqueos.

@Slf4j(topic = "c.test23")
public class MyTest23 {
    
    
    public static ReentrantLock lock=new ReentrantLock();
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            log.debug("上锁");
            lock.lock();
//            try {
    
    
//                lock.lockInterruptibly();
//            } catch (InterruptedException e) {
    
    
//                e.printStackTrace();
//                log.debug("无法获取锁");
//            }
            try {
    
    
                log.debug("获取到锁");
            } finally {
    
    
                lock.unlock();
            }
        }, "t1");
        t1.start();

        lock.lock();
        Sleeper.sleep(1);
        log.debug("帮助t1解锁");
        t1.interrupt();


    }
}

Tiempo de espera:
puede usar tryLock aquí para establecer el tiempo de espera para adquirir el bloqueo. Si se agota, automáticamente dejará de adquirir el bloqueo. En lugar de mantenerlo cerrado

@Slf4j(topic = "c.test24")
public class MyTest24 {
    
    
    public static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            log.debug("尝试获得锁");

            try {
    
    
                if(!lock.tryLock(2, TimeUnit.SECONDS)){
    
    
                    log.debug("获取锁失败");
                    return ;
                }
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
                log.debug("获取锁失败1");
                return;
            }
            try{
    
    
                log.debug("获取锁成功");
            }finally {
    
    
                lock.unlock();
            }
        }, "t1");

        log.debug("main获取锁");
        lock.lock();
        t1.start();
        Sleeper.sleep(1);
        log.debug("main解锁");
        lock.unlock();
    }
}

Para resolver el problema del filósofo
,
puede utilizar el tryLock de ReentrantLock. Si el intento falla, ejecutará el siguiente proceso en lugar de esperar a obtener el bloqueo. Si no puede obtener el bloqueo, el hilo no se bloqueará.

 @Override
    public void run() {
    
    
        while (true) {
    
    
            if(left.tryLock()){
    
    

                try{
    
    
                    if(right.tryLock()){
    
    
                        try{
    
    
                          eat();
                        }finally {
    
    
                            right.unlock();
                        }
                    }
                }finally {
    
    
                    left.unlock();
                }
            }
        }
    }

Las variables de condición
definen
sincronizar para tener un bloqueo y liberar el bloqueo en waitSet mediante esperar y notificar. Para ReentrantLock, es equivalente a tener múltiples salas de espera. Después de crear el bloqueo, puede crear múltiples variables de condición (múltiples salas). Se puede considerar que hay múltiples salas en ReentrantLock, y el ingreso a diferentes salas se puede procesar a través de diferentes pequeños Cerraduras. Pero, de hecho, el candado liberado sigue siendo ReentrantLock y luego se entrega a otros para que lo usen. Sin embargo, se pueden controlar diferentes habitaciones a través de variables de condición, lo que permite que la misma habitación pero el área del hilo compitan por el candado.

Aquí se utilizan variables de condición, pero ingresan a diferentes salas de subprocesos y operan de la misma manera. Los hilos de las habitaciones despiertas no son los mismos.

@Slf4j(topic = "c.Test24")
public class Test224 {
    
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {
    
    


        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
    
    
                    log.debug("没烟,先歇会!");
                    try {
    
    
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
    
    
                ROOM.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
    
    
                    log.debug("没外卖,先歇会!");
                    try {
    
    
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
    
    
                ROOM.unlock();
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
    
    
                ROOM.unlock();
            }
        }, "送外卖的").start();

        sleep(1);

        new Thread(() -> {
    
    
            ROOM.lock();
            try {
    
    
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
    
    
                ROOM.unlock();
            }
        }, "送烟的").start();
    }

}

Use un bloqueo para fijar el orden de ejecución.
La
idea es que ambos subprocesos necesitan usar este bloqueo, pero el subproceso t1 debe ejecutarse después de que se ejecute el subproceso t2. Entonces la condición de juicio es booleana. Si se ejecuta t2, modifíquelo y despierta el hilo t1. Si el subproceso t1 encuentra que t2 no se está ejecutando, entonces esperar ingresa a esperar. Si se despierta falsamente, puede realizar un bucle mientras ingresa a esperar nuevamente.

@Slf4j(topic = "c.25")
public class MyTest25 {
    
    
    static Object lock=new Object();
    static boolean t2Run=false;
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            synchronized (lock){
    
    


                try {
    
    
                    while(!t2Run){
    
    
                        lock.wait();
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                log.debug("1");
            }
        }, "t1");


        Thread t2 = new Thread(() -> {
    
    
            synchronized (lock){
    
    
                log.debug("2");
                //唤醒线程1
                t2Run=true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();

    }
}

El segundo método
consiste en utilizar el método de estacionamiento de LockSupport. Si se ejecuta t1 primero, se estacionará y entrará en bloqueo, y luego, después de ejecutar t2, el desaparcamiento despertará a t1. No importa si t2 se ejecuta primero, el desbloqueo del hilo aquí cambiará el contador a 1. Si t1 primero verifica el contador y descubre que es 1, entonces puede continuar ejecutándose.

@Slf4j(topic = "c.26")
public class MyTest26 {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            LockSupport.park();
            log.debug("1");
        }, "t1");

        Thread t2 = new Thread(() -> {
    
    
            log.debug("2");
            LockSupport.unpark(t1);
        }, "t2");

        t1.start();
        t2.start();


    }
}


La idea de imprimir el método sincronizado abc a su vez (ejecución alternativa)
es en realidad juzgar si se alcanza el indicador de este hilo. Si es 1, se ejecutará t1. Si es 2, se ejecutará t2. Después de la ejecución, debe activar otros subprocesos para verificar si es su propia condición. De lo contrario, continúe ingresando la condición de espera. Si se cumple la condición, adquiera el bloqueo y continúe con la ejecución.

public class MyTest27 {
    
    
    public static void main(String[] args) {
    
    
        WaitNotify1 waitNotify1 = new WaitNotify1(1, 5);
        Thread t1 = new Thread(() -> {
    
    
            waitNotify1.print("a",1,2);
        }, "t1");
        Thread t2 = new Thread(() -> {
    
    
            waitNotify1.print("b",2,3);
        }, "t2");
        Thread t3 = new Thread(() -> {
    
    
            waitNotify1.print("c",3,1);
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class  WaitNotify1{
    
    

    public void print(String s,int waitFlag,int nextFlag){
    
    

        synchronized (this){
    
    
            try {
    
    
                for(int i=0;i<loopNum;i++){
    
    
                    //如果不是1那么就等待
                    while(this.flag!=waitFlag){
    
    
                        this.wait();
                    }
                    System.out.print(s);
                    this.flag=nextFlag;
                    this.notifyAll();
                }

            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

    }


    private int flag;
    private int loopNum;

    public WaitNotify1(int flag,int loopNum) {
    
    
        this.flag = flag;
        this.loopNum = loopNum;
    }
}

La idea de ReentrantLock de lidiar con problemas de ejecución secuencial.
La idea de ejecución aquí es en realidad abrir más variables de condición a través del bloqueo. Las variables de condición controlan cada baño. Use variables de condición para bloquear subprocesos y luego liberarlos. . A, byc se abren tres veces respectivamente y luego se llama a otra condición después de ejecutar una para desbloquear la otra y continuar con la ejecución. Al principio, debe bloquear los tres subprocesos a, byc, y luego desbloquear uno manualmente para iniciar la ejecución del bucle.

public class MyTest28 {
    
    
    public static void main(String[] args) {
    
    
        WaitLock waitLock = new WaitLock(5);
        Condition a = waitLock.newCondition();
        Condition b = waitLock.newCondition();
        Condition c = waitLock.newCondition();

        new Thread(()->{
    
    
             waitLock.print("a",a,b);
         },"t1").start();
        new Thread(()->{
    
    
            waitLock.print("b",b,c);
        },"t2").start();
        new Thread(()->{
    
    
            waitLock.print("c",c,a);
        },"t3").start();

        Sleeper.sleep(1);
        waitLock.lock();
        try{
    
    
            //唤醒a
            a.signal();
        }finally {
    
    
            waitLock.unlock();
        }
    }
}

@Slf4j(topic = "c.lock")
class WaitLock extends ReentrantLock{
    
    

    private int loopNum;

    public WaitLock(int loopNum) {
    
    
        this.loopNum = loopNum;
    }

    public void print(String str,Condition cur,Condition next){
    
    
        for(int i=0;i<loopNum;i++){
    
    
            lock();
            try{
    
    
                cur.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                unlock();
            }

        }
    }
}

Idea de ejecución de LockSupport
De hecho, aquí puede usar park directamente para bloquear tres subprocesos y luego iniciar manualmente un subproceso. Si necesita iniciar el hilo bloqueado, debe desaparcar (t), lo que significa que debe pasar los parámetros del hilo correspondientes y reactivar el hilo correspondiente. Después de ejecutar t1, despierta t2, y después de ejecutar t2, despierta t3, luego el método llamado en este momento debe pasar los parámetros de hilo correspondientes. Además, los parámetros del hilo se pueden compartir y colocar en el área del método. Si se colocan en el hilo principal, no se pueden ver.

@Slf4j(topic = "c.test29")
public class MyTest29 {
    
    
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
    
    
        WaitPark waitPark = new WaitPark(5);
        t1=new Thread(()->{
    
    
            waitPark.print("a",t2);
        },"t1");
        t2=new Thread(()->{
    
    
            waitPark.print("b",t3);
        },"t2");
        t3=new Thread(()->{
    
    
            waitPark.print("c",t1);
        },"t3");

        t1.start();
        t2.start();
        t3.start();
        Sleeper.sleep(1);
        LockSupport.unpark(t1);
    }
}
@Slf4j(topic = "c.lock")
class WaitPark{
    
    
    private int loopNum;

    public WaitPark(int loopNum) {
    
    
        this.loopNum = loopNum;
    }



    public void print(String str,Thread t){
    
    
        for(int i=0;i<loopNum;i++){
    
    
            LockSupport.park();
            log.debug(str);
            LockSupport.unpark(t);
        }
    }
}

Supongo que te gusta

Origin blog.csdn.net/weixin_45841848/article/details/132614018
Recomendado
Clasificación