directory
• Write it first
• atomic
Locking mechanism
• Write it first
In other words, the minimum unit of execution in a process is a thread, and there is at least one thread in a process. When it comes to multithreading, there are two concepts, serial and parallel, that we can understand better. So-called serial is relative to A single thread to perform multiple tasks, and we had to download files, for example, we download multiple files, it is in the serial according to certain order to download, that is to say, after must wait for download A, can begin downloading B, they cannot overlap in time.
At the heart of writing thread-safe code is managing state access operations, especially access to shared and mutable states. If the object is thread-safe, synchronization mechanisms need to be used to coordinate access to the mutable state of the object. Failure to do so can result in data corruption and other unintended consequences. The Java synchronization mechanism is the keyword synchronized, which provides exclusive locking, including volatile variables, explicit locking, and atomic variables.
Single-threading is roughly defined as “what you see is what you know,” so by defining thread-safety, a class is said to be thread-safe if it always behaves correctly when accessed by multiple threads. If a class can’t run correctly in a single thread, it certainly won’t be thread-safe. If an object is implemented correctly, invariance or a posteriori condition is not violated in any operation, including calls to the object’s public methods or reads or writes to its public domain. Any serial or parallel operations performed on an object instance of a thread-safe class are not in an invalid state.
Stateless objects are always safe. What is stateless? This class contains neither any fields nor any references to fields in other classes. For example, most servlets are stateless, greatly reducing the complexity of implementing Servlet thread-safety, which becomes an issue only when servlets need to hold some information while processing requests.
• atomic
So atomicity is an indivisible thing. How do we understand that? For example, what happens when we add a state to a stateless object? Instead of counting how many times a class is called (it could be a Servlet), we define a private field of type Long (named count) and increment this value without calling it once in the method (that is, count++) as follows.
Integer count = 0;
public void getCount(a) {
count ++;
System.out.println(count);
}
Copy the code
Since we are not doing any synchronization on this class, it is not thread-safe, and while it works in a single-threaded environment, it can be problematic in multi-threaded situations. We notice that count++, which looks like an operation, but it’s not atomic, because it doesn’t perform as an indivisible operation, actually has three separate operations, which read the value of count, increment it by one, and then compute the structure and write it to count, This is a read-modify-write sequence of operations, and the result depends on the previous state. I ran the above code through multiple threads, and the result is as follows
And you can see that there are two 26s here, so why does that happen, and it’s clear that our method is not thread-safe at all, and there are A number of reasons why that might happen, and let’s say the most common one is that thread A, when we enter the method, gets the value of count, Thread B also comes in, so thread A and thread B get the same count as each other.
In concurrent programming, this kind of incorrect result due to improper execution timing is a very important condition, which has a formal name, “race condition.” What are race conditions? Race conditions occur when the correctness of a calculation depends on the alternate execution timing of multiple threads. The most common type of race condition is the “check before you execute” operation, where a potentially invalid observation is used to determine the next action. Such as first observed that a condition is true (file X does not exist, for example), then use the corresponding action according to the result of this observation (create file X), but in fact, when you observed between the results and begin to create documents, observation may become invalid (another thread is in the file created during the X), This can lead to problems (unexpected exceptions, data overwrites, corrupted files, and so on).
As with most concurrent errors, race conditions do not always produce errors and require some kind of improper execution timing. An atomic operation is an operation performed atomically for all operations that access the same state, including the operation itself. The operations like count++ we mentioned earlier are called compound operations, which consist of a set of operations that must be performed atomically to ensure thread-safety. In practice, existing thread-safe objects should be used as much as possible to manage the state of the class. It is easier to determine the possible state of thread-safe objects and their transitions than non-thread-safe objects, and thus it is easier to maintain and verify thread-safety. Here it is worth mentioning that in Java to we done a lot of atomic operation type, under this package Java. Util. Concurrent. The atomic.
Locking mechanism
We said the above, we can undertake atomic operations on a single class, so that we can guarantee the security of our program, however, we think, if when multiple atomic operations simultaneously, and each are interdependent relationship between atomic operation, in this case, how do we ensure correct program is run (thread safe)? If we still use atomic operations, we need to update all relevant state variables in a single atomic operation to maintain state consistency.
Java provides a built-in locking mechanism to support atomicity, called synchronized blocks. Synchronized blocks consist of two parts: an object reference that acts as a lock and a block of code that is protected by the lock. A method modified with the keyword synchronized is a synchronized code block that spans the entire method body, where the lock of the synchronized code block is the object on which the method call resides. Each Java object can be used as a lock to implement synchronization, and these are called built-in or monitor locks. The thread automatically acquires the lock before entering the synchronized block and releases it when exiting the synchronized block. The only way to acquire a built-in lock is to enter the synchronized block or method protected by the lock. It is worth noting that a built-in lock is a mutex, meaning that at most one thread can hold the lock. Note that any thread executing a synchronized block cannot see that another thread is executing a synchronized block protected by the same lock (visibility is an issue here). Synchronized directly added to the method, although can be very convenient to solve the problem of thread safety, but will also bring the problem of low performance, so how to use synchronized, is also worth learning.
Reentrant functions can be used concurrently by more than one task without worrying about data errors. In contrast, non-reentrant functions cannot be shared by more than one task unless they are mutually exclusive (either by using semaphores, or by disabling interrupts in critical parts of the code). A reentrant function can be interrupted at any time and resumed later without loss of data. Reentrant functions either use local variables or protect their own data when using global variables.
When a thread requests a lock held by another thread, the requesting thread is blocked. However, since the built-in lock is reentrant, if a thread attempts to acquire a lock already held by itself, the request succeeds. “Reentrant” means that the granularity of the operation to acquire the lock is thread, not call. One way to implement reentrant is to associate a fetch count with an owner thread for each lock, and when the count is zero, the lock is considered not held by any thread. When a thread requests an unheld lock, the JVM takes note of the holder and sets the lock count to 1. If the same thread requests the lock again, the count increases, and when the thread exits the synchronized block, the counter is decremented accordingly. When the count reaches 0, the lock is released.
At this point we could encapsulate multiple compound operations in a synchronized code block, but this is not enough. If a synchronized code block is used to coordinate access to a variable, synchronization needs to be used at all locations where the variable is accessed. Also, when a lock is used to coordinate access to a variable, the same lock is used at all locations where the variable is accessed. Also, synchronization is not only necessary when writing to shared variables. Mutable state variables that may be accessed by multiple threads at the same time need the same lock to access them. In this case, we are protected by the lock when we create state variables.
It is worth mentioning here that when using locks, you should be aware of what is implemented in the code block and whether it takes a long time to execute the code block. Whether performing computationally intensive operations or performing an operation that might block, holding a lock for too long can cause activity or performance problems. Do not hold locks when performing long computations or operations that may not be completed quickly, such as network IO or console IO.