[Java Concurrency in practice] Chapter II thread safety

Source: Internet
Author: User

Thread Safety

The core of writing thread-safe code is to manage state access operations, especially access to shared and mutable states.

The state of an object refers to the data stored in a state variable, such as an instance or a static domain. the state of an object may be included in the domain of other dependent objects. For example, the state of a hashmap is stored not only in the HashMap object itself, but also in the Map.entry object. The state of an object contains any data that might affect its externally visible behavior.

"Sharing" means that variables can be accessed concurrently by multiple threads, while "mutable" means that the value of a variable can change over its lifetime.

whether an object needs to be thread-safe depends on whether it is accessed by multiple threads. This refers to the way the object is accessed in the program, not the functionality that the object implements. to make an object thread-safe, you need to use a synchronization mechanism to coordinate access to the mutable state of the object.

When multiple threads access a state variable and one of the threads performs a write operation, the synchronization mechanism must be used to coordinate the access of the variables to those threads. The primary synchronization mechanism in Java is the keyword synchronized, which provides an exclusive locking method, but the term "synchronization" also includes variables of volatile type, explicit locks, and atomic variables.

If multiple threads are accessing the same mutable state variable without proper synchronization, the program will have an error. There are three ways to fix this problem:
This state variable is not shared between threads.
Modify the state variable to a variable that is immutable.
Use synchronization when accessing state variables.


When designing thread-safe classes, good object-oriented techniques, non-modifiable, and explicit immutability specifications can help.

First make the code run correctly, and then increase the speed of the code. Even so, it's best to only optimize when performance test results and application requirements tell you that you have to improve performance, and that the measurement results show that this optimization can actually lead to performance improvements in the real world.

2.1 What is thread safety

What is the meaning of "security"? In the definition of thread security, the core concept is correctness. Correctness means that the behavior of a class is exactly the same as its specification. In good specification, a variety of invariant conditions are often defined to constrain the state of an object, as well as to define a variety of posteriori conditions to describe the results of an object operation. Since we don't usually write detailed specifications for our classes, we don't know if these classes are correct. But we can use them after we're sure that "Class code works." With a clearer definition of "correctness", thread safety can be defined: When multiple threads access a class, the class always behaves correctly, then it is called a thread-safe class. You can think of a thread-safe class as a class that will not be compromised in both a concurrent environment and a single-threaded environment.

This class is thread-safe when multiple threads access a class, regardless of how the runtime environment is scheduled or how those threads will be executed alternately, and if no additional synchronization or collaboration is required in the keynote code, and the class behaves correctly.

If an object is implemented correctly, the invariant or posteriori conditions are not violated in any operation, including a common method of calling an object or a read/write operation on its common domain. Any serial or parallel execution on a thread-safe object instance does not leave the object in an invalid state.

The necessary synchronization mechanisms are encapsulated in the thread security class, so the client does not need to take further synchronization measures.

Stateless: It does not include any fields, nor any references to fields in other classes. The temporary state in the calculation is only available in local variables on the thread stack, and can only be accessed by the executing thread.

Stateless objects must be thread-safe.

2.2 atomicity

For example, although the increment operation ++count is a compact syntax that makes it look like an operation, the operation is not atomic, and it does not execute as an indivisible operation. In fact, it contains three separate operations, reads the value of count, adds a value of 1, and then writes the result to count. This is a "read-modify-write" sequence of operations, and its resulting state depends on the previous state.

2.2.1 Race condition

A race condition occurs when the correctness of one calculation depends on the timing of multiple threads alternating, in other words, the correct result depends on luck. The most common type of race condition is the "check after execution" operation, which determines the next action through a potentially defunct observation.

The failure of this observation is the essence of most race conditions--based on a possible failure of observation to make judgments or perform a calculation, a race condition of this type is called "Check it Out first": first to see if a condition is true (for example, file x does not exist), Then take the corresponding action (create file X) based on this observation, but in fact, as you observe this result and begin to create the file, the observation result may become invalid (another thread creates file X during this time), resulting in various problems (unexpected exception, data overwritten, file damage, etc.).

