78 an Exclusive Diagram of Java Concurrency Tools

78 An Exclusive Diagram of Java Concurrency Tools #

This lesson provides an outline of the key points in this column and organizes and summarizes the content of the previous 77 lessons to facilitate your review of the previous content. If you are preparing for an interview and don’t have time to read the previous content, you can quickly establish a Java concurrency knowledge system through this lesson. If you find any weak points in your knowledge, you can review the specific content of that lesson.

This column is divided into three major modules: Module 1: Concrete Concurrency Basics, Module 2: Exploring JUC (Java.util.concurrent) Concurrency Tools, Module 3: Delving into Low-Level Principles and Understanding the Reasons Behind Them. Let’s start with Module 1: Concrete Concurrency Basics.

Module 1: Concrete Concurrency Basics #

Ascension of Thread Foundations #

Firstly, the basic concepts of threads are explained and elaborated. In terms of implementing multithreading, only one method of implementing threads is discussed, and the traditional notions of two or three methods are analyzed. The correct method of thread termination is also explained, and it is pointed out that the use of volatile flag for thread termination is not comprehensive enough.

Then, six thread states (NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED) are introduced, along with their transition paths. After that, the focus is on methods such as wait, notify/notifyAll, and sleep, which are frequently tested in interviews. Their points of attention are explained, including:

  • Why must the wait method be used in synchronized code?
  • Why are wait/notify/notifyAll defined in the Object class while sleep is defined in the Thread class?

A comparison between wait/notify and sleep is also made, along with an analysis of their similarities and differences. Three approaches to implementing the producer-consumer pattern are then presented using wait/notify, Condition, and BlockingQueue, followed by a comparison between them.

Thread Safety #

In the lessons on thread safety, the concept of thread safety is first explained. The scenarios where threads are not safe include incorrect runtime results, publication or initialization errors, as well as liveness issues including deadlock, livelock, and starvation.

Next, four particular scenarios where thread safety needs to be emphasized are summarized. They are as follows:

  • When there are operations involving shared resources or variables.
  • Operations that rely on timing.
  • There is a binding relationship between different data.
  • The class being used does not declare itself as thread-safe.

Afterwards, the performance issues brought by multithreading are explained, including the context switches and cache invalidation caused by thread scheduling, as well as the overhead caused by thread cooperation.

Module 2: Exploring JUC (Java.util.concurrent) Concurrency Tools #

Thread Pool #

Next, we enter Module 2: Exploring JUC Concurrency Tools. In the thread pool section, we first present three reasons for using a thread pool, which means that using a thread pool is better than manually creating threads in the following aspects:

  • It can solve the system overhead problem of thread lifecycle and also speed up response time.
  • It can coordinate the use of memory and CPU to avoid improper resource usage.
  • It can centrally manage resources.

After understanding the benefits of thread pools, it is necessary to understand the meanings of each parameter of the thread pool, including corePoolSize, maxPoolSize, keepAliveTime, workQueue, ThreadFactory, and Handler. This is also a very common topic in interviews, and we need to know the meaning of each parameter.

The thread pool may also reject the tasks we submit. We explained 2 situations of rejection and 4 rejection policies, which are AbortPolicy, DiscardPolicy, DiscardOldestPolicy, and CallerRunsPolicy. We can choose the appropriate rejection policy according to our business needs.

We then introduced 6 common thread pools, including FixedThreadPool, CachedThreadPool, ScheduledThreadPool, SingleThreadExecutor, SingleThreadScheduledExecutor, and ForkJoinPool. Each of these 6 thread pools has its own characteristics and uses different parameters.

Next, we introduced blocking queues, and in thread pools, 3 types of blocking queues are commonly used: LinkedBlockingQueue, SynchronousQueue, and DelayedWorkQueue. We also explained why we should not create thread pools automatically. The main reason is that automatically created thread pools may pose risks such as OOM. By manually creating thread pools, we can clearly define their running rules and reject new task submissions when necessary, making it safer.

