In interviews, you will sometimes be asked if you have used multithreading on projects.

For ordinary fresh graduates or working hours not long junior development?? — CRUd shed tears of no technology.

The blogger has put together a simple example of a project that uses multithreading in the hope that it will inspire you.

Multithreaded Development example

Application background

The background of the application is very simple. The project done by the blogger is an audit project, and the audit data need to be pushed to the third-party supervision system. This is just a simple connection, but there is a problem.

We need to push about 300,000 data, but the interface provided by the third-party supervision only supports a single push (don’t ask why not support batch, ask is not to discuss tear theory than better). It can be estimated that 300,000 pieces of data, a data in 3 seconds, roughly 250 (why exactly this number) hours.

Therefore, it is considered to introduce multi-threading to carry out concurrent operation, reduce the time of data push, and improve the real-time data push.

Design points

To prevent the repeat

The data we push to the third party must not be pushed repeatedly, and there must be a mechanism to ensure the isolation of data pushed by each thread.

Here are two ideas:

    1. All data is fetched into a collection (memory) and then cut, each thread pushing a different segment of data
    1. Using the database paging mode, each thread takes the data push of the interval of [start,limit], we need to ensure the consistency of start

The second method is used because loading all the data into memory may have a large memory footprint, considering that the amount of data may continue to increase.

Failure mechanism

We also have to take into account the failure of the thread to push data.

If it’s your own system, we can pull out the method called by multiple threads and add a transaction, a thread exception, the whole rollback.

However, we could not do transactions with the third-party connection, so we adopted the method of directly recording the failure state in the database, and we could deal with the failed data in other ways later.

Thread pool selection

In practice, we definitely want to use thread pools to manage threads. For thread pools, we usually use ThreadPoolExecutor as a thread pool service. SpringBoot also provides an asynchronous approach to thread pools, although SprignBoot is more convenient. But using ThreadPoolExecutor is more intuitive to control the pool, so we create the pool directly using the ThreadPoolExecutor constructor.

Schematic diagram of technical design:

The core code

There’s a lot of buzz. It’s time to show you code. I extracted the code from the project and simplified it to an example.

The core code is as follows:

/ * * *@AuthorThree points *@Date 2021/3/5
 * @Description* /
@Service
public class PushProcessServiceImpl implements PushProcessService {
    @Autowired
    private PushUtil pushUtil;
    @Autowired
    private PushProcessMapper pushProcessMapper;

    private final static Logger logger = LoggerFactory.getLogger(PushProcessServiceImpl.class);

