Concurrent Programming Theme

Posted by Youko on Tue, 14 May 2019 18:47:51 +0200

CountDownLatch (counter)

CountDownLatch is located under a concurrent package and can be used to perform counter-like functions. If thread A needs to wait for other n threads before it can be executed, CountDownLatch can be used to achieve this function. CountDownLatch is implemented through a counter whose initial value is the number of threads, and each thread completes its own function After the task, the value of the counter will be reduced by 1. When the value of the counter is 0, it means that all threads have been executed and the waiting threads can be restored.

package com.kernel;

import java.util.concurrent.CountDownLatch;

public class Test001 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ",Subthread starts execution...");
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + ",Subthread terminates execution...");

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ",Subthread starts execution...");
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + ",Subthread terminates execution...");

            }
        }).start();
        countDownLatch.await();
        System.out.println("Execution of other threads is complete");
        System.out.println("Wait for the thread to execute...");
    }
}

Cyclic Barrier

Cyclic Barrier passes in a number when initialized, which records the number of threads calling the await method. Only when the number of threads is the same as the number provided when the object is created, will all threads entering the thread waiting be awakened to continue execution.

As the name implies, it's like a screen. Only when people arrive can they cross the barrier together.

Cyclic Barrier can also provide a construct that can be passed into a Runable object, which will wake up before all threads wake up after they pass through the barrier together.

package com.kernel;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

class Write extends Thread {
    private CyclicBarrier cyclicBarrier;

    public Write(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        System.out.println("thread" + Thread.currentThread().getName() + ",Writing data");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread" + Thread.currentThread().getName() + ",Write data successfully.....");
        try {
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println("All threads are executed");
    }
}

public class Test002 {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        for (int i = 0; i < 5; i++) {
            Write write = new Write(cyclicBarrier);
            write.start();
        }
    }
}

Semaphore

Semaphore is a count-based semaphore that can be created by specifying a value that specifies how many threads concurrently execute, apply before execution, return after execution. After that value, the thread application signal will be blocked, knowing that there are other threads holding the signal to execute the return signal.

Semaphore can be used to build object pools, resource pools, etc., such as database connection pools.

We can also create a Semaphore with a count of 1 as a mechanism similar to mutex, also known as a binary Semaphore, representing two mutex states.

package com.kernel;

import java.util.Random;
import java.util.concurrent.Semaphore;

public class Test003 extends Thread {
    private String name;
    private Semaphore windows;

    public Test003(String name, Semaphore windows) {
        this.name = name;
        this.windows = windows;
    }

    @Override
    public void run() {
        int availablePermits = windows.availablePermits();
        if (availablePermits > 0) {
            System.out.println(name + ":It's my turn at last.");
        } else {
            System.out.println(name + ":**,Could you hurry please");
        }
        try {
            windows.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + ":I want XXX,Remaining window");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + ":I bought it.");
        windows.release();


    }

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 5; i++) {
            Test003 test003 = new Test003("The first" + i + "personal", semaphore);
            test003.start();
        }
    }
}

Concurrent queue

  • Array Deque
  • PriorityQueue
  • Concurrent LinkedQueue (concurrent queue based on linked list)
  • DelayQueue (deferred blocking queue, blocking queue implements BlockingQueue interface)
  • Array BlockingQueue (Array-based concurrent blocking queue)
  • LinkedBlockingQueue (linked list-based FIFO blocking queue)
  • Linked Blocking Deque
  • Priority Blocking Queue
  • SynchronousQueue (concurrent synchronous blocking queue)

Concurrent LinkedDeque

It is an unbounded queue with higher performance than blocking queue. It is a thread-safe queue based on linked list and does not allow null.

Add and offer are ways to add elements, and there is no difference between them.

poll removes and deletes elements from the queue

peek looks at the queue header element without deleting it

BlockingQueue

Is a bounded queue, blocking queue common phrase production consumer scenario

Blockages can occur in the following situations:

The queue element is full and contains elements in it.

Queue element empty, also want to get elements from the queue

Producer-consumer model based on blocking queue

