Java concurrent programming is widely used in practical work. Sometimes it is necessary to do something asynchronously through multithreading, and sometimes it is necessary to improve the efficiency of a task execution through multithreading. The most frequently asked questions in an interview with an Internet company. This article is a bit long with a lot of code. Please be patient to finish it.
A key concept
Context switch
-
Concept: CPU allocates running time to runnable threads through time slice algorithm. When switching between different threads, the state of the current thread needs to be saved and the state information of the thread to be executed needs to be restored. This process is context switch.
-
How can context switches be reduced or avoided?
-
Lockless concurrent programming
-
CAS algorithm
-
Using minimum threads
-
coroutines
A deadlock
-
Concept: Two or more threads hold a lock on which the other is waiting
-
How do I avoid deadlocks?
-
Avoid one thread acquiring multiple locks at the same time
-
Avoid a thread occupying multiple resources in a lock at the same time. Try to ensure that each lock occupies only one resource
-
Try using a timing lock
-
For database locks, lock and unlock must be in a database connection
Java concurrency underlying mechanism
volatile
-
Purpose: In multiprocessor development to ensure the visibility of a shared variable between multiple threads, that is, when one thread changes the value of the variable, other threads can immediately see the latest value of the variable.
-
How it works: Writing to a variable that is volatile does two things
-
Write the current processor cache line to system memory;
-
Invalidate data cached by other cpus at this memory address
-
Key points of use:
-
Volatile only guarantees visibility, not synchronization. For example, if the changed value for a variable depends on the last changed value, using volatile does not guarantee concurrency safety
synchronized
-
Definition: Synchronized is a method of communication between Java multithreads. There are three specific applications of synchronized:
-
For normal synchronous methods, the lock is the current instance object
-
For statically synchronized methods, the lock is the Class object of the current Class
-
For blocks of synchronized code, locks are objects configured in synchronized parentheses
-
Key points of use:
-
Constructors cannot be modified with synchronized
-
It is recommended to minimize the granularity of locks, for example, if a synchronized code block can satisfy a requirement without using a synchronized method
-
If you can confirm that all locks in an application are competed by different threads in most cases, you can disable biased locking by running -xx :+UseBiasedLocking to improve performance.
-
The Java object header stores the address of the Monitor Record, which records the threads that hold it. The Java object header stores the address of the Monitor Record, which records the threads that hold it.
-
Monitor: Monitor is not a special object, but a method or mechanism that Java uses to control access to an object. Every object in Java is associated with a Monitor. Only one Thread can lock a Monitor at a time. When a monitor is locked by a thread, other threads attempting to lock the monitor can only block wait.
-
Object header: Synchronized lock status is described in the header of a Java object. The object header includes Mark Word and Klass Word.
-
In a 32-bit virtual machine, the overall object header size is 64bits (8 bytes), with Mark Word and Klass Word occupying 4 bytes each.
-
Lock state: There are four types of locks in Java from lowest level to highest. Lock state – > bias lock – > Lightweight lock – > heavyweight lock. Biased lock depends on a field in Mark Word pointing to the current thread to identify whether the holder of the lock is the current thread, if so, directly into the synchronization code block; Assuming biased lock is disabled, lightweight lock refers to the state that two threads acquire the lock. One thread obtains the lock, and the other thread fails to obtain the lock. First, CAS spins to acquire the lock.
-
Lock upgrade process, only from low to high, not from high to low, avoid unnecessary waste of resources. For example, if a lock has reached a heavyweight lock status, subsequent threads competing for the lock will simply block and not perform CAS spin. There’s a nice picture in Reference 7 that I put here:
Object Header (32-bit VM)
Atomic operation
CPU level atomic operations
Atomic operations at the CPU level rely on CPU instructions that manipulate data in memory across the bus, so there are two ways in the CPU:
-
LOCK bus: use the LOCK instruction to the bus signal, to achieve a
-
Lock cache: at some point in time, only operations on a memory address need to be atomic;
Atomic operations in JAVA
Atomic operations can be implemented in Java through CAS and locks.
-
Atomic operations are implemented using CAS. Starting with Java1.5, the Java.lang. concurrent package provides many classes to support atomic operations, such as AotmicIntenger and AtomicLong, which can add or subtract the current value of a variable atomically.
-
Atomic operations are implemented using locks, which ensure that only the thread holding the lock can operate on a specified variable.
Having said that, the interview question must be asked – the use and extension and optimization of thread pools for concurrent programming
It’s code time, so to speak
In short, after a thread pool is used, the creating thread handles getting free threads from the pool, and closing the thread becomes returning threads to the pool. In other words, improved thread reuse.
The JDK provided me with thread pool tools out of the box after 1.5, and we’ll learn how to use them today.
-
Executors Thread pool Specifies the thread pools that can be created by the factory
-
How do I create a thread pool manually
-
How do I extend the thread pool
-
How do I optimize exception information for thread pools
-
How do I design the number of threads in a thread pool
1. Executors Thread pool Specifies the thread pools that can be created by the factory
Let’s start with the simplest thread pool usage example:
staticclass MyTask implements Runnable {
@Override
public void run() {
System.out
.println(System.currentTimeMillis() + “: Thread ID :” + Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyTask myTask = new MyTask();
ExecutorService service1 = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
service1.submit(myTask);
}
service1.shutdown();
}
Running results:
We created a thread pool instance, set the default number of threads to 5, and submitted 10 tasks to the pool, printing the current millisecond time and the thread ID. From the result, we can see that 5 threads with the same ID printed the millisecond time.
This is the simplest example.
Now let’s talk about other ways to create threads.
1. The fixed thread pool ExecutorService service1 = Executors. NewFixedThreadPool (5); This method returns a thread pool with a fixed number of threads. The number of threads in this thread pool is always the same. When a new task is submitted, if there are idle threads in the thread pool, the task will be executed immediately. If there are no idle threads, the new task will be temporarily stored in a task queue (default unbounded queue INT maximum number). When there are idle threads, the task in the task queue will be processed.
2. The singleton thread pool ExecutorService service3 = Executors. NewSingleThreadExecutor (); This method returns a thread pool with only one thread. If more than one task is submitted to the thread pool, the task is stored in a task queue (default unbounded int maximum number) and executed in first-in, first-out order until the thread is idle.
3. The cached thread pool ExecutorService service2 = Executors. NewCachedThreadPool (); This method returns a pool of threads that can be adjusted according to the actual situation. The number of threads in the pool is uncertain, but if there are idle threads that can be reused, the reusable threads are preferred, all threads are working, and if a new task is submitted, a new thread is created to handle the task. All threads will return to the thread pool for reuse after completing the current task.
4. The task to the calling thread pool ExecutorService service4 = Executors. NewScheduledThreadPool (2); The method also returns a ScheduledThreadPoolExecutor object, the thread pool can specify the number of threads.
There is no difference in the usage of the first three threads, but the key is the fourth thread pool, which we can still learn from despite the many threading task scheduling frameworks. How do you use it? Here’s an example:
class A {
public static void main(String[] args) {
ScheduledThreadPoolExecutor service4 = (ScheduledThreadPoolExecutor) Executors
.newScheduledThreadPool(2);
// If the previous task is not completed, the scheduling will not start
service4.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
// If the execution time of the task is longer than the interval, the execution time is used (to prevent task stacking).
Thread.sleep(10000);
System.out.println(System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}// initialDelay indicates the first delay time; Period Indicates the interval
}, 0, 2, TimeUnit.SECONDS);
service4.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println(System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}// initialDelay indicates the delay time; Delay + Task execution time = Equal to the interval period
}, 0, 2, TimeUnit.SECONDS);
// Schedule tasks once at a given time
service4.schedule(new Runnable() {
@Override
public void run() {
System.out.println(” schedule executed after 5 seconds “);
}
}, 5, TimeUnit.SECONDS);
}
}
}
The above code creates a ScheduledThreadPoolExecutor task scheduling thread pool, call the three method, respectively, to explain emphatically scheduleAtFixedRate and scheduleWithFixedDelay method, The function of these two methods is very similar, the only difference is the calculation method of the interval time of executing characters. The former interval algorithm takes the long time according to the specified period and task execution time, while the latter takes the specified delay time + task execution time. If students are interested, you can run through the code above. You can see the clues.
Ok, JDK packaging to us to create a thread pool of four methods, however, please note that due to the highly encapsulate these methods, therefore, if use undeserved, out of the question will be, therefore, I would suggest that the programmer should be to manually create a thread pool, and is the premise of manually create highly understand thread pool parameter Settings. So let’s look at creating a thread pool manually.
2. How do I manually create a thread pool
Here is an example of creating a thread pool manually:
/ * *
* Default 5 threads (default number, i.e. minimum number),
* Maximum 20 threads (specifying the maximum number of threads in the thread pool),
* Idle time 0 seconds (when the thread pool carding exceeds the number of cores, the survival time of the excess idle time, i.e. how long the number of idle threads exceeding the number of cores, will be destroyed),
* Wait queue length 1024,
* Thread name [mxr-task -%d], easy to trace back,
* refused to strategies: when the task queue is full, throw RejectedExecutionException
* anomalies.
* /
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 20, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024)
, new ThreadFactoryBuilder().setNameFormat(“My-Task-%d”).build()
, new AbortPolicy()
);
ThreadPoolExecutor (ThreadPoolExecutor) has seven parameters. Let’s take a look:
-
CorePoolSize Number of core threads in the thread pool
-
MaximumPoolSize Maximum number of threads
-
KeepAliveTime Idle time (how long the excess idle time will be destroyed when the thread pool has exceeded the number of core threads)
-
Unit Time unit
-
WorkQueue Queue that needs to store tasks when the core thread is full of work
-
ThreadFactory Factory for creating threads
-
Handler Specifies the policy to reject a queue when it is full
We won’t talk about the first few parameters, it’s very simple, mainly the last few parameters, queue, thread factory, reject policy.
Let’s start with queues. The thread pool provides four queues by default.
-
Unbounded queue: the default size is int Max, so it may use up system memory and cause OOM, which is very dangerous.
-
Direct commit queue: no capacity, no save, directly create new threads, so need to set a large number of thread pools. Otherwise, it is easy and dangerous to implement rejection strategies.
-
Bounded queue: If core is full, it is stored in the queue, if core is full and the queue is full, threads are created until maximumPoolSize is reached, and if the queue is full and the maximum number of threads is reached, a rejection policy is executed.
-
Priority queue: Executes tasks based on their priorities. You can also set the size.
I used unbounded queue in my project, but set the task size to 1024. If you have a lot of tasks, it is recommended to split them into multiple thread pools. Don’t put all your eggs in one basket.
Now the rejection strategy, what is the rejection strategy? How to handle tasks that are still committed when the queue is full. The JDK has four policies by default.
-
AbortPolicy: Directly throws an exception to prevent the system from working properly.
-
CallerRunsPolicy: This policy runs the currently discarded task directly in the caller thread as long as the thread pool is not closed. Obviously, this will not actually drop the task, but it is highly likely that the performance of the task submission thread will drop dramatically.
-
DiscardOldestPolicy: This policy discards the oldest request, which is a task to be executed, and attempts to resubmit the current task.
-
DiscardPolicy: This policy silently discards unprocessed tasks without any processing, which I think is the best solution if the task is allowed to be lost.
Of course, if you are not happy with the RejectedExecutionHandler, you can implement the rejectedExecution method yourself.
Finally, thread factories, all threads in the thread pool are created by thread factories. The default thread factory is too single. Let’s look at the default thread factory:
/ * *
* The default thread factory
* /
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s ! = null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = “pool-” +
poolNumber.getAndIncrement() +
“-thread-“;
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() ! = Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
The thread name is pool- + thread pool number + -thread- + thread number. Set to non-daemon thread. The priority is the default.
What if we want to change the name? Implement the ThreadFactory interface and override the newThread method. But there are already artificial wheels, such as the ThreadFactoryBuilder factory provided by Google’s Guaua used in our example. You can customize the thread name, daemon or not, priority, exception handling and so on, powerful.
3. How do I extend the thread pool
So can we extend the functionality of thread pools? Such as recording the execution time of thread tasks. In fact, the JDK’s thread pool has reserved interfaces for us, and there are two empty methods in the thread pool core that are reserved for us. There is also a method called when the thread pool exits. Let’s look at an example:
/ * *
* How to extend the thread pool by overriding beforeExecute, afterExecute, and terminated methods, which are empty by default.
*
* You can monitor the start and end times of each thread’s task execution, or customize some enhancements.
*
These methods are called in the Worker’s runWork method
* /
public class ExtendThreadPoolDemo {
static class MyTask implements Runnable {
String name;
public MyTask(String name) {
this.name = name;
}
@Override
public void run() {
System.out
.println(” executing: Thread ID:” + thread.currentThread ().getid () + “, Task Name = “+ Name”);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println(” Ready to execute: “+ ((MyTask) r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(” execute done: “+ ((MyTask) r).name);
}
@Override
protected void terminated() {
System.out.println(” thread pool exits “);
}
};
for (int i = 0; i < 5; i++) {
MyTask myTask = new MyTask(“TASK-GEYM-” + i);
es.execute(myTask);
Thread.sleep(10);
}
es.shutdown();
}
}
We overrode the beforeExecute method, which is called before the task is executed, and the afterExecute method, which is called after the task is finished. There is also a terminated method that is called when the thread pool exits. What is the result of the execution?
As you can see, the before and after methods are called before and after each task execution. It’s like performing a cut. The shutdown method is called and the terminated method is called.
4. How to optimize thread pool exception information
How do I optimize exception information for thread pools? Before we get to that, let’s talk about a bug that isn’t easy to spot:
Look at the code:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0L,
TimeUnit.MILLISECONDS, new SynchronousQueue<>());
for (int i = 0; i < 5; i++) {
executor.submit(new DivTask(100, i));
}
}
static class DivTask implements Runnable {
int a, b;
public DivTask(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
double re = a / b;
System.out.println(re);
}
}
Execution Result:
Note: There are only 4 results, one of which is swallowed and has no information. Why is that? If you look closely at the code, you’ll see that 100/0 is bound to report an error, but there is no error message. Why? In fact, if you use the execute method you will print an error message, and if you use the Submit method without calling its GET method, the exception will be swallowed because, if an exception occurs, it will be returned as the return value.
What to do? We can use the execute method, of course, but we can override the submit method instead.
static class TraceThreadPoolExecutor extends ThreadPoolExecutor {
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
// super.execute(command);
super.execute(wrap(command, clientTrace(), Thread.currentThread().getName()));
}
@Override
public Future<? > submit(Runnable task) {
// return super.submit(task);
return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
}
private Exception clientTrace() {
return new Exception(“Client stack trace”);
}
private Runnable wrap(final Runnable task, final Exception clientStack,
String clientThreaName) {
return new Runnable() {
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
clientStack.printStackTrace();
throw e;
}
}
};
}
}
We rewrite the Submit method to encapsulate the exception information and print the stack information if an exception occurs. Let’s see what happens when we use the rewritten thread pool.
From the results, we can clearly see the cause of the error message: By Zero! And the stack information is clear, easy to troubleshoot. Optimized the default thread pool policy.
5. How to design the number of threads in the thread pool
The size of the thread pool has a certain impact on the performance of the system, too large or too small number of threads can not play the optimal system performance, but the determination of the size of the thread pool does not need to be very precise. The Java Concurrency in Practice post a general rule of thumb for sizing a thread pool, which is usually based on the number of cpus, the size of memory, and other factors.
The formula is a bit more complicated, but in a nutshell, if you are CPU intensive, you should have the same number of threads as the number of CPU cores, avoiding a lot of useless thread context switching. If you are IO intensive and require a lot of waiting, you can set the number of threads to be higher, such as CPU cores multiplied by 2.
As for how to obtain the number of CPU cores, Java provides a method:
The Runtime. GetRuntime (). AvailableProcessors ();
Returns the number of cores for the CPU.
conclusion
Ok, here, we have a high concurrency, how to use a thread pool, here, the author suggests that you manually create a thread pool, so that the various parameters of the thread pool can have a precise understanding, in the system for troubleshooting or tuning is good. For example, set the appropriate number of core threads, maximum number of threads, rejection policy, thread factory, queue size and type, etc., can also be G thread factory custom thread.
Ps: More good articles on architecture follow the architect inn, and the architecture information is available in the public account. I can’t say a good architecture article every day, but I will definitely update it when I have time.