    // The number of queries per thread per time
    private static final Integer LIMIT = 5000;
    // The number of threads from
    private static final Integer THREAD_NUM = 5;
    // Create a thread pool
    ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2.0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));

    @Override
    public void pushData(a) throws ExecutionException, InterruptedException {
        // The counter can only be called by one request at a time, preventing data from being pushed repeatedly
        int count = 0;
        // Total number of unpushed data
        Integer total = pushProcessMapper.countPushRecordsByState(0);
        logger.info("Number of unpushed data items: {}", total);
        // Calculate how many rounds are needed
        int num = total / (LIMIT * THREAD_NUM) + 1;
        logger.info("Number of rounds to go through :{}", num);
        // Count the total number of successfully pushed data pieces
        int totalSuccessCount = 0;
        for (int i = 0; i < num; i++) {
            // The receiving thread returns the result
            List<Future<Integer>> futureList = new ArrayList<>(32);
            // Start THREAD_NUM parallel query update library, lock
            for (int j = 0; j < THREAD_NUM; j++) {
                synchronized (PushProcessServiceImpl.class) {
                    int start = count * LIMIT;
                    count++;
                    // The submission thread is identified by its data starting position
                    Future<Integer> future = pool.submit(new PushDataTask(start, LIMIT, start));
                    // Do not set the value first, to prevent blocking, put into the collectionfutureList.add(future); }}// Count the success data of this round of push
            for (Future f : futureList) {
                totalSuccessCount = totalSuccessCount + (int) f.get(); }}// Update the push flag
        pushProcessMapper.updateAllState(1);
        logger.info("Data push completed, data need to be pushed :{}, push success :{}", total, totalSuccessCount);
    }

    /** * Push data thread class */
    class PushDataTask implements Callable<Integer> {
        int start;
        int limit;
        int threadNo;   // Thread number

        PushDataTask(int start, int limit, int threadNo) {
            this.start = start;
            this.limit = limit;
            this.threadNo = threadNo;
        }

        @Override
        public Integer call(a) throws Exception {
            int count = 0;
            // Push data
            List<PushProcess> pushProcessList = pushProcessMapper.findPushRecordsByStateLimit(0, start, limit);
            if (CollectionUtils.isEmpty(pushProcessList)) {
                return count;
            }
            logger.info("Thread {} starts pushing data", threadNo);
            for (PushProcess process : pushProcessList) {
                boolean isSuccess = pushUtil.sendRecord(process);
                if (isSuccess) {   // Push succeeded
                    // Update the push identifier
                    pushProcessMapper.updateFlagById(process.getId(), 1);
                    count++;
                } else {  // Push failed
                    pushProcessMapper.updateFlagById(process.getId(), 2);
                }
            }
            logger.info("Thread {} pushed {} successfully", threadNo, count);
            returncount; }}}Copy the code

The code is very long, but let’s briefly talk about the key points:

  • Thread creation: The inner thread class has chosen to implement the Callable interface so that it is easy to get the result of the execution of a thread task, which in this case is used to count the number of successful thread pushes
 class PushDataTask implements Callable<Integer> {
Copy the code
  • Create a thread pool using ThreadPoolExecutor,
  // Create a thread pool
      ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM * 2.0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
Copy the code

The main structural parameters are as follows:

– corePoolSize: The thread core parameter is set to 5

– maximumPoolSize: Indicates the maximum number of threads. The value is a multiple of 2 of the number of core threads

– keepAliveTime: The keepAliveTime of non-core idle threads is set to 0

– unit: duration for non-core threads to stay alive timeunit. SECONDS is selected

– workQueue: Thread pool waiting queue. Block the queue with a LinkedBlockingQueue with an initial size of 100

The default AbortPolicy is AbortPolicy: discard the task and throw an exception.

  • Use synchronized to ensure that pushed operations can only be invoked by a single request

  synchronized (PushProcessServiceImpl.class) {
Copy the code
  • Use a collection to receive the result of a thread’s run, preventing blocking
List<Future<Integer>> futureList = new ArrayList<>(32);
Copy the code

Ok, so that’s the main code and simple parsing.

For this simple demo, this is just a simple push data processing. Consider if this example can be used elsewhere in your project. For example, monitoring system data verification, auditing system data statistics, e-commerce system data analysis, wherever there is a lot of data processing, you can combine this example into your project, so that you have multi-threaded development experience.

The full repository address is at the bottom of this article 👇👇

The online interviewer

  • Interviewer: Good for you, young man.
  • Third: Of course.
  • Interviewer: Yo, young man, very confident, then I have to give you a good test.
  • Old three: put horse come over, but test no harm.

Interviewer: Let’s start with the simplest. What is a thread

To talk about threads, you must first talk about processes.

Process is a program execution process, is the basic unit of the system to run the program, so the process is dynamic. System run a program is a process from the creation, run to the death of the process.

A thread is similar to a process, but a thread is a smaller unit of execution than a process. A process can generate multiple threads during its execution. Unlike the process heap of similar process are Shared by multiple threads and method of area resources, but each thread has its own program counter, the virtual machine and the local method stack, so the system in one thread, or switch between individual threads, burden is much smaller than the process, also because of this, thread, also known as a lightweight process.

Interviewer: How do you create threads in Java

There are three main ways to create threads in Java:

  • Inherit from Thread: The Thread class is essentially an instance that implements the Runnable interface and represents an instance of a Thread. The only way to start a Thread is through the start() instance method of the Thread class. The start() method is a native method that will start a new thread and execute the run() method.

  • Implement a Runnable interface: If your class extends another class, you can’t extends Thread directly. In this case, you can implement a Runnable interface.

  • Implement the Callable interface and override the call() method to return a Future value. This is what I used in the example above.

Interviewer: Talk about the thread lifecycle and state

In Java, threads have six states:

state instructions
NEW Initial state: The thread has been created, but the start() method has not yet been called
RUNNABLE Health status: Java threads generally refer to both ready and running states in the operating system as “running”
BLOCKED Blocked: The thread is blocked by the lock
WAITING Wait state: indicates that the thread is in a wait state. Entering this state indicates that the current thread needs to wait for some specific action (notification or interrupt) by another thread.
TIME_WAITING Timeout wait state: Unlike WAITIND, this state is self-returned at a specified time
TERMINATED Terminated status: Indicates that the current thread has completed execution

Threads do not stay in a fixed state during their lifetime. Instead, they switch between different states as the code is executed. Java thread states change as shown in the diagram below:

Interviewer: I see you mentioned thread blocking, so why not talk about thread deadlock

A thread deadlock describes a situation in which multiple threads are blocked while one or all of them wait for a resource to be released. Because the thread is blocked indefinitely, the program cannot terminate normally.

As shown in the figure below, thread A holds resource 2 and thread B holds resource 1, and they both want to apply for each other’s resource, so the two threads wait for each other and enter A deadlock state.

To generate a deadlock, four conditions must be met:

  1. Mutually exclusive: The resource can be occupied by only one thread at any time.

  2. Request and hold condition: when a process is blocked by a request for a resource, it holds on to a resource it has acquired.

  3. No deprivation condition: the obtained resources cannot be forcibly deprived by other threads before they are used up, and the resources can be released only after they are used up.

  4. Cyclic wait condition: Several processes form an end-to-end cyclic wait resource relationship.

Interviewer: How do you avoid deadlocks?

In order to avoid a deadlock, we only need to break one of the four conditions that produce a deadlock.

  1. Break the mutex: There is no way to break this condition, because the lock is intended to make them mutually exclusive (critical resources require mutually exclusive access).

  2. Break request and hold conditions: request all resources at once.

  3. Destruct the non-deprivation condition: when a thread that occupies a part of a resource applies for other resources, it can actively release the resources it occupies if it fails to apply for other resources.

  4. Breaking cycle waiting conditions: prevent by ordering resources. If resources are applied for in a certain order, they are released in reverse order. Breaks cyclic wait conditions.

Interviewer: I see you used synchronized in your example. Tell me about the use of synchronized

Three main uses of the synchronized keyword:

1. Modify instance method: lock the current object instance, before entering the synchronization code to obtain the lock of the current object instance

synchronized void method(a) {
 // Business code
}
Copy the code

2. Modify static methods: lock the current class, apply to all object instances of the class, and obtain the lock of the current class before entering the synchronization code. Because static members do not belong to any instance object, they are members of the class (static means that this is a static resource of the class, no matter how many objects are new, there is only one copy). Thus, if thread A calls A non-static synchronized method of an instance object, and thread B needs to call A static synchronized method of the class to which the instance object belongs, this is allowed, and mutual exclusion does not occur. This is because access to a lock held by a static synchronized method is a lock of the current class, whereas access to a lock held by a non-static synchronized method is a lock of the current instance object.

synchronized void staic method(a) {
 // Business code
}
Copy the code

**3.** modifies code block: specifies the object to be locked, and locks the given object/class. Synchronized (this | object) said before entering the synchronization code base for a given object lock. Synchronized means to acquire the lock of the current class before entering the synchronized code

synchronized(this) {
 // Business code
}
Copy the code

In my example, WE use synchronized to modify the code block, lock the PushProcessServiceImpl class, and obtain the lock of the current class before entering the synchronized code, preventing PushProcessServiceImpl objects from calling push data methods in the control layer.

Interviewer: Is there any other way to use synchronized besides using it? Let me elaborate on that

You can use the locks provided by the JUC package. The main classes and interfaces associated with the Lock interface are as follows.

Main methods in Lock:

  • Lock: Used to acquire the lock. If the lock is acquired by another thread, it enters the wait state.
  • LockInterruptibly: If a thread is waiting to acquire the lock when this method is used to acquire it, the thread can respond to an interruption, which interrupts the thread’s wait state.
  • TryLock: The tryLock method returns a value that is used to attempt to acquire the lock, returning true on success or false on failure (i.e. the lock has been acquired by another thread).
  • TryLock (long, TimeUnit) : Similar to tryLock, except that there is a wait time. Return true if the lock is acquired during the wait time, and false if it times out.
  • Unlock: Releases the lock.

Other interfaces and classes:

  • ReetrantLock (reentrant Lock) : Implements the Lock interface, can reentrant Lock, and internally defines fair Lock and non-fair Lock. Can do everything synchronized can do.
  • ReadWriteLock:
public interface ReadWriteLock {  
    Lock readLock(a);       // Obtain the read lock
    Lock writeLock(a);      // Obtain the write lock
}  
Copy the code

One to get a read lock and one to get a write lock. This means that the read and write operations on the file are divided into two locks to be allocated to threads, so that multiple threads can read at the same time.

  • ReetrantReadWriteLock: ReetrantReadWriteLock also supports fairness selection, reentry, and lock degradation.

Interviewer: Tell me the difference between synchronized and Lock

category synchronized Lock
There are levels Java keywords, at the JVM level Is an interface, API level
The release of the lock When the thread that acquired the lock finishes executing the synchronization code, the JVM releases the lock The lock must be released in the finally, otherwise it can cause a thread deadlock
To acquire the lock Suppose thread A acquires the lock and thread B waits. If thread A blocks, thread B will wait A Lock can be acquired in more than one way, which we’ll talk about below. Basically, a thread can try to acquire a Lock without having to wait
The lock state Unable to determine Can be judged
The lock type Reentrant non-interruptible non-fair Reentrant judging fair (both)
performance A small amount of synchronization A large number of simultaneous

Interviewer: You mentioned synchronized at the JVM level. Do you know anything about that?

Synchronized takes advantage of the atomic built-in locks (Monitor objects) provided by Java, with an ObjectMonitor object built into each object. This built-in and invisible lock is also known as a monitor lock.

Synchronous statement block

The implementation of the synchronized block uses the Monitorenter and monitorexit directives, with the monitorenter indicating the start of the synchronized block and the monitorexit indicating the end of the synchronized block.

When monitorenter is executed, it attempts to acquire the built-in lock. If the object is not locked or has already acquired a lock, the lock counter is +1. At this point, other threads competing for the lock will enter the waiting queue.

When monitorexit is executed, the counter is set to -1. When the counter is set to 0, the lock is released and threads in the waiting queue continue to compete for the lock.

Synchronized modification method

Synchronized modified methods do not have monitorenter or monitorexit directives; instead, they are replaced by the ACC_SYNCHRONIZED identifier, which indicates that the method is a synchronized method. This ACC_SYNCHRONIZED access flag is used by the JVM to tell if a method is declared to be synchronized and therefore to make the corresponding synchronized call.

Of course, the details are slightly different, but essentially they are both about acquiring atomic built-in locks.

Digging a little deeper, synchronized actually has two queues, waitSet and entryList.

  1. When multiple threads enter a synchronized block of code, they first enter entryList

  2. When a thread has acquired the monitor lock, it is assigned to the current thread and the counter is +1

  3. If a thread calls wait, it releases the lock, sets the current thread to null, counters -1, and enters a waitSet waiting to be awakened. If a thread calls notify or notifyAll, it enters an entryList contention lock

  4. If the thread finishes executing, the lock is also released, the counter is -1, and the current thread is set to NULL

Can you talk about the optimization of synchronized?

Since JDK1.6, synchronized itself has been improving its lock mechanics, and in some cases it’s not a serious lock. Optimization mechanisms include adaptive locking, spin locking, lock elimination, lock coarsening, bias locking, and lightweight locking.

The status of the lock from low to high is no lock **-> biased lock -> lightweight lock -> heavy lock. The process of upgrading is from low to high.

Spin-locking: Since most of the time locks are held for very short periods of time and shared variables are locked for very short periods of time, there is no need to suspend threads, and context switching back and forth between user and kernel mode seriously affects performance. The concept of spin is to let the thread perform a busy loop, which can be interpreted as doing nothing to prevent the thread from switching from user mode to kernel mode. Spin-locking can be enabled by setting -xx :+UseSpining. The default number of spins is 10, which can be set using -xx :PreBlockSpin.

Adaptive lock: An adaptive lock is an adaptive spin lock whose time is not fixed, but determined by the previous spin time on the same lock and the state of the lock holder.

Lock elimination: Lock elimination occurs when the JVM detects that some synchronized block of code has no data race at all, that is, no need to lock.

Lock coarsening: Lock coarsening is when multiple operations lock the same object, extending the synchronization of the lock beyond the entire sequence of operations.

Biased locking: When threads access synchronized blocks access to lock in the object lock records in the head and the stack frame store thread ID, biased locking this thread enters the synchronized block again after all don’t need the CAS to lock and unlock, biased locking will bias the first thread gets the lock, if no other threads subsequent won this lock, thread holding the lock will never need to synchronize, Conversely, when other threads compete for a biased lock, the thread holding the biased lock releases the biased lock. You can use the setting -xx :+UseBiasedLocking to open the bias lock.

Lightweight lock: When code enters a synchronized block, the JVM will use CAS to attempt to acquire the lock. If the update is successful, the JVM will mark the status bit in the object header as a lightweight lock. If the update fails, the current thread will attempt to spin to acquire the lock.

The process of lock upgrade is very complicated. To put it simply, biased locking is done by comparing the object header to the thread ID without even requiring CAS, while lightweight locking is done by modifying the object header record and spin, and heavyweight locking is done by blocking all but the thread that owns the lock.

Interviewer: Tell me about CAS

Compare And Swap/Set (CAS) the process of the CAS algorithm is as follows: It contains three parameters CAS(V,E,N). V for the variable to be updated (memory value), E for the expected value (old), and N for the new value. The value of V is set to N if and only if the value of V is equal to the value of E. If the value of V is different from the value of E, then another thread has already updated it, and the current thread does nothing. Finally, CAS returns the true value of the current V.

CAS is an optimistic lock that always thinks it can complete the operation successfully. When multiple threads concurrently operate on a variable using CAS, only one will win and update successfully, and the rest will fail. Failed threads are not suspended, they are simply told that they failed and allowed to try again, although failed threads are also allowed to abort. Based on this principle, the CAS operation can detect interference with the current thread by other threads and handle it appropriately, even if there is no lock.

Java. Util. Concurrent. Atomic package under most of the class is implemented using CAS operation (AtomicInteger AtomicBoolean, AtomicLong).

Interviewer: What problems will CAS cause?

  1. ABA problem:

For example, if one thread pulls A out of memory location V, then another thread, two, pulls A out of memory, and two does something to become B, and then two changes the data at V to A, At this time, thread One performs CAS operation and finds that the memory is still A, then one operation succeeds. Although the CAS operation for thread One was successful, there may be underlying problems. Starting with Java1.5, the JDK’s atomic package provides a class called AtomicStampedReference to solve ABA problems.

  1. Long cycle time and high overhead:

In the case of severe resource competition (severe thread conflict), CAS has a higher probability of spin, thus wasting more CPU resources and being less efficient than synchronized.

  1. Atomic operations that guarantee only one shared variable:

We can use a cyclic CAS to guarantee atomicity when we operate on one shared variable, but when we operate on multiple shared variables, the cyclic CAS cannot guarantee atomicity, so we can use locks.

Interviewer: Can you explain the principle of ReentrantLock

ReentrantLock is a ReentrantLock based on Lock. All locks are implemented based on AQS. AQS and Condition maintain different objects respectively. It provides shared locks and mutex based on the operation of state.

Interviewer: Can you talk about AQS

AbstractQueuedSynchronizer, abstract queue type synchronizer, AQS defines a set of synchronizer framework for multithreaded access to a Shared resource, many synchronization class implements are dependent on it, such as commonly used

Already/Semaphore/CountDownLatch.

The core idea of AQS is that if the requested shared resource is idle, the current thread requesting the resource is set as a valid worker thread, and the shared resource is set to the locked state. If the requested shared resource is occupied, then you need a mechanism for threads to block, wait, and allocate locks when they wake up. This mechanism AQS is implemented with CLH queue locking, which queues threads that are temporarily unable to acquire locks.

Take a look at the AQS schematic:

AQS uses an int member variable to represent the synchronization state, and queues resource threads through the built-in FIFO queue. AQS uses CAS to perform atomic operations on the synchronization state to modify its value.

private volatile int state;// Share variables, using volatile to ensure thread visibility
Copy the code

State information is operated on using protected types getState, setState, compareAndSetState

// Return the current value of the synchronization status
protected final int getState(a) {
 return state; }
// Set the value of synchronization status
protected final void setState(int newState) {
 state = newState; }
// atomically (CAS operation) set the synchronization state value to the given value update if the current synchronization state value equals expect (expected value)
protected final boolean compareAndSetState(int expect, int update) {
 return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
Copy the code

If the lock is successfully set to 1 and the current thread ID is assigned, then the lock is successful. Once the lock is acquired, other threads will be blocked and enter the blocking queue spin. When the lock is released, the thread that acquired the lock will wake up the thread in the blocking queue. Releasing the lock will reset state to 0 and the current thread ID to null.

Interviewer: can you say about the Semaphore CountDownLatch/CyclicBarrier

  • Semaphore – Allows multiple threads to access a resource simultaneously: Both synchronized and ReentrantLock allow only one thread to access a resource at a time, while Semaphore allows multiple threads to access a resource simultaneously.
  • CountDownLatch: CountDownLatch is a synchronization utility class that coordinates synchronization between multiple threads. This tool is usually used to control thread waiting. It allows a thread to wait until the countdown is over before executing.
  • CyclicBarrier: CyclicBarrier is very similar to CountDownLatch in that it can also implement technical waits between threads, but it is more complex and powerful than CountDownLatch. The main application scenarios of CountDownLatch are similar. CyclicBarrier literally means a CyclicBarrier. What it does is block a group of threads when they reach a barrier (also called a synchronization point) until the last thread reaches the barrier, the barrier opens and all threads intercepted by the barrier continue to work. The default constructor for CyclicBarrier is CyclicBarrier(int parties), whose argument is the number of threads that the barrier blocks. Each thread calls await() to tell CyclicBarrier that I have reached the barrier, and then the current thread is blocked.

Do you know the principle of volatile?

Volatile is a lighter option than synchronized’s approach to solving the memory visibility problem of shared variables, without the overhead of context switching. Using volatile to declare variables ensures that the updated value is immediately visible to other threads.

Volatile solves the problem of memory visibility by using memory barriers to ensure that no reorders of instructions occur.

We know that threads read shared variables from main memory into working memory and then write the results back to main memory, but this can cause visibility problems. For example, suppose we have a dual-core CPU architecture with two levels of caching, including L1 and L2 levels of caching.

If X is volatile and thread A reads X again, the CPU will force thread A to reload the latest value from main memory to its working memory according to the cache consistency protocol, rather than using the value from the cache directly.

As for the memory barrier issue, volatile fixes will add different memory barriers to ensure that visibility issues are properly implemented. The barriers here are based on what is provided in the book, but the actual memory barriers are different depending on the CPU architecture and the reordering strategy. For example, on x86 platforms, there is only one memory barrier, StoreLoad.

  1. The StoreStore barrier ensures that normal writes on it are not reordered by volatile writes

  2. The StoreLoad barrier ensures that volatile reads and subsequent volatile reads will not be reordered

  3. LoadLoad barrier, which prohibits volatile reads from being reordered with subsequent normal reads

  4. LoadStore barrier, which disallows volatile reads and subsequent ordinary write reordering

Interviewer: Tell me about your understanding of the Java Memory model (JMM) and why you use it

With the development of CPU and memory speed difference, resulting in the CPU speed is much faster than memory, so now the CPU added cache, cache can be generally divided into L1, L2, L3 level cache. Based on the example above, we know that this causes problems with cache consistency, so adding cache consistency protocol also causes problems with memory visibility, and compiler and CPU reordering causes problems with atomicity and order. The JMM memory model is a set of normative constraints on multi-threaded operations. Through the JMM, we can mask the memory access differences between different hardware and operating systems. This ensures that Java programs have consistent memory access across different platforms, and that programs can execute correctly when concurrency is high.

Interviewer: I see you are using thread pools. Can you tell me why

  1. Improve thread utilization and reduce resource consumption.
  2. Speed up the response. The creation time of the thread is T1, the execution time is T2, and the destruction time is T3. Using the thread pool eliminates the time of T1 and T3.
  3. Facilitates unified management of thread objects
  4. The maximum number of concurrency can be controlled

Interviewer: Can you talk about the core parameters of the thread pool?

Take a look at the ThreadPoolExecutor constructor:

public ThreadPoolExecutor(int corePoolSize,
                         int maximumPoolSize,
                         long keepAliveTime,
                         TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler) 
Copy the code
  • CorePoolSize: This value is used to initialize the number of core threads in a thread pool. When the number of thread pools in a thread pool is smaller than corePoolSize, the system creates a thread pool by default by adding a task. The number of corePoolSize threads can be started once by calling the prestartAllCoreThreads method. When the number of threads is equal to corePoolSize, new tasks are appended to the workQueue.

  • Allow the maximum number of threads maximumPoolSize: maximumPoolSize said to allow the maximum number of threads = (non-core threads + core number of threads), when BlockingQueue also full, However, when the total number of threads in the pool is less than maximumPoolSize, new threads are created again.

  • Active Time keepAliveTime: Non-core thread =(maximumPoolSize – corePoolSize), the maximum time that a non-core thread can survive idle without working.

  • Keepalive unit: The time for which non-core threads in the thread pool are kept alive

  • WorkQueue: A thread pool wait queue that maintains Runnable objects waiting to execute. When running when the number of threads is equal to corePoolSize, a new task is added to the workQueue, and if the workQueue is also full, a non-core thread is tried to execute the task

  • ThreadFactory: The factory used to create a new thread, which can be used to set the thread name, whether it is a daemon thread, and so on.

  • RejectedExecutionHandler: Saturation policy to be executed when corePoolSize, workQueue, and maximumPoolSize are not available.

Interviewer: Tell me the whole thread pool workflow

  1. When the thread pool is created, there are no threads in it. The task queue is passed in as an argument. However, even if there are tasks in the queue, the thread pool does not immediately execute them.

  2. When the execute() method is called to add a task, the thread pool does the following:

  • A) If the number of running threads is smaller than the corePoolSize, create a thread to run the task immediately;

  • B) Queue the task if the number of running threads is greater than or equal to corePoolSize;

  • C) If the queue is full and the number of threads running is less than maximumPoolSize, create a non-core thread to run the task immediately;

  • D) If the queue is full and the number of threads running is greater than or equal to maximumPoolSize, the thread pool is processed accordingly according to the denial policy.

  1. When a thread completes a task, it takes the next task off the queue to execute.

  2. When a thread has nothing to do for a certain amount of time (keepAliveTime), the thread pool determines that if the number of threads currently running is greater than the corePoolSize, the thread will be stopped. So after all the tasks of the thread pool are done, it eventually shrinks to the size of corePoolSize.