package com.kernel;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class ProducerThread implements Runnable {
    private BlockingQueue<String> blockingQueue;
    private AtomicInteger atomicInteger = new AtomicInteger();
    private volatile boolean flag = true;

    public ProducerThread(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        System.out.println("###Producer thread has been started###");
        while (flag) {
            String data = atomicInteger.incrementAndGet() + "";
            try {
                boolean offer = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
                if (offer) {
                    System.out.println(Thread.currentThread().getName() + ", Production queue" + data + "Success");
                } else {
                    System.out.println(Thread.currentThread().getName() + ", Production queue" + data + "fail");
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Producer thread termination");
    }

    public void stop() {
        this.flag = false;
    }
}

class CustomerThread implements Runnable {

    private BlockingQueue<String> blockingQueue;
    private volatile boolean flag = true;

    public CustomerThread(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        System.out.println("###The consumer thread has been started###");
        while (flag) {
            String data = null;
            try {
                data = blockingQueue.poll(2, TimeUnit.SECONDS);
                if (data == null) {
                    flag = false;
                    System.out.println("Consumers did not receive messages for more than 2 seconds.");
                    return;
                }
                System.out.println("Successful Queue Information Access by Consumers,data:" + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("The end of the consumer process");
    }
}

public class Test006 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        ProducerThread producerThread = new ProducerThread(blockingQueue);
        CustomerThread customerThread = new CustomerThread(blockingQueue);
        new Thread(producerThread).start();
        new Thread(customerThread).start();
        Thread.sleep(1000 * 10);
        producerThread.stop();
    }
}

Introduction to Thread Pool

Thread pool is actually a container that can hold threads, in which threads can be reused repeatedly, saving the consumption of repeatedly creating and destroying threads.

What are the advantages of thread pooling?

Reduce resource consumption: Reuse threads that have been created to save threads that have been created and destroyed repeatedly

Improving response speed: As everyone knows, creating threads is not immediately available. After creating threads, they enter the ready state. It needs CPU scheduling to get into the running state. With thread pool, as long as the task arrives, there are idle threads in the thread pool, they can work immediately.

Improving thread management: Threads are scarce resources. If created unrestrictedly, they will not only consume system resources, but also reduce system stability. Thread pool can be used for uniform allocation, tuning and monitoring.

Classification of thread pools:

newCachedThreadPool

Create a cacheable thread pool that can be recycled over and over again. If the number of tasks is greater than the number of natural threads, continue to create threads.

public class Test008 {
    public static void main(String[] args) {
        // Cacheable thread pool (reusable) infinite
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
}

newFixedThreadPool

Create a fixed-length thread and store it in the queue beyond the thread

public class Test009 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
}

newScheduledThreadPool

Create a thread pool with fixed length and support for timed and periodic task execution

public class Test010 {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 10; i++) {
            scheduledExecutorService.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
                // Represents a delay of 3 seconds
            },3, TimeUnit.SECONDS);
        }
    }
}

newSingleThreadExecutor

Create a single-threaded thread pool, which only uses a single worker thread to execute tasks, ensuring that all tasks are executed in a specified order (FIFO, LIFO, priority)

public class Test011 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
        executorService.shutdown();
    }
}

Thread pool source code analysis

  • Number of corePoolSize Core Threads (Number of Actual Running Threads)
  • Maximum PoolSize Maximum number of threads (maximum number of threads can be created)
  • Keeping AliveTime Thread Free Timeout
  • Unit time unit
  • WorQueue Cache Queue

To submit a task to the thread pool, first determine that the current number of threads is less than corePoolSize, and if it is less than corePoolSize, create a new thread to execute the task.

If the current number of threads is equal to corePoolSize, the task is added to the cache queue when the task comes back

If the cache queue is full, determine whether the current thread is less than maximumPoolSize

If it is less than maximumPoolSize, create threads to execute tasks; otherwise, adopt a task rejection strategy to process

If the current number of threads is greater than corePoolSize and the idle time of a thread is greater than keepAliveTime, the thread is terminated until the number of threads in the thread pool is not greater than corePoolSize.

If the thread lifetime in the core pool is allowed to be set, the idle time of the thread in the core pool exceeds keepAliveTime, and the thread terminates.

