Entrevistador Jingdong: Fale-me sobre a diferença entre sincronizado e ReentrantLock!

Prefácio

Anteriormente, apresentamos muito conteúdo sobre multi-threading. Em multi-threading, há um assunto muito importante que precisamos superar, isto é, a segurança de thread. Problemas de segurança de thread referem-se à poluição de dados ou outros resultados inesperados de execução de programas causados ​​por operações simultâneas entre threads em vários threads.

Discussão segura

1) Casos não seguros para thread

Por exemplo, se A e B transferem dinheiro para C ao mesmo tempo, suponha que o saldo original de C seja 100 yuans, e A transfira 100 yuans para C, e ele está em processo de transferência. Nesse momento, B também transfere 100 Yuan para C. Neste momento, A é transferido para C com sucesso., O saldo passa a ser de 200 yuans, mas B pergunta que o saldo de C é de 100 yuans adiantado e também é de 200 yuans após a transferência ser bem-sucedida Quando A e B completaram a transferência para C, o saldo ainda é de 200 yuans em vez dos esperados 300 yuans, o que é um problema típico de segurança de linha.

2) Exemplo de código não seguro para thread

Não importa se você não entende o conteúdo acima, vamos dar uma olhada no código específico que não é seguro para thread:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -› addNumber());
        Thread thread2 = new Thread(() -› addNumber());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Os resultados da execução do programa acima são os seguintes:

número : 12085

O resultado de cada execução pode variar ligeiramente, mas quase nunca é igual à soma cumulativa (correta) de 20.000.

3) Solução segura para rosca

A solução thread-safe tem as seguintes dimensões:

  • Os dados não são compartilhados, um único thread é visível, como ThreadLocal é um único thread visível;
  • Use classes thread-safe, como StringBuffer e classes de segurança em JUC (java.util.concurrent) (serão especificamente introduzidas no artigo a seguir);
  • Use códigos de sincronização ou bloqueios.

Sincronização e bloqueio de thread

1) sincronizado

① Introdução ao sincronizado

Sincronizado é um mecanismo de sincronização fornecido por Java. Quando um encadeamento está operando um bloco de código sincronizado (código modificado sincronizado), outros encadeamentos podem apenas bloquear e esperar que o encadeamento original seja executado antes de executar.

② uso sincronizado

synchronized pode modificar o bloco de código ou método, o código de amostra é o seguinte:

// 修饰代码块
synchronized (this) {
    // do something
}
// 修饰方法
synchronized void method() {
    // do something
}

Use synchronized para concluir o código não seguro para thread no início deste artigo.

Método 1: Use synchronized para decorar o bloco de código, o código é o seguinte:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread sThread = new Thread(() -› {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });
        Thread sThread2 = new Thread(() -› {
            // 同步代码
            synchronized (ThreadSafeTest.class) {
                addNumber();
            }
        });
        sThread.start();
        sThread2.start();
        sThread.join();
        sThread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Os resultados da execução do programa acima são os seguintes:

número : 20.000

Método 2: use o método de modificação sincronizada, o código é o seguinte:

class ThreadSafeTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread sThread = new Thread(() -› addNumber());
        Thread sThread2 = new Thread(() -› addNumber());
        sThread.start();
        sThread2.start();
        sThread.join();
        sThread2.join();
        System.out.println("number:" + number);
    }
    public synchronized static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Os resultados da execução do programa acima são os seguintes:

número : 20.000

③ Princípio de realização sincronizada

A essência do synchronized é alcançar a segurança do thread entrando e saindo do objeto Monitor. Tome o seguinte código como exemplo:

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("Java");
        }
    }
}

Quando usamos javap para compilar, o bytecode gerado é o seguinte:

Compiled from "SynchronizedTest.java"
public class com.interview.other.SynchronizedTest {
  public com.interview.other.SynchronizedTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."‹init›":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/interview/other/SynchronizedTest
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Java
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

Pode-se ver que a JVM (Java Virtual Machine) usa as instruções monitorenter e monitorexit para conseguir a sincronização.A instrução monitorenter é equivalente a travar, e o monitorexit é equivalente a liberar o travamento. O monitorenter e o monitorexit são implementados com base no Monitor.

2) ReentrantLock

① Introdução ao ReentrantLock

ReentrantLock (bloqueio de reentrada) é uma implementação de bloqueio fornecida pelo Java 5 e sua função é basicamente a mesma que sincronizada. O bloqueio de reentrada adquire o bloqueio chamando o método lock () e libera o bloqueio chamando unlock ().

