índice
Bloquear bloqueio de sincronização
Classe de contêiner simultânea
A condição está despertando e esperando
Estrutura de branch / merge ForkJoinPool
fundo
Continue a relaxar ... Organize as notas do estudo do tópico JUC no mês passado. JUC é a abreviatura de java.util.concurrent package, os códigos de amostra estão todos em execução no ambiente jdk8
CASO
Introdução
CAS é a abreviatura de Compare And Swap, usado para garantir a atomicidade dos dados, que é o suporte de hardware para operação simultânea e compartilhamento de dados
Contém três valores: valor de memória V, valor estimado A, valor atualizado B
Se e somente se V == A, V = B, caso contrário, não faça nada
inscrição
A classe de implementação do CAS em Java é uma classe de dados atômicos, que pode modificar o valor atomicamente
A modificação original como i = i ++ é dividida em três etapas:
int temp = i;
i = i + 1;
i = temp;
Isso é obviamente inseguro em um ambiente multi-threaded
public class Main {
public static void main(String[] args) {
CASDemo casDemo = new CASDemo();
for (int i = 0; i < 10; i++) {
new Thread(casDemo).start();
}
}
}
class CASDemo implements Runnable {
private int mNum = 0;
@Override
public void run() {
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + ":" + getNum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getNum() {
return mNum++;
}
}
O resultado da saída pode ter valores duplicados, mas se você mudar para uma classe atômica, isso não acontecerá
public class Main {
public static void main(String[] args) {
CASDemo casDemo = new CASDemo();
for (int i = 0; i < 10; i++) {
new Thread(casDemo).start();
}
}
}
class CASDemo implements Runnable {
private AtomicInteger mAtomicNum = new AtomicInteger(0);
@Override
public void run() {
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + ":" + getAtomicNum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getAtomicNum() {
return mAtomicNum.getAndIncrement();
}
}
Além de usar o algoritmo CAS para garantir a atomicidade das operações, as variáveis atômicas também usam a palavra-chave volatile para garantir a visibilidade da memória
CountDownLatch
Introdução
CountDownLatch (Latch): Quando algumas operações são concluídas, apenas as operações de todos os outros threads são concluídas, a operação atual continuará a ser executada
inscrição
Uso Participa do código a seguir e de seus comentários
public class Main {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5); // 初始化闭锁对象,给出初值
LatchDemo demo = new LatchDemo(latch);
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
new Thread(demo).start();
}
latch.await(); // 等待闭锁值为0
System.out.println("耗费时间:" + (System.currentTimeMillis() - start));
}
}
class LatchDemo implements Runnable {
private final CountDownLatch mLatch;
public LatchDemo(CountDownLatch latch) {
mLatch = latch;
}
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
synchronized (mLatch) { // 由于闭锁对象被多个线程引用,所以此处加个同步锁
mLatch.countDown(); // 子线程执行完,闭锁值-1
}
}
}
Interface chamável
Introdução
Além de implementar a interface Runnable e herdar a classe Thread, implementar a interface Callable é a terceira maneira de implementar threads. Em comparação com a implementação da interface Runnable, a interface Callable precisa fornecer o tipo, o método tem valores de retorno e exceções e precisa do suporte de FutureTask
usar
Primeiro implemente a interface Callable, especifique a genérica como String e gere uma sequência de letras aleatórias de comprimento 10
class CallableDemo implements Callable<String> {
private static final String sCHARACTERS = "abcdefghijklmnopqrstuvmxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private final Random mRandom = new Random();
@Override
public String call() throws Exception {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10; i++) {
int index = Math.abs(mRandom.nextInt()) % sCHARACTERS.length();
builder.append(sCHARACTERS.charAt(index));
}
return builder.toString();
}
}
Em seguida, use FutureTask para executar a tarefa e obter o resultado
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
CallableDemo demo = new CallableDemo();
FutureTask<String> task = new FutureTask<>(demo); // 实例化FutureTask,泛型和Callable的泛型一致
new Thread(task).start();
System.out.println(task.get()); // get()方法会阻塞,直到结果返回。因此FutureTask也可以用于闭锁
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Também é relativamente fácil usar FutureTask para obter o bloqueio para contar o tempo de execução do thread
try {
CallableDemo demo = new CallableDemo();
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
FutureTask<String> task = new FutureTask<>(demo);
new Thread(task).start();
System.out.println(task.get());
}
System.out.println("耗费时间:" + (System.currentTimeMillis() - start));
} catch (ExecutionException e) {
e.printStackTrace();
}
Bloquear bloqueio de sincronização
Introdução
Além do bloco de código de sincronização e do método de sincronização, Lock é a terceira maneira de resolver o problema de segurança multi-thread. É mais flexível do que o método de sincronização e o bloco de código de sincronização. Ele precisa ser bloqueado por meio do lock () e, em seguida, liberado por meio do método unlock ().
usar
Vá diretamente para o código, o método unlock () deve ser colocado no bloco finally no try-catch
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
Ticket ticket = new Ticket();
for (int i = 0; i < 3; i++) {
new Thread(ticket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Ticket implements Runnable {
private int mTick = 100;
private Lock mLock = new ReentrantLock();
@Override
public void run() {
while (mTick > 0) {
mLock.lock();
mTick--;
System.out.println(Thread.currentThread().getName() + ":" + mTick);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mLock.unlock();
}
}
}
}
palavra-chave volátil
1. Introdução
Volátil é usado para resolver o problema de visibilidade de memória.O problema de visibilidade de memória se refere ao problema de que quando vários threads operam em dados compartilhados, porque eles acessam o TLAB de seu próprio thread, a operação um do outro é invisível. Sobre o TLAB, consulte as notas de estudo de JVM de heap de artigo das seções relevantes
Para resolver o problema de visibilidade da memória, você pode usar métodos sincronizados, de bloqueio e voláteis
2. Use
Para o código a seguir, o valor de demo.isFlag () do thread principal é false. Devido à existência de TLAB, o valor do sinalizador no thread principal e no thread filho não podem ser modificados de forma síncrona.
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
while (true) {
if (demo.isFlag()) {
System.out.println("........");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadDemo implements Runnable {
private boolean mFlag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (Exception e) {
}
mFlag = true;
}
public boolean isFlag() {
return mFlag;
}
public void setFlag(boolean flag) {
mFlag = flag;
}
}
Mas adicionar a palavra-chave volatile é diferente. Volitale muda a visibilidade da memória do sinalizador. Quando o thread filho muda o sinalizador, ele é descarregado diretamente na memória; enquanto o thread principal lê o sinalizador diretamente da memória e o valor do sinalizador foi alterado para verdadeiro
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
while (true) {
if (demo.isFlag()) {
System.out.println("........");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ThreadDemo implements Runnable {
private volatile boolean mFlag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (Exception e) {
}
mFlag = true;
}
public boolean isFlag() {
return mFlag;
}
public void setFlag(boolean flag) {
mFlag = flag;
}
}
Mas o problema que se segue é que volátil trará degradação de desempenho, mas ainda é maior do que sincronizado, mas volátil não é mutuamente exclusivo e não pode garantir a atomicidade das variáveis, basta alterar a operação da variável de TLAB para a memória principal.
Classe de contêiner simultânea
Tome ConcurrentHashMap como um exemplo para aprender sobre classes de contêiner simultâneas
Introdução
O thread de HashMap não é seguro, o thread de HashTable é seguro, mas é de baixa eficiência.
ConcurrentHashMap usa um mecanismo de segmentação de bloqueio e usa concurrentLevel para medir a segmentação de bloqueio. O valor padrão é 16. Cada segmento mantém um mapa hash, que é um bloqueio independente, portanto, quando vários threads acessam diferentes segmentos de bloqueio, não é apenas seguro para thread, mas também eficiente.
Mas desde jdk1.8, o ConcurrentHashMap interno também foi substituído pelo algoritmo CAS
Outras classes de contêiner
TreeMap síncrono pode ser substituído por ConcurrentSkipListMap. Quando o número de leituras e travessias for muito maior do que o número de atualizações, você pode usar CopyOnWriteArrayList para substituir o ArrayList sincronizado
O uso da classe container síncrona é igual ao da classe container comum. Como usá-lo e como usá-lo, ignore-o aqui, mas você pode consultar o artigo Multithreading para realizar o acúmulo de sequência , que usa um fila de corrente simultânea
Falso despertar
Despertar falso significa que quando o método wait () é envolvido em uma instrução if, ocorrerá um erro quando ele for ativado, como o código a seguir
class Clerk {
private int mNum = 0;
public synchronized void add() throws InterruptedException {
if (mNum >= 1) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
wait();
}
mNum += 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
public synchronized void sub() throws InterruptedException {
if (mNum <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
wait();
}
mNum -= 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
}
class Producer implements Runnable {
private Clerk mClerk;
public Producer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.add();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Clerk mClerk;
public Consumer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.sub();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Ao executar dois produtores e dois consumidores, você descobrirá que mCount é negativo. Isso ocorre porque, depois que o produtor notificou o balconista, vários consumidores realizaram a operação mCount-1
Para resolver o problema do falso despertar, o método wait () deve ser chamado no loop
class Clerk {
private int mNum = 0;
public synchronized void add() throws InterruptedException {
while (mNum >= 1) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
wait();
}
mNum += 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
public synchronized void sub() throws InterruptedException {
while (mNum <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
wait();
}
mNum -= 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
notifyAll();
}
}
class Producer implements Runnable {
private Clerk mClerk;
public Producer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.add();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Clerk mClerk;
public Consumer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.sub();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
A condição está despertando e esperando
Os bloqueios síncronos também podem implementar consumidores produtores, e o objeto Condição no objeto de bloqueio também tem métodos de espera e ativação semelhantes
class Clerk {
private int mNum = 0;
private Lock mLock = new ReentrantLock();
private Condition mCondition = mLock.newCondition();
public void add() throws InterruptedException {
mLock.lock();
try {
while (mNum >= 1) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum > 1..");
mCondition.await(50, TimeUnit.MILLISECONDS);
}
mNum += 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
mCondition.signalAll();
} finally {
mLock.unlock();
}
}
public void sub() throws InterruptedException {
mLock.lock();
try {
while (mNum <= 0) {
System.out.println(Thread.currentThread().getName() + ": " + "mNum < 0..");
mCondition.await(50, TimeUnit.MILLISECONDS);
}
mNum -= 1;
System.out.println(Thread.currentThread().getName() + ": " + mNum);
mCondition.signalAll();
} finally {
mLock.unlock();
}
}
}
class Producer implements Runnable {
private Clerk mClerk;
public Producer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.add();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Clerk mClerk;
public Consumer(Clerk mClerk) {
this.mClerk = mClerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
mClerk.sub();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Comece um número igual de produtores e consumidores para verificá-lo
Use a condição para obter fios alternados ordenados, como impressão alternativa de ABC
public class Main {
public static void main(String[] args) throws InterruptedException {
try {
AlterDemo demo = new AlterDemo();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
demo.printA();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
demo.printB();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
demo.printC();
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class AlterDemo {
private int mNum = 1;
private Lock mLock = new ReentrantLock();
private Condition mCondition1 = mLock.newCondition();
private Condition mCondition2 = mLock.newCondition();
private Condition mCondition3 = mLock.newCondition();
public void printA() {
mLock.lock();
try {
while (mNum != 1) {
mCondition1.await();
}
System.out.println("A");
mNum = 2;
mCondition2.signal();
} catch (Exception e) {
} finally {
mLock.unlock();
}
}
public void printB() {
mLock.lock();
try {
while (mNum != 2) {
mCondition2.await();
}
System.out.println("B");
mNum = 3;
mCondition3.signal();
} catch (Exception e) {
} finally {
mLock.unlock();
}
}
public void printC() {
mLock.lock();
try {
while (mNum != 3) {
mCondition3.await();
}
System.out.println("C");
mNum = 1;
mCondition1.signal();
} catch (Exception e) {
} finally {
mLock.unlock();
}
}
}
Bloqueio de leitura e escrita
Introdução
O bloqueio de leitura e gravação é para garantir a exclusão mútua de gravação / gravação
uso
Use os métodos readLock () e writeLock () para obter o bloqueio de leitura e gravação do objeto de bloqueio de leitura e gravação
class WriteReadDemo {
private int mNum = 0;
private ReadWriteLock mLock = new ReentrantReadWriteLock();
public void set(int num) {
mLock.writeLock().lock();
try {
mNum = num;
} finally {
mLock.writeLock().unlock();
}
}
public void get() {
mLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + ": " + mNum);
} finally {
mLock.readLock().unlock();
}
}
}
Em seguida, inicie a verificação de discussão
WriteReadDemo demo = new WriteReadDemo();
for (int i = 0; i < 50; i++) {
new Thread(() -> demo.set((int) (Math.random() * 100)), "Write").start();
}
for (int i = 0; i < 100; i++) {
new Thread(demo::get, "Read" + i).start();
}
Grupo de discussão
Introdução
A fim de reduzir a sobrecarga de criação, manutenção e destruição de threads, podemos usar pools de threads para criar threads de agendamento em vez de diretamente novos
uso
O pool de threads pode ser criado por meio do novo método relevante de Executores
newFixedThreadPool () cria um pool de threads de tamanho fixo
newCachedThreadPool () cria um pool de threads de cache, a capacidade pode ser ajustada de acordo com as necessidades
newSingleThreadExecutor () cria um único pool de threads
newScheduledThreadPool () cria um pool de threads de tamanho fixo e executa tarefas regularmente
ExecutorService pool = Executors.newFixedThreadPool(5);
Após a criação, você pode executar a tarefa por meio do método submit () do pool de threads
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
int sum = 0;
for (int j = 0; j < 51; j++) {
sum += j;
}
System.out.println(Thread.currentThread().getName() + ": " + sum);
});
}
Finalmente, feche o pool de threads
pool.shutdown();
shutdown () irá esperar que todas as threads filhas em execução terminem e então encerrar, e shutdownNow () irá imediatamente encerrar e encerrar as threads filhas que ainda não terminaram.
Para tarefas de agendamento ScheduledExecutorService, você precisa chamar seu método schedule (), passar o objeto executável ou chamável, atraso e unidade de atraso
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
pool.schedule(() -> {
int sum = 0;
for (int j = 0; j < 51; j++) {
sum += j;
}
System.out.println(Thread.currentThread().getName() + ": " + sum);
}, (long) (Math.random() * 1000), TimeUnit.MILLISECONDS);
}
pool.shutdown();
Se você estiver interessado em ler o código-fonte do pool de threads, consulte o artigo Breve análise do código-fonte do pool de threads para Android Development and Learning
Estrutura de branch / merge ForkJoinPool
Introdução
Estrutura Fork / Join: se necessário, divida uma tarefa grande (Fork) em várias tarefas pequenas até que não possa mais ser dividida; em seguida, mescle os resultados das tarefas pequenas (Join) nos resultados da tarefa grande
Isso é semelhante à estrutura MapReduce, exceto que o processo de divisão de MapReduce é inevitável
Modo de roubo de trabalho:
Quando um encadeamento de subtarefa completa as tarefas em sua fila de tarefas e a grande tarefa não terminou, ele irá roubar aleatoriamente uma tarefa do final da fila de outros encadeamentos de subtarefa para executar
usar
Primeiro defina a classe de tarefa, herde a classe pai RecursiveTask, especifique o tipo genérico e implemente o método compute ()
class FJSumCalculate extends RecursiveTask<Long> {
private long mStart, mEnd;
private static final long sTHRESHOLD = 100L; // 拆分临界值
public FJSumCalculate(long start, long end) {
mStart = start;
mEnd = end;
}
@Override
protected Long compute() {
long length = mEnd - mStart;
if (length <= sTHRESHOLD) {
long sum = 0L;
for (long i = mStart; i <= mEnd; i++) {
sum += i;
}
return sum;
}
// 拆分
long middle = (mEnd + mStart) / 2;
FJSumCalculate left = new FJSumCalculate(mStart, middle - 1);
left.fork();
FJSumCalculate right = new FJSumCalculate(middle, mEnd);
right.fork();
// 合并
return left.join() + right.join();
}
Preste atenção à escolha do valor crítico, não muito pequeno
Em seguida, teste no método principal, use ForkJoinPool para executar a tarefa
ForkJoinPool pool = new ForkJoinPool();
System.out.println(pool.invoke(new FJSumCalculate(1L, 99999L)));
pool.shutdown();
em vez de
LongStream também pode ser usado em java8 para realizar lógica de acumulação paralela semelhante
LongStream.rangeClosed(1L, 99999L).parallel().reduce(Long::sum).getAsLong();
Conclusão
Este é o fim das notas de estudo do tópico JUC, sejam bem-vindos