Reasonable configuration of thread pool

IO-intensive: This task requires a lot of IO, that is, a lot of blocking. Running IO-intensive tasks on a single thread will result in wasting a lot of CPU computing power and wasting waiting. Therefore, using multi-threading in IO-intensive tasks can greatly speed up the running of programs, even on a single-core CPU. This acceleration mainly takes advantage of the wasted blocking time, general IO. Task-intensive threads are set to 2 * core + 1

CPU-intensive: This task requires a lot of computation. Without IO blocking, the CPU has been running at full speed. CPU-intensive tasks can only be accelerated on a real multi-core CPU (through multi-threading). On a single-core CPU, no matter how many simulated multi-threads you drive, this task can not be accelerated, because the total computing power of the CPU is those, generally CPU-intensive tasks. Task threads are set to core + 1

Callable

In Java, there are two ways to create threads: inherit Thread or implement Runable interface. The disadvantage of these two ways is that after the thread task is executed, the execution result can not be obtained. So we usually use shared variables or shared memory and thread communication to get the result. Callable and Future are also provided in Java to achieve the task conclusion. The operation of a result, Callable is used to perform tasks and produce results, and Future is used to obtain results.

Common methods of Future

V get() gets the result of asynchronous execution, and if no result is available, this method blocks until the asynchronous computation is completed.

V get(Long timeout, Timeunit unit) gets the asynchronous execution result. If no result is available, this method will block, but there will be a time limit. If the blocking time exceeds the set timeout time, this method will throw an exception.

boolean isDone() returns true if the task ends, whether it ends normally or cancels halfway or if an exception occurs

boolean isCanceller() returns true if the task is cancelled before it is completed

Boolean cancel (boolean mayInterrupt If Running) If the task is not started, this method will return false, if cancel(true) is called during the task execution, it will try to stop the task by interrupting the task execution, if the stop is successful, return true, if cancel(false) does not affect the task execution, then return false, when the task is completed. Calling this method returns false, with parameters indicating whether the execution process is interrupted

Future mode

For multithreading, if thread A waits for the result of thread B, then thread A does not have to wait for the result of thread B, until B has the result, it can get a Future future first, and then get the real result after B has the result.

Simulate Future

Data

public interface Data {
    // Get the results of subthread execution
    public String getRequest() throws InterruptedException;
}

FutureData

public class FutureData implements Data {
    private boolean flag = false;
    private RealData realData;

    public synchronized void setRealData(RealData realData) {
        if (flag)
            return;
        this.realData = realData;
        flag = true;
        // awaken
        notify();
    }

    @Override
    public synchronized String getRequest() throws InterruptedException {
        while (!flag) {
            // wait for
            wait();
        }
        // Return result
        return realData.getRequest();
    }
}

RealData

public class RealData implements Data {
    private String result;

    public RealData(String data) throws InterruptedException {
        System.out.println("Downloading" + data);
        Thread.sleep(5000);
        System.out.println("Download finished!");
        result = "author:kernel";
    }

    @Override
    public String getRequest() {
        return result;
    }
}

FutureClient