② Uso de ReentrantLock

Uso básico do ReentrantLock, o código é o seguinte:

Lock lock = new ReentrantLock();
lock.lock();    // 加锁
// 业务代码...
lock.unlock();    // 解锁

Use ReentrantLock para melhorar o código não seguro para threads no início deste artigo, consulte o seguinte código:

public class LockTest {
    static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // ReentrantLock 使用
        Lock lock = new ReentrantLock();
        Thread thread1 = new Thread(() -› {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        Thread thread2 = new Thread(() -› {
            try {
                lock.lock();
                addNumber();
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("number:" + number);
    }
    public static void addNumber() {
        for (int i = 0; i ‹ 10000; i++) {
            ++number;
        }
    }
}

Tente adquirir o bloqueio

ReentrantLock pode tentar acessar o bloqueio sem bloquear, usando o método tryLock (), que é usado especificamente da seguinte maneira:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -› {
    try {
        reentrantLock.lock();
        Thread.sleep(2 * 1000);
 
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}).start();
// 线程二
new Thread(() -› {
    try {
        Thread.sleep(1 * 1000);
        System.out.println(reentrantLock.tryLock());
        Thread.sleep(2 * 1000);
        System.out.println(reentrantLock.tryLock());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

O resultado da execução do código acima é o seguinte:

falso
verdadeiro

Tente adquirir o bloqueio por um período de tempo

tryLock () tem um método de extensão tryLock (long timeout, TimeUnit unit) para tentar adquirir o bloqueio por um período de tempo. O código de implementação específico é o seguinte:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -› {
    try {
        reentrantLock.lock();
        System.out.println(LocalDateTime.now());
        Thread.sleep(2 * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        reentrantLock.unlock();
    }
}).start();
// 线程二
new Thread(() -› {
    try {
        Thread.sleep(1 * 1000);
        System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS));
        System.out.println(LocalDateTime.now());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

O resultado da execução do código acima é o seguinte:

2019-07-05 19:53:51
true
2019-07-05 19:53:53

Pode-se ver que o bloqueio é adquirido diretamente pelo thread dois após hibernar por 2 segundos, então o parâmetro de tempo limite no método tryLock (tempo limite longo, unidade TimeUnit) refere-se ao tempo máximo de espera para adquirir o bloqueio.

③ Precauções ReentrantLock

Ao usar o ReentrantLock, você deve se lembrar de liberar o bloqueio, caso contrário, o bloqueio ficará permanentemente ocupado.

Perguntas relacionadas à entrevista

1. Quais são os métodos comumente usados ​​de ReentrantLock?

Resposta: Os métodos comuns de ReentrantLock são os seguintes:

  • lock (): usado para obter o bloqueio
  • desbloquear (): usado para liberar o bloqueio
  • tryLock (): tente adquirir o bloqueio
  • getHoldCount (): Consulta o número de vezes que o thread atual executa o método lock ()
  • getQueueLength (): Retorna o número de threads que estão na fila para adquirir este bloqueio
  • isFair (): se o bloqueio é justo

2. Quais são as vantagens do ReentrantLock?

Resposta: ReentrantLock tem o recurso de adquirir o bloqueio de forma não bloqueadora, usando o método tryLock (). ReentrantLock pode interromper o bloqueio adquirido.Depois de obter o bloqueio usando o método lockInterruptibly (), se a thread for interrompida, uma exceção será lançada e o bloqueio adquirido atualmente será liberado. ReentrantLock pode adquirir o bloqueio dentro do intervalo de tempo especificado, usando o método tryLock (tempo limite longo, unidade TimeUnit).

3. Como ReentrantLock cria um bloqueio justo?

Resposta: new ReentrantLock () cria um bloqueio injusto por padrão. Se você deseja criar um bloqueio justo, você pode usar o novo ReentrantLock (true).

4. Qual é a diferença entre fair lock e injusto lock?

Resposta: O bloqueio justo refere-se à ordem em que os threads adquirem bloqueios de acordo com a ordem de bloqueio, enquanto o bloqueio não justo refere-se ao mecanismo de captura de bloqueio. O thread que bloqueia () primeiro não necessariamente adquire o bloqueio primeiro.

5. Qual é a diferença entre lock () e lockInterruptibly () em ReentrantLock?

Resposta: A diferença entre lock () e lockInterruptibly () é que se o encadeamento for interrompido durante a aquisição do encadeamento, lock () irá ignorar a exceção e continuará a esperar pelo encadeamento de aquisição, enquanto lockInterruptibly () lançará uma InterruptedException. Análise do problema: execute o código a seguir e use lock () e lockInterruptibly () no thread para visualizar os resultados da execução. O código é o seguinte:

 Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            interruptLock.lock();
            //interruptLock.lockInterruptibly();  // java.lang.InterruptedException
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);

Se você executar o código a seguir, verá que o programa não relatará um erro ao usar lock () e sairá imediatamente após a operação ser concluída; ao usar lockInterruptibly (), lançará uma exceção java.lang.InterruptedException, o que significa : se a thread for interrompida no caminho para obter a thread, Lock () irá ignorar a exceção e continuará a esperar para adquirir a thread, enquanto lockInterruptibly () lançará uma InterruptedException.

6. Qual é a diferença entre synchronized e ReentrantLock?

Resposta: Tanto o synchronized quanto o ReentrantLock garantem a thread de segurança, e suas diferenças são as seguintes:

  • ReentrantLock é mais flexível de usar, mas deve haver uma ação cooperativa para liberar o bloqueio;
  • ReentrantLock deve adquirir e liberar manualmente o bloqueio, enquanto sincronizado não precisa liberar e abrir manualmente o bloqueio;
  • ReentrantLock se aplica apenas a bloqueios de bloco de código, enquanto synchronized pode ser usado para modificar métodos, blocos de código, etc .;
  • O desempenho do ReentrantLock é ligeiramente superior ao do synchronized.

7. O tryLock de ReentrantLock (3, TimeUnit.SECONDS) significa esperar 3 segundos antes de adquirir o bloqueio. Esta afirmação está correta? porque?

Resposta: Não, tryLock (3, TimeUnit.SECONDS) significa que o tempo máximo de espera para adquirir o bloqueio é de 3 segundos, durante os quais ele sempre tentará adquirir em vez de esperar 3 segundos antes de adquirir o bloqueio.

8. Como o sincronizado realiza a atualização do bloqueio?

Resposta: Há um campo threadid no cabeçalho do objeto de bloqueio. O threadid está vazio quando é acessado pela primeira vez. A JVM (Java Virtual Machine) permite que ele mantenha um bloqueio tendencioso e define o threadid para seu segmento id. Neste momento, ele primeiro julgará se o threadid é consistente, especialmente o ID do thread. Se for consistente, pode ser usado diretamente. Se não for consistente, o bloqueio de polarização de atualização é um bloqueio leve. O bloqueio é adquirido através de um certo número de ciclos de giro sem bloqueio.Após um certo número de execuções, será atualizado para um bloqueio pesado, entre no bloco, todo o processo é o processo de atualização do bloqueio.

Resumindo

Este artigo apresenta duas formas de sincronização de thread, sincronizado e ReentrantLock. ReentrantLock é mais flexível e eficiente. No entanto, ReentrantLock só pode modificar o bloco de código. O uso de ReentrantLock exige que o desenvolvedor libere o bloqueio manualmente. Se você esquecer de liberar o bloqueio, o o bloqueio sempre será Ocupado. Sincronizado usa uma gama mais ampla de cenários, que podem modificar métodos comuns, métodos estáticos e blocos de código. O editor também resume um mapa mental multithread aqui. Para facilitar que os amigos organizem melhor os pontos técnicos, compartilhe-o com todos !

[Falha na transferência da imagem do link externo. O site de origem pode ter um mecanismo de link anti-leech. Recomenda-se salvar a imagem e carregá-la diretamente (img-0WEYlMrX-1614240957624) (https: //ask8088-private-1251520898.cn- south.myqcloud.com/developer -images / article / 7948575 / n2ecyj3vob.png? q-sign-algorithm = sha1 & q-ak = AKID2uZ1FGBdx1pNgjE3KK4YliPpzyjLZvug & q-sign-time = 1614240-key-time = q-key-time = 1614240359; 1614247559 & q-key-time = q-1614240-time = q-key-24759 tempo 161424759 1614240359-q-keylist = & q-signature = d02e79d208db8b3706d7c9fd331fd49efb9bdaf9)]

Acho que você gosta

Origin blog.csdn.net/QLCZ0809/article/details/114089183
Recomendado
Clasificación