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)]