public class FutureClient {
    public Data submit(String requestData) {
        FutureData futureData = new FutureData();
        new Thread(new Runnable() {
            @Override
            public void run() {
                RealData realData = null;
                try {
                    realData = new RealData(requestData);
                    futureData.setRealData(realData);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        return futureData;
    }
}

test

public class Test002 {
    public static void main(String[] args) throws InterruptedException {
        FutureClient futureClient = new FutureClient();
        Data request = futureClient.submit("Request parameters");
        System.out.println("The request was sent successfully");
        // What should the main thread do?
        System.out.println("Perform other tasks");
        // Get the results of other threads
        String result = request.getRequest();
        System.out.println("Get results" + result);
    }
}

Execution process:

First, create a request to FutureClient, then real Data performs the download task, encapsulates the result, and then gets the result function to monitor whether the thread gets the result at any time. If it gets it, it returns. If it does not get it, it will block all the time. The function that sets the result will wake up the function that returns the result immediately.

Reentrant lock

Re-entry locks, also known as recursive locks, refer to memory functions that still have code to acquire the lock after the same thread's outer function has acquired the lock, but are not affected.

ReentrantLock (explicit lock, lightweight lock) and Synchronized (built-in lock, heavyweight lock) are re-entrant locks

Read write lock

When the program involves some read and write operations on shared variables, there is no problem for multiple threads to read and write simultaneously without write operations. If one thread is doing write operations, other threads should not read or write operations on it.

Reading and writing coexist, reading and reading can coexist, writing and writing can not coexist.

public class Test001 {
    private volatile Map<String, Object> cache = new HashMap<String, Object>();
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private ReadLock readLock = reentrantReadWriteLock.readLock();
    private WriteLock writeLock = reentrantReadWriteLock.writeLock();

    public void put(String key, String value) {
        try {
            writeLock.lock();
            System.out.println("Write in put Method key: " + key + ",value" + value + ",start");
            Thread.sleep(1000);
            cache.put(key, value);
            System.out.println("Write in put Method key: " + key + ",value" + value + ",End");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    public String get(String key) {
        try {
            readLock.lock();
            System.out.println("read key: "+ key + ",start");
            Thread.sleep(1000);
            String value = (String) cache.get(key);
            System.out.println("read key: "+ key + ",End");
            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            readLock.unlock();
        }
    }

    public static void main(String[] args) {
        Test001 test001 = new Test001();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test001.put("i", i + "");
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    test001.get(i + "");
                }
            }
        });
        t2.start();
    }
}

Pessimistic Lock

Pessimistic locks pessimistically think that every operation will cause the loss of updates, adding exclusive locks to every query, and thinking that others will modify every time they go to get data, so they will lock every time they get data, so that others will block the data until it gets the lock. Traditional relational databases use many of these locking mechanisms, such as Line locks, table locks, read locks, write locks, etc. are all locked before operation.

Optimistic lock

Optimistic locks are optimistic that each query will not result in loss of updates, using version field control.

First, we query the publication number by condition, and then judge whether the current version number is the same as the previous version number when updating. If it is consistent, it proves that no one has changed it, updating it directly, otherwise, we will re-query to update it.

CAS lockless mechanism (using atomic classes)

Compared with lock, using CAS lock-free mechanism will make the program look more complex, but due to its non-blocking nature, deadlock will not occur, and the interaction between threads is much less than that of lock. There is no system overhead caused by competition between locks and the overhead caused by frequent calls between threads when using CAS.

CAS principle

CAS includes three parameters, namely V (representing the variables to be updated, the value of main memory), E (representing the expected value, the value of local memory), N (new value). If V = E, then the value of V is set to N. If V!= E, indicating that other threads have modified, the current thread does nothing. Finally, CAS returns the true value of the current V.

CAS is very similar to optimistic lock, which is handled with optimistic attitude. Multiple threads use CAS operation at the same time, only one can win, the others all fail. The threads that fail will not hang up, only inform of failure, and allow to try again, of course, also allow to give up. Based on this principle, CAS operation can find that other threads are against the current thread even if there is no lock. Disturbance and proper handling

Simply put, CAS requires you to give an additional expectation, which is what you think the variable should look like now. If the variable is not what you think it should be, it means that it has been modified. You just read it again and try to modify it again.

Advantages and disadvantages

Advantage:

In the case of high concurrency, it has better performance than locking programs.

Deadlock immunity

Disadvantages:

There is an obvious problem in CAS, that is, ABA problem.

If it was changed to B during this period and then back to A, the CAS operation would mistakenly assume that it had never been modified. In response to this situation, a tagged atomic reference class, AtomicStampedReference, is provided in the concurrent package, which can ensure the correctness of CAS by controlling the version of the value of the variable.

Atomic class

Atomic operation classes in Java can be roughly divided into four categories: atomic update basic type, atomic update array type, atomic update reference type and atomic update attribute type. All of these atomic classes use the concept of unlock, and some places use the thread safety type of CAS operation directly.
AtomicBoolean

AtomicInteger

AtomicLong

AtomicReference

Topics: Java Windows less Database