Balking mode of concurrent programming: on the singleton mode of thread safety

Posted by blakey on Sun, 21 Nov 2021 05:53:18 +0100

introduction

The previous article mentioned that the "multi-threaded version of if" can be used to understand the Guarded Suspension mode, which is different from the if in a single thread. This "multi-threaded version of if" needs to wait and is very persistent. You must wait until the condition is true. But obviously, not all scenes in this world need to be so persistent. Sometimes we need to give up quickly.

Let's draw a conclusion first. After reading this article, you can compare it with the previous article:
Guarded Suspension mode of concurrent programming: standard implementation of waiting for wake-up mechanism

Balking mode and Guarded Suspension mode do not seem to have much relationship in terms of implementation. Balking mode can be solved only by mutual exclusion, while Guarded Suspension mode uses advanced concurrency primitives such as management; However, from the perspective of application, they all solve the "thread safe if" semantics. The difference is that the Guarded Suspension mode will wait for the if condition to be true, while the balking mode will not wait.

The implementation logic of the auto save function is generally to automatically perform the save operation every certain time. The premise of the save operation is that the file has been modified. If the file has not been modified, the save operation needs to be quickly abandoned.

The following example code codes the auto save function. Obviously, AutoSaveEditor is not thread safe, because it does not use synchronization for the read and write of the shared variable changed. How to ensure the thread safety of AutoSaveEditor?

class AutoSaveEditor{
  // Has the file been modified
  boolean changed=false;
  // Timed task thread pool
  ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
  // Scheduled auto save
  void startAutoSave(){
    ses.scheduleWithFixedDelay(()->{autoSave();
    }, 5, 5, TimeUnit.SECONDS);
  }
  // Automatic save operation
  void autoSave(){
    if (!changed) {
      return;
    }
    changed = false;
    // Perform save operation
    // Omitted and implemented
    this.execSave();
  }
  // Edit operation
  void edit(){
  // Omit editing logic
  ......
  changed = true;
  }
}

Read and write the shared variable changed methods autoSave() and edit() with mutual exclusive locks. Although this is simple, the performance is poor because the range of locks is too large. Then we can narrow the scope of the lock and lock only where the shared variable changed is read and written.

// Automatic save operation
void autoSave(){
  synchronized(this){
   if (!changed) {
     return;
   }
   changed = false;
  }
  // Perform save operation
  // Omitted and implemented
  this.execSave();
}
// Edit operation
void edit(){
  // Omit editing logic
  ......
  synchronized(this){
    changed = true;
  }
}

The shared variable in the example is a state variable, and the business logic depends on the state of the state variable: when the state meets a certain condition, the essence of executing a business logic is just an if. In a multithreaded scenario, it is a "multithreaded version of if". There are still many application scenarios for this "multithreaded version of if", so some people have summarized it into a design pattern called Balking pattern.

Classic implementation of Balking mode

Balking mode is essentially a standardized solution to "multithreaded version of if". For the example of automatic saving above, the writing method after using balking mode normalization is as follows. You will find that only the assignment operation of shared variable changed in edit() method is extracted into change(), This has the advantage of separating concurrent processing logic from business logic.

boolean changed=false;
// Automatic save operation
void autoSave(){
  synchronized(this){
    if (!changed) {
      return;
    }
    changed = false;
  }
  // Perform save operation
  // Omitted and implemented
  this.execSave();
}
// Edit operation
void edit(){
  // Omit editing logic
  ......
  change();
}
// Change state
void change(){
  synchronized(this){
    changed = true;
  }
}

The Balking mode is implemented with synchronized, which is the most secure way. It is recommended that you also use this scheme in your actual work. However, in some specific scenarios, volatile can also be used, but the premise of using volatile is that there is no requirement for atomicity.

Implementation of thread safety in Balking mode

A typical application scenario of Balking mode is single initialization. The following example code is its implementation. In this implementation scheme, we declare init() as a synchronous method, so that only one thread can execute init() method at the same time; The init() method will set inited to true after the first execution, so that subsequent threads executing the init() method will no longer execute doInit().

class InitTest{
  boolean inited = false;
  synchronized void init(){
    if(inited){
      return;
    }
   // Omit the implementation of doInit
   doInit();
   inited=true;
  }
}

Thread safe singleton mode is actually a single initialization, so Balking mode can be used to realize thread safe singleton mode.

The following is the classic "double check" implementation singleton mode code:

class Singleton{
  private static volatile Singleton singleton;
  // Construction method privatization
  private Singleton() {}
  // Get instance (single instance)
  public static Singleton getInstance() {
    // First inspection
    if(singleton==null){
      synchronize{Singleton.class){
       // Secondary check after lock acquisition
       if(singleton==null){
         singleton=new Singleton();
       }
      }
    }
    return singleton;
  }
}

Summary:
If you feel useful, please praise and pay attention! Thank you for your support 🙏 thank you!

Topics: Java Concurrent Programming Singleton pattern