Since we mentioned manually creating threads, how do we set the parameters of the thread pool? Here we need to consider the appropriate number of threads. We provide a general suggestion:

  • The higher the proportion of average working time of threads, the fewer threads are needed.
  • The higher the proportion of average waiting time of threads, the more threads are needed.
  • Conduct corresponding stress tests for different programs to determine the most suitable number of threads. Finally, the tutorial explained how to shutdown a thread pool and discussed 5 methods related to shutting down a thread pool: shutdown(), isShutdown(), isTerminated(), awaitTermination(), and shutdownNow(). The focus was on the difference between shutdown() and shutdownNow(), where the former gracefully shuts down the thread pool while the latter immediately shuts it down. The tutorial then explained the principle of “thread reuse” in thread pools and analyzed the source code of the execute() method, which is a very important method in thread pools.

Various types of “locks” #

In Java, there are many types of locks, such as pessimistic locks and optimistic locks, shared locks and exclusive locks, fair locks and unfair locks, reentrant locks and non-reentrant locks, interruptible locks and non-interruptible locks, spin locks and non-spin locks, biased locks/lightweight locks/heavyweight locks, etc. Regarding pessimistic locks and optimistic locks, we analyzed their respective use cases. We also analyzed the principle of synchronized, a pessimistic lock, and discovered the “monitor” lock behind it. Then we compared synchronized with Lock and provided some recommendations for choosing between them:

If possible, it is best to neither use Lock nor synchronized, but instead prefer other mature tools in the java.util.concurrent (JUC) package, as they typically handle all locking and unlocking operations automatically for us. If locks must be used, prefer synchronized because it reduces the amount of code and the likelihood of errors. Using Lock requires writing an unlock statement in a finally block; otherwise, significant issues may arise. With synchronized, we don’t have to worry about these problems because it automatically unlocks. Of course, if synchronized cannot meet our needs, we have to consider using Lock.

The next topic is about locks. Lock has many powerful features, such as attempting to acquire a lock and acquiring with a timeout. We introduced several commonly used methods: lock(), tryLock(), tryLock(long time, TimeUnit unit), lockInterruptibly(), and unlock(). We explained their functions and then discussed fair locks and unfair locks. In a fair lock, locks are acquired in the order that threads requested them, while in an unfair lock, threads can jump the queue, which can improve overall efficiency in certain cases. Although unfair locking is the default, fair locks can also be used.

Next, we talked about read-write locks. ReadWriteLock is suitable for scenarios with more reads and fewer writes. Rational use of read-write locks can further improve concurrency efficiency. The rules are as follows: either one or more threads hold read locks at the same time or one thread holds a write lock. Read-write locks can be summarized as read-read sharing and mutual exclusion for everything else, including write-write, read-write, and write-read. We also explained the promotion and demotion of read-write locks and the queuing strategy.

As for spin locks, we first introduced what a spin lock is, then compared the lock acquisition process between spin locks and non-spin locks, and explained the benefits of spin locks. We also implemented a reentrant spin lock and analyzed its drawbacks and use cases.

In the section on locks, we also discussed the optimizations applied by the JVM to locks. This includes adaptive spin locks, lock elimination, lock coarsening, biased locking, lightweight locking, heavyweight locking, etc. With these optimizations, the performance of synchronized is not inferior to other locks. So, it is perfectly fine to use synchronized to meet business requirements in terms of performance.

A comprehensive view of concurrent containers #

Concurrent containers are a key topic. In the chapter on concurrent containers, we first explained why HashMap is not thread-safe and then compared the differences between ConcurrentHashMap in Java 7 and Java 8. This includes differences in data structure, concurrency level, the principle of ensuring concurrency safety, handling hash collisions, and the time complexity of queries. We also analyzed why ConcurrentHashMap converts to a red-black tree only when a bucket exceeds 8. This is a trade-off between time and space. We also compared ConcurrentHashMap and Hashtable. Although both are thread-safe, they differ in terms of versions, ways of ensuring thread safety, performance, and modifying during iteration.