Interviewer: What are the rejection strategies

There are four main rejection strategies:

  1. AbortPolicy: Discard the task directly and throw an exception. This is the default policy

  2. CallerRunsPolicy: Only the caller’s thread is used to process the task

  3. DiscardOldestPolicy: Discards the oldest task in the waiting queue and executes the current task

  4. DiscardPolicy: Discards the task without throwing an exception

Interviewer: Tell me about your core thread count

Threads are a scarce resource in Java, and the pool is neither bigger nor smaller. Tasks are computation-intensive, IO – intensive, and mixed.

  1. It is recommended that the thread pool should not be too large. The number of cpus should be +1. +1 is because there may be page missing (that is, there may be some data in the hard disk that needs an extra thread to read the data into memory). If the number of thread pools is too large, frequent thread context switches and task scheduling may occur. The code for obtaining the current number of CPU cores is as follows:
Runtime.getRuntime().availableProcessors();
Copy the code
  1. IO intensive: the number of threads is moderately large, and the number of Cpu cores of the machine is *2.
  2. Mixed type: if the intensive station is large then the split is not necessary, if the IO type occupy a lot of necessary, Mark under.

Interviewer: Describe some common blocking queues

  1. ArrayBlockingQueue: Bounded blocking queue composed of array structures.

  2. LinkedBlockingQueue: A bounded blocking queue consisting of a linked list structure.

  3. PriorityBlockingQueue: Unbounded blocking queue that supports priority ordering.

  4. DelayQueue: An unbounded blocking queue implemented using a priority queue.

  5. SynchronousQueue: A blocking queue that does not store elements.

  6. LinkedTransferQueue: An unbounded blocking queue composed of a linked list structure.

  7. LinkedBlockingDeque: A two-way blocking queue consisting of a linked list structure