2.2.2 Example: Race condition in deferred initialization
@NotThreadSafepublicclass LazyInitRace{    privatenull;    publicgetInstance()    {        ifnull)            new ExpensiveObject();        return instance;    }}

Race conditions do not always produce errors, and an inappropriate execution timing is required. However, race conditions can also lead to serious errors. Assuming that Lazyinitrace is used to initialize the application-scoped registry, if a different instance is returned in multiple calls, either some of the registration information is lost, or multiple behaviors exhibit inconsistent views of the same set of registered objects.

2.2.3 Compound operation

To avoid the problem of race conditions, you must somehow prevent other threads from using this variable when a thread modifies the variable, ensuring that other threads can read and modify the state only before or after the modification operation is complete, rather than in the process of modifying the state.

Assuming that there are two operations A and B, if from the thread that executes a, when another thread executes B, either all of B is executed, or no B is executed at all, then A and B are atomic to each other. Atomic manipulation means that this operation is an atomic operation for all operations that access the same state, including the operation itself.

To ensure thread safety, operations such as "Check after execution" (such as lazy initialization) and "read-Modify-write" (for example, increment operations) must be atomic. We will collectively refer to "Check after execution" and "read-Modify-write" as a composite operation: contains a set of operations that must be performed atomically to ensure thread safety.

When a state is added to a stateless class, the class is still thread-safe if the state is managed entirely by a thread-safe object.

In practice, you should use existing thread-safe objects (such as Acomiclong) to manage the state of your classes whenever possible. It is easier to determine the possible state of a thread-safe object and its state transitions than non-thread-safe objects, making it easier to maintain and validate thread security.

2.3 Plus lock mechanism

The definition of thread security requires that the invariant conditions are not compromised regardless of the execution timing or alternation of operations between multiple threads.

When multiple variables are involved in an invariant condition, each variable is not independent of each other, but the value of one variable constrains the values of other variables. Therefore, when you update a variable, you need to update the other variables simultaneously in the same atomic operation.

Because there is no guarantee that you will get two values at the same time: Thread A gets these two values in the process, threads B may have modified them so that thread A will also find that the immutability condition has been compromised.

To maintain state consistency, you need to update all related state variables in a single atomic operation.

2.3.1 Built-in lock

Java provides a built-in locking mechanism to support atomicity: Synchronizing blocks of code, which consist of two parts: an object reference as a lock, and a block of code that is protected by this lock. The method that is decorated with the keyword synchronized is a synchronous block of code that spans the entire method body, where the lock of the synchronization block is the object that the method call resides on. The static synchronized method takes a class object as a lock.