We then introduced CopyOnWriteArrayList. It is suitable for scenarios where read operations need to be as fast as possible, and it doesn’t matter if write operations are slower. This is typically the case in scenarios with more reads and fewer writes. The read-write rule of CopyOnWriteArrayList is that reading does not require locking at all, and writing does not block read operations. In other words, reading can be done simultaneously with writing. Only synchronization is required between write operations. We also discussed its characteristics, such as enabling modification during iteration, and mentioned its three drawbacks: memory consumption, copying overhead in complex or large collections, and data consistency. Finally, we analyzed its source code.

Blocking Queue #

In the concurrent container, there is a focus on the blocking queue. Firstly, it introduces what a blocking queue is and analyzes the three groups of methods in the blocking queue. It also provides code examples. Then it introduces five common types of blocking queues and their characteristics, which are ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue, and DelayQueue.

After that, it compares the concurrent safety principles of blocking and non-blocking queues. The blocking queue mainly utilizes ReentrantLock and its Condition to implement it, while the non-blocking queue uses CAS to ensure thread safety.

Finally, it explains how to choose a suitable blocking queue based on functionality, capacity, expandability, memory structure, and performance.

Atomic Classes #

Atomic class is an important character in the JUC package. Firstly, it introduces six types of atomic classes, including primitive type atomic classes, array type atomic classes, reference type atomic classes, composite type atomic classes, Adder, and Accumulator.

Then it analyzes the performance issues of AtomicInteger in high-concurrency situations and how to solve them. The main reason for poor performance in high-concurrency scenarios is collision and contention. We can use LongAdder to solve this problem. The internal principle of LongAdder is also analyzed. Then it compares atomic classes with volatile. If there is only a visibility problem, volatile can be used to solve it. But if atomicity needs to be ensured, atomic classes or other tools should be used instead of volatile.

After that, it compares atomic classes with synchronized. They are similar in functionality, but they have differences in principles, applicable scope, granularity, and performance. Finally, it introduces Accumulator, which is a more general version of Adder introduced in Java 8.

ThreadLocal #

Firstly, it explains two scenarios that are suitable for ThreadLocal:

  • The first is to use it to save objects that are exclusively owned by each thread, such as a date utility class.
  • The second is to use ThreadLocal to save scenarios or context information for each thread, making it easier for subsequent methods to obtain this information and avoiding the need for parameter passing.

Of course, ThreadLocal is not used to solve the problem of multi-threaded access to shared resources because its design intent is that the resources are not shared, but each thread has a copy of the resources, and each copy is exclusively owned by the thread.

Then it analyzes the internal structure of ThreadLocal. It is necessary to understand the relationship between Thread, ThreadLocal, and ThreadLocalMap. It also introduces the use of the remove method after using ThreadLocal to prevent memory leaks.

Future #

Next is the content related to Future. Firstly, it compares Callable and Runnable. They are different in method names, return values, exception throwing, and their relationship with the Future class. Then it introduces the main function of the Future class, which is to put the computation process into a sub-thread, control the execution process through Future, and finally obtain the calculation result. In this way, the overall program efficiency can be improved, which is an asynchronous idea.

We also provide detailed explanations of the five methods of Future: get, get(long timeout, TimeUnit unit), isDone(), cancel(), and isCancelled(). When using Future, one should be careful. For example, when obtaining the results of Future in a for loop, it is easy to block. It is recommended to use timeout limits. In addition, the lifecycle of Future cannot be rolled back, and Future itself cannot generate new threads. It needs to rely on the Thread class or thread pool to execute tasks in sub-threads.

After that, a problem of a “travel platform” is introduced. It hopes to efficiently obtain ticket information from various airlines. We evolve the code from serial execution to parallel execution, then parallel execution with timeouts. Finally, we find that if the response speed of the airlines is fast enough, there is no need to wait until the timeout is reached. We iterate step by step and upgrade our code. This “travel platform” problem is a commonly encountered problem in everyday work because we often need to parallelly obtain and process data.

Thread Cooperation #

