Probe into the volatile variable in Java multithreading

  • 2020-05-05 11:16:59
  • OfStack

The volatile variable provides thread visibility and does not guarantee thread safety and atomicity.

What is thread visibility:

Locks provide two main features: mutual exclusion (mutual exclusion) and visibility (visibility). Mutual exclusion allows only one thread to hold a particular lock at a time, so this feature can be used to implement a coordinated access protocol to Shared data so that only one thread can use the Shared data at a time. Visibility is more complicated, it must ensure that the lock is released before making changes to Shared data for later another thread to acquire the lock is visible - if there is no synchronization mechanism provides the visibility that thread to see the value of Shared variables can be modified before or inconsistent values, it will cause many serious problems.

See volatile's semantics :

volatile is equivalent to a weak implementation of synchronized, meaning that volatile implements semantics similar to synchronized without locking. It ensures that updates to the volatile field are communicated to other threads in a predictable manner.

volatile contains the following semantics:

(1) the Java storage model does not reorder the operations of valatile instructions: this ensures that the operations on volatile variables are performed in the order in which the instructions appear.

(2) the volatile variable is not cached in the register (visible only to threads) or other places invisible to CPU, and the volatile variable is always read from main memory each time. That is, for changes to the volatile variable, other threads are always visible and do not use variables inside their own thread stack. That is, in the happens-before rule, after a write to an valatile variable, any subsequent reads understand the result of the write.

Although the volatile variable has good features, volatile does not guarantee thread safety, that is, the operation of volatile field is not atomic, volatile variable only guarantees visibility (after one thread modifies, other threads can understand and see the result of this change), to guarantee atomicity, so far only locks!

principle for using Volatile:

Three principles for applying the volatile variable:

(1) write a variable that does not depend on the value of the variable, or only one thread modifies the variable

(2) the state of the variable does not need to participate in the invariant constraint

with other variables

(3) access variables do not need to be locked

In fact, these conditions indicate that these valid values that can be written to the volatile variable are independent of the state of any program, including the current state of the variable.

The restriction of the first condition prevents the volatile variable from being used as a thread-safe counter. While the increment operation (x++) looks like a single operation, it is actually a combined operation consisting of a sequence of read-modify-write operations that must be performed atomically, and volatile does not provide the required atomic properties. Doing the right thing requires keeping the value of x constant for the duration of the operation, which is not possible with the volatile variable. (however, if you adjust the value to be written only from a single thread, you can ignore the first condition.)

Most programming situations conflict with one of these three conditions, making the volatile variable less universally applicable to thread safety than synchronized. Listing 1 shows a non-thread-safe numeric range class. It contains an invariant that the lower bound is always less than or equal to the upper bound.

use volatile correctly:

mode #1: status flag

Perhaps the canonical use of the volatile variable is simply to use a Boolean status flag to indicate that an important one-time event occurred, such as completion of initialization or request an outage.

Many applications contain a control structure in the form of "do some work before you're ready to stop the program," as shown in listing 2:

Listing 2. Use

with the volatile variable as a status flag

 volatile boolean shutdownRequested;

 ... 

  public void shutdown() { shutdownRequested = true; }

  public void doWork() {

  while (!shutdownRequested) {

  // do stuff

  }

  }

It is likely that the shutdown() method will be called from outside the loop -- that is, in another thread -- so some synchronization needs to be performed to ensure the visibility of the shutdownRequested variable is correctly implemented. It may be called from the JMX listener, the action listener in the GUI event thread, through RMI, through an Web service, and so on. However, writing a loop with the synchronized block is more cumbersome than writing with the volatile status flag shown in listing 2. volatile is a good place to use volatile.

because it simplifies coding and the status flag does not depend on any other state in the program

A common feature of this type of state token is that there is usually only one state transition; The shutdownRequested flag is converted from false to true, and the program stops. This pattern can be extended to state flags that go back and forth, but only if the transition cycle is not detected (from false to true and then to false). In addition, some atomic state transition mechanisms, such as atomic variables, are required.

mode #2: one-time security release (one-time safe publication)