Interviewer: Tell me about some common thread pools

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

Note that Alibaba Java development manual fortifies the use of Executors to create the thread

The four typical thread pools are newFixedThreadPool, newSingleThreadExecutor, newCachedThreadPool,

NewScheduledThreadPool.

FixedThreadPool

  1. Thread pool of fixed length, with core threads, core threads is the maximum number of threads, no non-core threads.

  2. The unbounded queue to use is LinkedBlockingQueue. There is a risk of filling the waiting queue when used.

SingleThreadPool

There is only one thread to execute the task, which is suitable for sequential tasks and unbounded wait queues

CachedThreadPool

There are no core threads in the pool. The number of non-core threads is Integer. Max_value, which is infinite. Task queues are SynchronousQueue. If production is fast and consumption is slow, many threads will be created.

ScheduledThreadPoolExecutor

A pool of threads that execute tasks periodically, according to a specific schedule. There are core and non-core threads, which are also infinite in size. Suitable for performing periodic tasks.

Look at the constructor: whether the ThreadPoolExecutor constructor is called, the difference is that the task queue is DelayedWorkQueue.


  • Interviewer: These questions can be answered, very good, young man, very energetic!
  • Third: Thank you. Interviewer, look at this round of interviews…
  • Interviewer: Although your answer is very good, but the amount of data of your project is only one hundred thousand levels, which does not meet our requirements. So you can’t pass the interview.

The third is a left jab, followed by a right kick…

  • Interviewer: Ah… Young people do not speak martial arts, to attack…


Code address:Gitee.com/fighter3/th…

Well, through this article, I believe you have a certain understanding of the application and principle of multithreading. The crud mentioned at the beginning of the article is the blogger himself, the technical level is limited, it is inevitable to make mistakes, welcome to point out, thank you!




Reference:

[1] : Use multithreading to query millions of user data to convert Chinese characters into pinyin

[2] : This is a great way to learn about thread pools

[3] : SpringBoot Study Notes (17: Asynchronous call)

[4] : JavaGuide editor JavaGuide Interview Assault Edition

[5] : AI Xiaoxian edited “I want to enter the big factory interview summary”

[6] : Author unknown “Compilation of Java Core Knowledge Points”

[7] : Java concurrency basics, WHICH I sorted out with mind maps

[8] : Synchronized and lock

[9] : Synchronized and synchronized

[10] : Bugstack, “The Java Face Book Handbook”