Finally, I have finished reading this classic book on Concurrency in Java. Although Thinking in Java and Effective Java both have chapters on concurrency, this book goes deeper. Concurrency is a topic that Java programmers cannot leave behind, so it is extremely helpful to have a look at this book. Of course, this book is a long time old, and some things in it may be outdated, such as GUI programming. So I’ve selectively skipped the chapters that I don’t think are useful. What’s more, I have skipped the content in TIJ and EJ. Those of you who have not read the previous two books can look at the chapters I have skipped. Finally, there are a few actual combat content, I feel that my current level is not so high, it may not feel deep to put it aside, such as work after a period of time in the back to see, deepen understanding.
Thread safety
1. What is thread safety
- 1. A class can be called thread-safe if it consistently behaves correctly as expected when accessed by multiple threads.
- 2. The necessary synchronization mechanism is encapsulated in the thread-safe class, and the client does not need to take further synchronization measures when calling
- 3. A class must be thread-safe when it contains neither a domain nor references to a domain in any other class can be called stateless.
2. The atomicity
When an object is being used by multiple threads and we count how many times the object is called with an int, the object is not thread-safe. Because incrementing an int is not atomic, it is possible that an int is incremented by half in one thread and the object is switched to another thread, and the object behaves differently from what we expect.
- 1. Race conditions: When the correctness of an operation depends on the timing of multiple threads executing alternately, this is called a race condition. As in our example above, the correctness of incrementing an int depends on whether one thread operates halfway through another. A common race condition is check before execution, such as an operation in one thread being inserted between a check and execution operation in another thread.
- 2. Race conditions in delayed initialization:
- 1. Delayed initialization is a race condition that is checked before execution. For example, singletons are initialized when they are used in multithreading.
- 2. The previous example of increasing int is another kind of race condition: read-modify-write. Whenever a segment is scheduled to another thread, the class behaves differently than expected.
- 3. Compound operations: The race conditions we mentioned above are simply a series of related operations ———— compound operations, once this series of operations are interrupted, the thread is unsafe. So we need to change this set of operations to atomic operations via Java mechanisms.
3. Locking mechanism
We all know that Java has atomic variables, so for a class, we just need to make all fields atomic variables and that class is thread-safe? Obviously not. We mentioned race conditions in 2. When a series of operations on atomic variables are related to each other, that series of operations is a race condition, and if we don’t do that then the class is not thread-safe
- 1. Built-in lock:
- 1. Each object and Class object has a built-in lock that only one thread can hold.
- 2. The object lock is used for non-static methods. The Class object lock is used for static methods.
- 3. When thread 1 enters synchronized block 1, thread 1 holds the lock imported from synchronized block 1. When thread 1 runs from synchronized block 1, the lock will be released. It stops and waits for thread 1 to run from synchronized block 1.
- 4. The entire synchronized block is, in essence, an atomic operation, so race conditions can be put into a synchronized block so that this set of operations becomes thread-safe.
- 2. The reentrant:What happens when a thread attempts to enter the synchronized block of the same lock multiple times?
- 1. If there is no reentrant, the thread will wait for the synchronized block to complete, and a deadlock will occur. (That is, the thread can never exit the synchronized block, and other threads attempting to acquire the lock will block and crash.)
- 2. Reentrant means that once a thread has acquired a lock, it is possible to acquire it again without releasing the lock. When the lock is acquired by no thread, the internal count is 0; when a thread has acquired the lock, the count is 1; if the thread repeatedly acquires the lock, the count is increased by one; when the thread runs out of a synchronized block, the count is decreased by one.
Object sharing
Synchronization not only enables atomic operations, but also an important aspect of synchronization is memory visibility. Memory visibility means that when a shared variable is modified, other threads can immediately observe the changed value of the variable. You can’t do that if you’re not in sync.
1. The visibility
- 1. The first thing we need to know is that Java threads have their own independent caches, and the interaction between threads to share variables is realized through their own interaction with caches and main memory.
- 2. It would be uneconomical if the main memory was flushed every time a thread’s cache was changed, and every time main memory was changed by a thread’s cache, all threads were told to refresh their cache.
- 3. Due to 1 and 2, it occurs that when thread 1 changes a shared variable, thread 2 gets the same value as before. That is, thread 1 changes a shared variable and does not flush main memory, or thread 2 does not fetch a new shared variable from main memory, or both
- 4. To address memory visibility, we can use volatile and synchronization
- 5. Non-atomic 64-bit operations: While Java requires that variable reads and writes be atomic, 64-bit longs and doubles are divided into two 32-bit access operations. The volatile keyword is used.
2. Release and escape
- 1. Publish: Make an object accessible to objects outside of scope, such as making a reference to the object static or returning it in a non-private method
- 2. Escape: Since publishing an object’s internal state can break encapsulation and immutability and cause thread insecurity, it is called object escape in this case
- 3. When publishing an object, it may indirectly publish objects that it does not want to publish, such as a private array. Once published, the objects stored in it will also be published
- 4. If an inner class uses a method from the outer class, the outer class will also be published. This is called the This escape. For example, if you run a thread in a constructor and the class is visible to the thread before the class is constructed, you will have a problem with publishing objects before they are constructed. This problem can cause thread insecurity.
3. Thread closure
- 1. Thread closure means that an object is enclosed in a thread so that no synchronization of the object is required.
- 2. In Android, all view updates are performed in the main thread, so we can say that view objects are thread-closed.
- Ad-hoc thread closure: There is no guarantee of language features, only the programmer himself. In the case of volatile, writing to a volatile thread is guaranteed to notify other threads as long as no other thread writes to it
- 4. Stack closed: Local variables are stack closed as long as they are not published to other threads. ,
- ThreadLocal: Each Thread has a different version of a variable, with an internal implementation like Map
. ,t>
4. The invariance
- 1. Immutable objects must be thread-safe
- 2. Immutable objects meet the following conditions:
- 1. All fields are final, as are the fields within the domain
- 2. All domains cannot be changed
- 3. This does not escape during construction
5. Secure release
- 1. It is not safe to publish an object reference if it is only stored in the public domain because the object may not be built in another thread due to visibility issues
- 2. The correct object is broken: when an object is published like 1, thread 1 changes the state of the object in the middle of using it, causing thread 1 to throw an exception.
- 3. Immutable objects and initialization security: If the objects in 1 and 2 are immutable, then the case of 2 will not occur.
- 4. Safely publish mutable objects: To safely publish a mutable object, you need to make the reference and state of the object visible to all other threads in the following ways
- 1. Initialize object references statically, because the JVM’s classloading process is synchronous
- 2. Use volatile or AtomicReference for object references
- 3. Place object references into final fields
- 4. Lock object references
- 5. Share objects safely: When publishing an object, specify the multi-thread sharing rules for the object:
- 1. Thread closure? : Can only be owned by one thread
- 2. Is the share read-only? : Can be read concurrently
- 3. Is thread-safe sharing? : Class internal synchronization, can be used at will
- 4. Are they protected objects? : There is no synchronization inside the class, requiring the consumer to synchronize externally
Object composition ###1. Design thread-safe classes
- 1. How do I know if a class is thread-safe?
- 1. Find all the variables that make up the state of the object: all the fields of the object that will change
- 2. Find the invariance condition that constrains the state variables of the object: that is, the area where all state variables change
- 3. Establish concurrent access management policies for object states: that is, establish synchronization policies for all state variables
- 2. Collect synchronization requirements, such as the scope of variables, whether the current state of variables is related to the previous state, etc
2. Instance closure
- 1. When you know all the call paths of a non-thread-safe object, you can wrap it in a thread-safe class
- 2. Java monitor pattern: 1 This is the pattern that encapsulates all mutable objects and protects them with their own locks. This is how HashTable is implemented, but this is simple, coarse-grained encapsulation, but fine-grained encapsulation is required if performance is to be provided. In addition to the built-in lock, you can also use a private object lock, which prevents the client from acquiring the lock that protects the mutable object, but allows the client to use it through a public method.
3. Thread-safe delegates
- 1. You can delegate thread-safe properties to thread-safe classes, such as ConcurrentHashMap, through a delegate mechanism
- 2. When delegates fail: If multiple thread-safe objects in a class have composite immutability conditions, synchronization must still be performed in the class
Basic building blocks
1. Synchronize container classes
- Vector and HashTable were early synchronization classes.
- 1. Problems with synchronous container classes: some conformance operations such as iteration, jump (find the next element of the current element), and conditional operations (add if none) can cause problems when other elements are modified concurrently.
- 1. If the multithread getLast() and deleteLast() in the Vector are not synchronized, deleteLast() may be inserted in the middle of getLast(), resulting in an array boundary exception
- 2. To solve the problem of 1, you can add locks to both operations
- 3. The same problem occurs when iterating over the Vector. It also needs to be locked
- 2. The iterator and ConcurrentModificationException: Due to some concurrent container didn’t modify containers during the iteration of vision, so they are adopting the tactics of “instant” failure, namely the iteration if the container is changed, then throw ConcurrentModificationException.
- 1. Lock the entire iteration so that no exceptions are thrown during the iteration
- 2. If locked during iteration, performance problems will occur once the container size becomes large.
- 3. If you do not want to lock the container during iteration, you can clone the container and seal the cloned container locally. In this way, the cloned container will not have problems.
- 3. Hide iterators: In order to throw an exception during iteration, we choose to lock all iterations, but in some cases we do not iterate, and the Java class library implementation iterates over the container. Such as the container’s toString() method, which still throws an exception.
2. Concurrent containers
- Java1.5 provides concurrent containers instead of synchronous containers, which improves performance and provides common synchronous compound operations that avoid the situation in vectors
- 1.ConcurrentHashMap: it does not use the locking method of HashTable. It uses a segment-based lock. This concurrency is generally used and only abandoned when the Map is locked exclusively.
- 2. Additional atomic Map operations: Since ConcurrentHashMap cannot be locked exclusively by the client, the client cannot create new atomic operations, but some common compound operations are already implemented in ConcurrentMap
- 3.CopyOnWriteArrayList
3. Blocking queues and producer-consumer patterns
- Use of various BlockingQueues
4. Blocking method and interrupt method
- 1. Threads are blocked and suspended while I/O, wait lock, sleep, and wait are performed.
- 2. If a method throws an InterruptedException indicating that the method is blocking, the method is terminated as soon as possible if it is interrupted
- 3.Thread provides an interrupt method to check whether a Thread is interrupted.
- 4. When throwing an interrupt exception in a thread, there are two options:
- 1. Throw an exception upwards
- 2. If you are in a Runnable and can no longer throw an exception, you need to catch the exception and either stop the thread or restore the interrupt with the interrupt method
5. Synchronize tools
- 1. Locking: Until the locking state ends, no other thread can operate until the locking state ends.
- 1. For example, after a thread completes, other dependent threads run
- 2.CountDownLatch: Makes multiple threads wait for a set of events with a counter indicating how many events are left. The thread calling await blocks until the counter is 0
- 2.FutureTask: The thread calling GET blocks the result of the Callable.
- 3. Semaphore:
- 4. CyclicBarrier
6. Build an efficient and scalable result cache
- Actual combat content, dabbling in the project, see again later
Task execution
1. Execute tasks in the thread
- 1. Serial execution of tasks wastes CPU
- 2. Creating threads for each task is a waste of resources
2. The Executor framework
- 1.Executor is an interface
- 2. Based on the producer-consumer mode, the submitted task is the producer, and the thread of executing the task is the consumer
- 3. Thread pools: Executors provide a series of thread pools;
- 4.Executor lifecycle: The JVM only exits after all non-daemon threads exit, so stopping Executors is a problem
- Executor inherits the ExecutorService, which has several ways of managing the lifecycle
- There are three ExecutorService states: running, down, and Terminated. Because tasks in Executor are executed asynchronously, at some point some tasks may be placed in the task queue and not executed, while others may be executing.
- 3. After the shutdwon method is called, Executor is closed to accept new tasks, but the Executor state will not become terminated until previous tasks are complete
- 4. After the shutdwonNow method is called, it will directly become the terminated state, and no matter the running or not running tasks will be canceled
Cancel and close
1. The task is canceled
- 1. If it is a cyclic task, add a flag to the condition. If flag is no, exit the loop
- 2. In some cases, if a blocking method is called in a loop task, it may take some time to exit, or even never exit
- 3. In case of 2, we can use interrupts to terminate the thread
- Thread 1 calls thread 2’s interrupt representation: Thread 1 wants thread 2 to stop working if appropriate (note that thread 2 does not stop immediately, i.e., non-preemptively)
- 2. Methods in the blocking library, such as sleep and WAIT, check whether the thread is interrupted before calling. If interrupted, the thread clears the interruption and throws InterruptedException.
- 3. If the thread is in a non-interrupt state, such as a while loop, then we can determine whether the interrupt occurs in the while condition, if so, we will exit the loop
- 4. An interrupt can be used to cancel an interrupt state, or if you want to continue with another blocking library operation after catching an exception
- 5.Future can be cancelled with cancel
- Synchronous IO, Socket IO, and asynchronous IO are blocking methods that do not throw InterruptedException when the thread interrupts. However, you can use these methods to achieve the same effect by making them throw exceptions, such as closing the socket, closing the stream, etc
- 7. When a thread is acquiring a lock and the task cannot be cancelled using any of the above methods, use Lock#lockInterruptibly
- 8. We can encapsulate the methods in 6 as non-standard ways to cancel tasks.
2. Stop the thread-based service
- 1. Whenever a thread exists longer than it was created, it must be provided with a life-cycle method, such as stopping the thread
- 2. One way to turn off the producer-consumer is by “poison pill” objects, meaning that once a consumer gets a particular object, processing can be stopped and the consumer thread can be closed
- 3. Limitations of shutdownNow: When forcibly shutting down ExecutorService, there is no way to know which tasks are running and which are not. We can implement AbstractExecutorService and delegate to an ExecutorService, but in the implementation record tasks that were not performed at shutdownNow.
3. Handle abnormal thread terminations
- 1. A thread that throws an exception is terminated if it does not handle it
- 2. One solution to 1 is to catch an exception at the very outside of the entire thread and then handle the exception. It’s a little bit better than just ending the program, but there’s a security issue because if you throw an exception, the whole program might be affected.
- 3. We can take the initiative to detect abnormal: by implementing Thread. UncaughtExceptionHandler interface, to write an uncaught exception into the abnormal log in, or for other restorative operation
- 4. Perform the task described in 3 only when you submit the task through execute. If submit is used, the exception is considered part of the return, such as executing a Future with Submit
4. The JVM to shut down
- 1. Close the hook: a set of cleanup threads registered with Runtime.addShutdownHook will be called to clean up the resource.
- 1. If there are threads running, all threads will execute concurrently.
- 2. When all hook threads have finished executing, the JVM runs the finalizer.
- 3. If the finalizer or hook thread does not finish executing, the closing process will be suspended and the JVM will need to be forcibly shut down
- 4. When the JVM is forcibly shut down, the application thread is forcibly terminated, but the hook thread is not closed
- 2. Daemon threads: These threads do not affect JVM shutdown
Avoid active hazards
1. The deadlock
- 1. Dining for philosophers:
- 1. Mutually exclusive condition: A resource can be used by only one process at a time.
- 2. Request and hold conditions: when a process is blocked by requesting resources, it holds on to acquired resources.
- 3. Non-deprivation condition: the process can not forcibly deprive the resources it has acquired before they are used up.
- 4. Circular waiting condition: a circular waiting resource relationship is formed between several processes.
Deadlock avoidance and diagnosis
- 1. Timing Lock: Use a Lock instead of a built-in Lock to specify a Lock waiting period
- 2. Analyze deadlocks using thread dumps
3. Other active hazards
- 1. Hunger: a thread cannot access the resource it needs for a long time. For example, if the thread priority is not set correctly, a thread cannot obtain the CPU time slice for a long time
- 2. Live locks
Performance and scalability
1. Overhead introduced by threads
- 1. Context switching: Threads that are blocked will be suspended by the JVM. If blocked frequently, the time slice cannot be fully scheduled, increasing the time for context switching
- 2. Memory synchronization: Synchronized or volatile flusher the local cache of all threads, which consumes time
- 3. Blocking: A thread that is blocked will be suspended, so there will be two more time for context switching, so less blocking
2. Reduce lock contention
- There are three ways to reduce lock competition: 1. Reduce lock holding time 2. Use a coordinated exclusive lock
- 1. Narrow the lock scope: minimize the number of code in critical sections, especially operations such as I/O operations that block threads
- 2. Reduce lock granularity: Reduce the frequency with which threads request the same lock, that is, split the lock into multiple locks
- 3. Lock fragmentation: If you split a competitive lock into multiple locks, it may still be competitive. Splitting the locks on a group of independent objects is called lock segmentation. For example, if a Map bucket is protected by 16 locks and one lock is protected by N/16 buckets, the concurrent write performance is improved by 16 times. The downside is that exclusive access requires multiple locks, which is more difficult and expensive
- 4. Avoid hotspots?
- 5. Some alternatives to exclusive locks: concurrent containers, read-write locks, immutable objects, and atomic variables
The explicit lock
1. The Lock and already
- 1.Lock provides polling, timing, and interrupt Lock acquisition functions. Otherwise, it is similar to the built-in Lock
- 2. Polling locks and timing locks: You can use tryLock to avoid polling or timing locks to avoid deadlocks
2. The fairness
- 1. When creating a ReentrantLock, you can set the fairness of the lock. By default, the lock is unfair
- 1. Fair lock: Obtain the lock according to the order in which the threads queue
- 2. Unfair lock: a thread that wants to acquire a lock but has not yet been placed in the lock queue can jump the queue if the lock is available
- 2. Choice between built-in Lock and Lock: The built-in Lock is simple, and Lock can be used as an advanced tool
3. Read and write locks
- 1. Do not lock read data. It is suitable for mass read operations
Atomic variables and non-blocking synchronization mechanism
1. Disadvantages of locks
- 1. The overhead of suspending and waking up a thread due to a lock is high.
- 2. Volatile is a lightweight synchronization mechanism, but synchronization errors can occur if you rely on old values
Hardware support for concurrency
- 1. An exclusive lock is a pessimistic lock, and you can take an optimistic approach to fine-grained operations: check for interference from other threads during the update process and fail if there is, rather than reject the operation.
- 2. Comparison switching: CAS instruction can detect interference from other threads, so that atomic read-change-write operation can be realized without locking
- Java1.5 implemented CAS operations at the bottom, and some atomic variables use this mechanism
3. Atomic variable classes
- 1. Atomic variables are a better volatile
- 2. Atomic variables perform better than locks
4. Non-blocking algorithms
- Actual combat, see later
What is the memory model
- 1. Each thread has its own local cache, which has a copy of each shared variable. The caches of all threads communicate with the main village in both directions, but not in real time.
- 2. In order to allow more instructions to be concurrent, instructions will be rearranged during bytecode compilation and bytecode instruction transfer, that is, the sequence of the code that does not have a certain order is not necessarily the order of the final execution.
- 3. There’s a rule for code sequencing: happens-before
- 1. Program sequence rule: A precedes B in the program and A precedes B in the thread
- 2. Monitor lock rule: The monitor lock must be unlocked before the same monitor lock is locked
- 3. Volatile rule: Writes to volatile variables must precede reads to them
- 4. Thread start rule: Thread#start() must precede any operation in the thread
- 5. Thread termination rule: All operations in a thread must be executed before other threads detect that the thread is terminated
- 6. Finalizer rule: The object constructor must be completed before starting the finalizer for the object
- 7. Interrupt rule: Thread 1 calls interrupt from thread 2 and must do so before the interrupt thread detects interrupt
- 8. Transitivity: A comes before B, B comes before C, so A comes before C
No angst peddling, no clickbait. Share some interesting things about the world. Topics include but are not limited to: science fiction, science, technology, the Internet, programmers, computer programming. The following is my wechat public number: Interesting things in the world, dry goods waiting for you to see.