In the classes related to thread cooperation, we explain Semaphore, CountDownLatch, CyclicBarrier, and Condition.

In the course on Semaphore, it first introduces its usage scenarios, usage, and attention points, including keeping the number of acquired and released permits as consistent as possible, setting fairness during initialization, and Semaphore supporting both inter-thread and inter-thread pool scenarios. For CountDownLatch, when creating the class, we need to pass the “count down” number in the constructor. Then the waiting threads call the await method to wait. Every time another thread calls the countDown method, the count is reduced by 1. When it becomes 0, the previously waiting threads will continue running.

Next, CyclicBarrier is introduced. It is similar to CountDownLatch in usage, as it can also block one or a group of threads until a preset condition is met and then trigger them together. However, there are many differences between them, such as the objects they act on, their reusability, and their ability to perform actions.

Finally, the relationship between Condition and wait/notify/notifyAll is explained. If Lock is used to replace synchronized, then Condition is used to replace the corresponding wait/notify/notifyAll in Object. Therefore, their usage and nature are very similar.

Module 3: Understanding the Bottom Principles #

Java Memory Model #

Then we move on to our third module: Understanding the Bottom Principles. The first focus is the Java Memory Model. First, we explain why we need the Java Memory Model, and then we introduce what it is, focusing on reordering, atomicity, and visibility.

Next, we introduce the related content of reordering, which has the advantage of improving processing speed.

Then we introduce atomicity, including what it is, what atomic operations are in Java, the special nature of atomicity for long and double, and the fact that combining atomic operations together does not guarantee overall atomicity.

After that, we explain visibility. We need to understand the relationship between main memory and working memory, as well as the “happens-before” relationship: if the first operation “happens-before” the second operation (or can be described as the first operation and the second operation satisfying the “happens-before” relationship), then we can say that the first operation is definitely visible to the second operation, which means that when the second operation is executed, it can definitely see the result of the first operation. This relationship is very important and a key point in the content of visibility.

Finally, we introduce the two functions of volatile: ensuring visibility and to some extent prohibiting reordering. We also analyze why volatile is necessary in the double-checked locking pattern of the singleton pattern. The main reason is to ensure thread safety.

CAS Principle #

In the lessons related to CAS (Compare-And-Swap), we first introduce the core idea of CAS, which is to compare the value in memory with the specified data. Only when these two values are the same, will the value in memory be replaced with the new value. The whole process has atomicity.

Then we introduce the applications of CAS, including its applications in concurrent containers, databases, and atomic classes. We also explain the three drawbacks of CAS: the ABA problem, the problem of spinning for too long, and the problem that the scope of thread safety cannot be flexibly controlled.

Deadlock Problem #

In the lessons related to deadlocks, we first introduce what a deadlock is: two or more threads (or processes) are indefinitely blocked, waiting for each other’s resources. We provide an example of a deadlock and explain the four necessary conditions for a deadlock: mutual exclusion, hold and wait, no preemption, and circular wait. We also demonstrate how to locate a deadlock with command lines and code, and provide three strategies to solve deadlock problems: avoidance, detection and recovery, and ostrich algorithm. Finally, we analyze the classic dining philosophers problem.

Final Keyword and “Immutability” #

First, we introduce the different effects of the final keyword when applied to variables, methods, and classes. We also analyze why adding final does not guarantee “immutability”? The main reason is that the content of the object modified by final can still change. Then we analyze why String is designed to be immutable? The main advantages of this design include the ability to use string constant pool, use it as a key for HashMap, cache HashCode, and ensure thread safety.

AQS Framework #

Finally, we talk about AQS (AbstractQueuedSynchronizer). We explain why we need AQS and its internal principles. We also analyze the application of AQS in the CountDownLatch class by examining its source code.

Summary #

The above is the key content of this column, which also covers most of the key knowledge of Java concurrency programming. I am also very happy to study and discuss Java concurrency knowledge with you. While writing, it is inevitable that there will be missing knowledge points. You can leave a message or contact LaGou customer service staff to join the reader group for this course and discuss together.