JUC thread study notes

table of Contents

background

CASE

Introduction

application

CountDownLatch

Introduction

application

Callable interface

Introduction

use

Lock sync lock

Introduction

use

volatile keyword

Concurrent container class

Introduction

Other container classes

False wakeup

Condition's wake and wait

Read-write lock

Introduction

usage

Thread Pool

Introduction

usage

ForkJoinPool branch/merge framework

Introduction

use

instead

Conclusion

background

Continue to relax... Organize the notes of studying the JUC thread last month. JUC is the abbreviation of java.util.concurrent package, the sample codes are all running in jdk8 environment

CASE

Introduction

CAS is the abbreviation of Compare And Swap, used to ensure the atomicity of data, which is the hardware support for concurrent operation and sharing of data

Contains three values: memory value V, estimated value A, updated value B

If and only if V == A, V = B, otherwise do nothing

application

The implementation class of CAS in Java is an atomic data class, which can modify the value atomically

The original modification such as i = i++ is divided into three steps:

int temp = i;

i = i + 1;

i = temp;

This is obviously unsafe in a multi-threaded environment

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++;
    }
}

The output result may have duplicate values, but if you change to an atomic class, this will not happen

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();
    }
}

In addition to using the CAS algorithm to ensure the atomicity of operations, atomic variables also use the volatile keyword to ensure memory visibility

CountDownLatch

Introduction

CountDownLatch (Latch): When some operations are completed, only the operations of all other threads are completed, the current operation will continue to execute

application

Usage Participate in the following code, and its comments

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
        }
    }
}

Callable interface

Introduction

In addition to implementing the Runnable interface and inheriting the Thread class, implementing the Callable interface is the third way to implement threads. Compared to implementing the Runnable interface, the Callable interface needs to give types, methods have return values ​​and exceptions, and need FutureTask support

use

First implement the Callable interface, specify the generic as String, and generate a random letter sequence of length 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();
    }
}

Then use FutureTask to execute the task and get the result

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();
        }
    }
}

It is also relatively easy to use FutureTask to achieve blocking to count thread running time

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();
}

Lock sync lock

Introduction

In addition to the synchronization code block and the synchronization method, Lock is the third way to solve the multi-thread safety problem. It is more flexible than the synchronization method and the synchronization code block. It needs to be locked through the lock() method, and then released through the unlock() method.

use

Go directly to the code, the unlock() method should be placed in the finally block in the 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();
            }
        }
    }
}

volatile keyword

1 Introduction

Volatile is used to solve the memory visibility problem. The memory visibility problem refers to the problem that when multiple threads operate on shared data, because they access the TLAB of their own thread, the operation of each other is invisible. About TLAB, see the article heap JVM study notes of the relevant sections

To solve the memory visibility problem, you can use synchronized, lock, and volatile methods

2. Use

For the following code, the value of demo.isFlag() of the main thread is false. Because of the existence of TLAB, the flag value in the main thread and the child thread cannot be modified synchronously.

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;
    }
}

But adding the volatile keyword is different. Volitale changes the memory visibility of the flag. When the child thread changes the flag, it is directly flushed to the memory; while the main thread reads the flag directly from the memory, and the value of the flag is changed to True

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;
    }
}

But the ensuing problem is that volatile will bring performance degradation, but it is still higher than synchronized, but volatile is not mutually exclusive, and can not guarantee the atomicity of variables, just change the variable operation from TLAB to main memory go with

Concurrent container class

Take ConcurrentHashMap as an example to learn about concurrent container classes

Introduction

HashMap thread is not safe, HashTable thread safe, but low efficiency.

ConcurrentHashMap uses a lock segmentation mechanism, and uses concurrentLevel to measure lock segmentation. The default value is 16. Each segment maintains a hash map, which is an independent lock, so when multiple threads access different lock segments, it is not only thread safe, but also efficient.

But since jdk1.8, the internal ConcurrentHashMap has also been replaced with the CAS algorithm

Other container classes

Synchronous TreeMap can be replaced with ConcurrentSkipListMap. When the number of readings and traversal is much larger than the number of updates, you can use CopyOnWriteArrayList to replace the synchronized ArrayList

The usage of the synchronous container class is the same as that of the ordinary container class. How to use it and how to use it, skip it here, but you can refer to the article Multithreading to realize the accumulation of sequence , which uses a concurrent chain queue

False wakeup

False wakeup means that when the wait() method is wrapped in an if statement, an error will occur when it is awakened, such as the following code

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();
            }
        }
    }
}

When running two producers and two consumers, you will find that mCount is negative. This is because after the producer has notified the clerk, multiple consumers have performed the mCount-1 operation

In order to solve the false wakeup problem, the wait() method should be called in the 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();
            }
        }
    }
}

Condition's wake and wait

Synchronous locks can also implement producer consumers, and the Condition object in the lock object also has similar waiting and wake-up methods

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();
            }
        }
    }
}

Start an equal number of producers and consumers to verify it

Use Condition to achieve orderly alternating threads, such as alternate printing of 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();
        }
    }
}

Read-write lock

Introduction

The read-write lock is to ensure the mutual exclusion of write/write

usage

Use the readLock() and writeLock() methods to obtain the read lock and write lock of the read-write lock object

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();
        }
    }
}

Then start thread verification

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();
}

Thread Pool

Introduction

In order to reduce the overhead of creating, maintaining, and destroying threads, we can use thread pools to create scheduling threads instead of directly new

usage

The thread pool can be created through the relevant new method of Executors

newFixedThreadPool() creates a fixed-size thread pool

newCachedThreadPool() creates a cache thread pool, the capacity can be adjusted according to needs

newSingleThreadExecutor() creates a single thread pool

newScheduledThreadPool() creates a fixed-size thread pool and executes tasks regularly

ExecutorService pool = Executors.newFixedThreadPool(5);

After creation, you can execute the task through the submit() method of the thread pool

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);
    });
}

Finally, close the thread pool

pool.shutdown();

shutdown() will wait for all running child threads to end and then shut down, and shutdownNow() will immediately shut down and terminate the child threads that have not yet ended.

For ScheduledExecutorService scheduling tasks, you need to call its schedule() method, pass in the runnable or callable object, delay and delay unit

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();

If you are interested in reading the thread pool source code, you can refer to the article Brief Analysis of Thread Pool Source Code for Android Development and Learning

ForkJoinPool branch/merge framework

Introduction

Fork/Join framework: If necessary, split a large task (Fork) into several small tasks until it can no longer be divided; then merge the results of the small tasks (Join) into the results of the large task

This is similar to the MapReduce framework, except that the splitting process of MapReduce is inevitable

 

Work stealing mode:

When a subtask thread completes the tasks in its task queue, and the big task has not ended, then it will randomly steal a task from the end of the queue of other subtask threads to execute

use

First define the task class, inherit the RecursiveTask parent class, specify the generic type, and implement the method 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();
    }

Pay attention to the choice of critical value, not too small

Then test in the main method, use ForkJoinPool to perform the task


ForkJoinPool pool = new ForkJoinPool();

System.out.println(pool.invoke(new FJSumCalculate(1L, 99999L)));

pool.shutdown();

instead

LongStream can also be used in java8 to perform similar parallel accumulation logic

LongStream.rangeClosed(1L, 99999L).parallel().reduce(Long::sum).getAsLong();

Conclusion

This is the end of the JUC thread study notes, welcome everyone

Guess you like

Origin blog.csdn.net/qq_37475168/article/details/107060372