synchronized(lock){    //访问或修改由锁保护的共享状态}

Each Java object can be used as a lock that implements synchronization, which is called a built-in lock or a monitor lock. The thread automatically obtains the lock before it enters the synchronization code block, and automatically releases the lock when exiting the synchronization code block, regardless of whether it exits through a normal control path or an exception thrown through a block of code. The only way to get a built-in lock is a synchronous code block or method that is protected by this lock.

Java's built-in lock is equivalent to a mutex (or mutex), which means that only one thread can hold this lock at most. When thread a attempts to acquire a lock held by thread B, thread A must wait or block until thread B releases the lock. If B never releases the lock, then a will wait forever.

2.3.2 Re-entry

Because the built-in lock is reentrant, if the thread view obtains a lock already held by itself, the request succeeds. "Re-entry" means that the granularity of the operation that acquires the lock is "thread", rather than a "call" to re-enter the implementation by associating a fetch count value and an owner thread for each lock. With a count of 0 o'clock, this lock is considered to be not held by any thread. When a thread requests a lock that is not held, the JVM notes the holder of the lock, and the Get count value is set to 1. If the same thread acquires the lock again, the count value is incremented, and when the thread exits the synchronization code block, the counted values are decremented accordingly. When the count value is 0 o'clock, the lock is released.

2.4 Using locks to protect status

Because locks enable their protected code paths to be accessed serially, some protocols can be constructed through locks to achieve exclusive access to shared state. As long as these protocols are always followed, you can ensure the consistency of the state.

A composite operation that accesses a shared state, such as a hit counter's increment operation (read-modify-write) or deferred initialization (which is performed after checking), must be atomic to avoid a race condition. If a lock is held during the execution of a composite operation, the composite operation becomes atomic. However, it is not enough to encapsulate a composite operation in a synchronous block of code. If you use synchronization to coordinate access to a variable, you need to use synchronization in all locations where the variable is accessed. Also, when a lock is used to coordinate access to a variable, the same lock is used in all locations where the variable is accessed.

For mutable state variables that may be accessed concurrently by multiple threads, the same lock is required to access it, in which case we call the state variable to be protected by this lock.

When acquiring a lock associated with an object, it is not possible to prevent other threads from accessing the object, and a thread can only block other threads from acquiring the same lock after acquiring the lock on the object. Each object has a built-in lock, just to avoid explicitly creating a lock object. You need to construct your own lock protocol or synchronization policy to secure access to shared state and use them from start to finish throughout your program.

Each shared and mutable variable should be protected by only one lock, so that the maintainer knows which lock it is.

A common locking convention is to encapsulate all mutable states inside an object and synchronize all paths that access the mutable state through the object's built-in lock, so that no concurrent access occurs on the object. This pattern is used in many thread-safe classes, such as vectors and other synchronous collection classes. In this case, all variables in the state of the object are protected with an object's built-in lock. However, there is nothing special about this pattern, and neither the compiler nor the runtime enforces this pattern, and if you forget to use it when adding new methods or code paths, the yoke protocol can be easily destroyed.

Not all data requires lock protection, and only variable data that is accessed concurrently by multiple threads needs to be protected by a lock.

When a variable is protected by a lock, it means that the lock needs to be acquired each time the variable is accessed, so that only one thread can access the variable at the same time. When the invariant condition of a class involves multiple state variables, there is another requirement: each variable in the immutable condition must be protected by the same lock. These variables can therefore be accessed or updated in a single atomic operation, ensuring that the invariant condition is not compromised.

For each invariant condition that contains multiple variables, all of the variables involved need to be protected by the same lock.

Although the Sychronized method ensures the atomicity of a single operation, additional locking mechanisms are required if multiple operations are to be combined into one conforming operation. (see section 4.4 for ways to increase atomic operations on thread-safe objects), and having each method as a synchronous method can also lead to activity problems or performance issues.

2.5 Activity and sex

Bad concurrent applications: which quantities can be called simultaneously, not only by the available processing resources, but also by the structure of the application itself.

By narrowing the scope of the synchronization code block, make sure that the synchronization code block is not too small, and do not split the actions of the current atoms into multiple synchronized code blocks. Operations that do not affect shared state and take longer to execute should be detached from the synchronization code block as much as possible so that other threads can access the shared state during the execution of these operations.

To determine the reasonable size of a synchronized block of code requires a tradeoff between various design requirements, including security, which needs to be met, simplicity, and performance.

There is often a mutual constraint between simplicity and performance. When implementing a synchronization strategy, be sure not to blindly sacrifice simplicity for performance (which could compromise security).

When using a lock, you should be aware of the functionality implemented in the code block and whether it will take a long time to execute the code block. Whether you are performing a computationally intensive operation or performing a potentially blocking operation, if you hold the lock for too long, you will have an active or performance problem.

Do not hold a lock when the execution takes longer to calculate or when an operation that may not be completed quickly, for example, network I/O or console I/Os.

Copyright NOTICE: This article for Bo Master original article, without Bo Master permission not reproduced.

[Java Concurrency in practice] Chapter II thread safety

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.