Lack of synchronization leads to an inability to achieve visibility, which makes it more difficult to determine when to write an object reference instead of a primitive value. In the absence of synchronization, it is possible to encounter an updated value for an object reference (written by another thread) and an old value for the object's state. This is the root of the problem that causes the name double-checked locking (double-checked-locking), where the object reference is read without synchronization, and the problem is that you might see an updated reference, but still see an incomplete constructed object through that reference).

One technique for implementing secure publishing objects is to define object references as volatile types. Listing 3 shows an example where a background thread loads some data from a database during startup. When other code is able to make use of this data, it checks to see if it has ever been published before it is used.

Listing 3. Use the volatile variable for a one-time safe release of


public class BackgroundFloobleLoader {

  public volatile Flooble theFlooble;

  public void initInBackground() {

  // do lots of stuff

  theFlooble = new Flooble(); // this is the only write to theFlooble

  }

  }

  public class SomeOtherClass {

  public void doWork() {

  while (true) {

  // do some stuff ... 

  // use the Flooble, but only if it is ready

  if (floobleLoader.theFlooble != null)

  doSomething(floobleLoader.theFlooble);

  }

  }

  }

If the theFlooble reference is not of type volatile, the code in doWork() will get an incomplete Flooble.

when it removes the reference to theFlooble

One requirement of this pattern is that the published object must be either thread-safe or effectively immutable (effectively immutable means that the state of the object will never be changed after publication). A reference of type volatile ensures the visibility of the published form of the object, but if the state of the object changes after publication, then additional synchronization is required.

mode #3: independent observation (independent observation)

Another simple way to safely use volatile is to "publish" observations for internal use on a regular basis. For example, suppose you have an environmental sensor that senses the ambient temperature. A background thread might read the sensor every few seconds and update the volatile variable that contains the current document. The variable can then be read by other threads to see the latest temperature value at any time.

Another application that USES this pattern is the collection of statistics for the program. Listing 4 shows how the authentication mechanism remembers the name of the last logged in user. The lastUser reference is used repeatedly to publish the value for use by the rest of the program.

Listing 4. Use the volatile variable for publishing

for multiple independent observations

public class UserManager {

  public volatile String lastUser;

  public boolean authenticate(String user, String password) {

  boolean valid = passwordIsValid(user, password);

  if (valid) {

  User u = new User();

  activeUsers.add(u);

  lastUser = user;

  }

  return valid;

  }

  }

This pattern is an extension of the previous pattern; Publishing a value for use elsewhere in the program, but unlike publishing a one-time event, this is a series of separate events. This pattern requires that the published value be effectively immutable -- that is, that the value's state does not change after publication. Code that USES this value needs to be aware that it can change at any time.

mode #4:"volatile bean" mode

The volatile bean pattern is suitable for frames that use JavaBeans as an "honor structure". In volatile bean mode, JavaBean is used as a set of containers with separate attributes of getter and/or setter methods. The rationale for the volatile bean pattern is that many frameworks provide containers for holders of volatile data, such as HttpSession, but the objects placed in those containers must be thread-safe.

In volatile bean mode, all data members of JavaBean are of type volatile, and the getter and setter methods must be very general -- they cannot contain any logic other than getting or setting the appropriate properties. In addition, for data members referenced by an object, the referenced object must be effectively immutable. This disables properties with array values, because when an array reference is declared as volatile, only the reference, not the array itself, has volatile semantics. For any volatile variable, the invariant or constraint cannot contain the JavaBean attribute. The example in listing 5 shows JavaBean:

following the pattern volatile bean

Mode #4:"volatile bean" mode


@ThreadSafe

  public class Person {

  private volatile String firstName;

  private volatile String lastName;

  private volatile int age;

  public String getFirstName() { return firstName; }

  public String getLastName() { return lastName; }

  public int getAge() { return age; }

  public void setFirstName(String firstName) {

  this.firstName = firstName;

  }

  public void setLastName(String lastName) {

  this.lastName = lastName;

  }

  public void setAge(int age) {

  this.age = age;

  }

  }

volatile advanced mode

The patterns described in the previous sections cover most of the basic use cases in which volatile is useful and easy to use. This section introduces a more advanced pattern in which volatile provides performance or scalability benefits.

