Notas de estudo do tópico JUC

índice

fundo

CASO

Introdução

inscrição

CountDownLatch

Introdução

inscrição

Interface chamável

Introdução

usar

Bloquear bloqueio de sincronização

Introdução

usar

palavra-chave volátil

Classe de contêiner simultânea

Introdução

Outras classes de contêiner

Falso despertar

A condição está despertando e esperando

Bloqueio de leitura e escrita

Introdução

uso

Grupo de discussão

Introdução

uso

Estrutura de branch / merge ForkJoinPool

Introdução

usar

em vez de

Conclusão

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

Acho que você gosta

Origin blog.csdn.net/qq_37475168/article/details/107060372
Recomendado
Clasificación