The advanced mode applied by volatile is very fragile. Therefore, the assumptions must be carefully proven, and the patterns are tightly encapsulated, because even very small changes can damage your code! Again, the reason for using the more advanced volatile use case is that it can improve performance, ensuring that you really identify the need to achieve this performance benefit before you start applying the advanced pattern. Need to weigh these patterns, abandon the readability and maintainability for possible performance gains - if you don't need to improve performance (or are not able to pass a strict test program that you need it), then it is likely to be a bad deal, because you'll likely do more harm than good, for what value is lower than to give up things.

Pattern #5: low cost read-write lock policy

By now, you should be aware that volatile's capabilities are not sufficient to implement a counter. Because ++x is really a simple combination of three operations (read, add, and store), its update value can be lost if multiple threads happen to try to incrementally manipulate the volatile counter at the same time.

However, if read operations far exceed write operations, you can use a combination of internal locking and the volatile variable to reduce common code path overhead. The thread-safe counter shown in listing 6 USES synchronized to ensure that the delta operation is atomic, and volatile to ensure that the current result is visible. This approach achieves better performance if updates are not frequent, because the read path overhead involves only volatile reads, which is generally better than the overhead of an uncontested lock acquisition.

Listing 6.

with volatile and synchronized for "low cost read-write lock "


@ThreadSafe

  public class CheesyCounter {

  // Employs the cheap read-write lock trick

  // All mutative operations MUST be done with the 'this' lock held

  @GuardedBy("this") private volatile int value;

  public int getValue() { return value; }

  public synchronized int increment() {

  return value++;

  }

  }

This technique is called "low cost read-write locking" because you use different synchronization mechanisms for read and write operations. Because the write operation in this example violates the first condition for using volatile, counters cannot be safely implemented using volatile -- you must use locks. However, you can use volatile in read operations to ensure the visibility of the current value, so you can use locks for all changes and volatile for read-only operations. Where the lock allows only one thread to access the value at a time, volatile allows multiple threads to perform reads, so when using volatile to ensure that the code path is read, you get a higher degree of sharing than when using the lock to execute the entire code path -- just like a read-write operation. However, keep in mind the weakness of this pattern: if you go beyond the most basic application of the pattern, it becomes very difficult to combine the two competing synchronization mechanisms.

on instruction reordering and Happens-before rule

1. Reorder

The Java language specification specifies that JVM threads maintain sequential semantics internally, meaning that instructions may not be executed in the same order as code as long as the end result of a program is the same as it would be in a strictly sequential environment. This process is done by a reordering called an instruction. The significance of instruction reordering lies in that JVM can properly reorder machine instructions according to the characteristics of the processor (CPU's multi-stage cache system, multi-core processor, etc.) to make the machine instructions more in line with the execution characteristics of CPU and maximize the performance of the machine.

The simplest model for program execution is to execute in the order in which the instructions appear, which is independent of the CPU that executes the instructions and maximizes the portability of the instructions. The technical term for this model is the sequential consistency model. But neither modern computer architecture nor processor architecture guarantees this (because manual specification does not always guarantee compliance with the characteristics of CPU processing).

2. appens-before rules

The Java storage model has an happens-before principle that if the action B wants to see the result of the action A (whether or not A/B is executed in the same thread), then A/B needs to satisfy the happens-before relationship.

Before introducing the happens-before principle, I introduce a concept: JMM action (Java Memeory Model Action), Java storage model action. An action (Action) includes: reads and writes to variables, monitors lock and release locks, threads start() and join(). We'll talk about locks later.

happens-before full rule:

(1) each Action in the same thread happens-before is followed by any Action.

(2) unlocking of a monitor happens-before locks the same monitor for each subsequent.

(3) write operation happens-before to volatile field, and read operation happens-before to every subsequent field.

(4) Thread.start () calls happens-before in the startup thread.

(5) all actions in Thread are happens-before checked by another thread to the end of the thread or back to Thread.join () or Thread.isAlive ()== false.

(6) one thread A calls another thread B's interrupt() both happens-before finds B interrupted by A (B throws an exception or A detects isInterrupted() or interrupted() of B.

(7) the end of an object constructor happens-before and the beginning of the object finalizer

(8) if A action happens-before action B, and B action happens-before action C, then A action happens-before action happens.

The above is the whole content of this article, about volatile variable for you to introduce here, hope to help you learn volatile variable in java.


Related articles: