Balking mode: Talking about thread-safe singleton mode

Topic: Balking mode: Talking about thread-safe singleton mode

In the last article, we mentioned that you can use ** “multi-threaded version of if” to understand Guarded Suspension mode. Unlike single-threaded if, this “multi-threaded version of if” needs to wait, and I'm still very persistent. I have to wait until the conditions are true. But obviously in this world, not all scenes need to be so persistent, and sometimes we need to give up quickly .

One of the most common examples that need to be given up quickly is the automatic save function provided by various editors. The implementation logic of the automatic save function is generally to automatically execute 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, you need to quickly give up the save operation. The following sample code will code the automatic save function. Obviously, the AutoSaveEditor class is not thread-safe, because reading and writing of shared variables changed does not use synchronization, so how to ensure the thread-safety of AutoSaveEditor?

class AutoSaveEditor{

  //文件是否被修改过
  boolean changed=false;
  
  //定时任务线程池
  ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor();
  
  //定时执行自动保存
  void startAutoSave(){
    ses.scheduleWithFixedDelay(()->{
      autoSave();
    }, 5, 5, TimeUnit.SECONDS);  
  }
  
  //自动存盘操作
  void autoSave(){
  //没有改变
    if (!changed) {
      return;
    }
    changed = false;
    //执行存盘操作
    //省略且实现
    this.execSave();
  }
  
  //编辑操作
  void edit(){
    //省略编辑逻辑
    ......
    changed = true;
  }
}

To solve this problem, I believe you must be here: the methods of reading and writing shared variables changed, autoSave () and edit (), can be added with a mutual exclusion lock. Although this is simple, the performance is poor because the range of the lock is too large. Then we can narrow the scope of the lock and only add the lock where the shared variable read and write is changed. The implementation code is shown below.

//自动存盘操作
void autoSave(){
  synchronized(this){
    if (!changed) {
      return;
    }
    changed = false;
  }
  //执行存盘操作
  //省略且实现
  this.execSave();
}

//编辑操作
void edit(){
  //省略编辑逻辑
  ......
  synchronized(this){
    changed = true;
  }
}

If you analyze this example program in depth, you will find that the shared variable in the example is a state variable, and the business logic depends on the state of this state variable: when the state meets a certain condition, the execution of a certain business logic, its essence In fact, it is just an if, put in a multi-threaded scene, it is a kind of "multi-threaded 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 the Balking pattern .

Classical implementation of Balking mode

The Balking mode is essentially a solution to the "multi-threaded version of if" in a standardized way. For the above example of automatic saving, the writing method after using the Balking mode to normalize is shown below. You will find that only the edit () method The assignment operation of the shared variable changed is extracted into change (), which has the advantage of separating concurrent processing logic from business logic.

boolean changed=false;

//自动存盘操作
void autoSave(){
  synchronized(this){
    if (!changed) {
      return;
    }
    changed = false;
  }
  //执行存盘操作
  //省略且实现
  this.execSave();
}

//编辑操作
void edit(){
  //省略编辑逻辑
  ......
  change();
}

//改变状态
void change(){
  synchronized(this){
    changed = true;
  }
}

Implementation of Balking mode with volatile

Earlier we used synchronized to implement the Balking mode. This implementation is the most secure. It is recommended that you also use this solution 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.
Before, there was a case of RPC framework routing table. In RPC framework, the local routing table is to synchronize information with the registration center. When the application starts, the application dependent service routing table will be synchronized from the registration center to the local routing table. If the registry is down when the application restarts, it will cause the services that the application depends on to be unavailable because the routing table of the dependent services cannot be found. In order to prevent this extreme situation, the RPC framework can automatically save the local routing table to a local file. If the registry is down when restarting, the routing table before restarting will be restored from the local file. This is actually a degraded plan.

The automatic saving routing table is the same as the automatic saving principle of the editor introduced earlier, and can also be implemented in the Balking mode, but we use volatile to implement it here. The code for implementation is shown below. The reason why volatile can be used is because there is no atomic requirement for write operations of shared variables changed and rt, and the scheduling method of scheduleWithFixedDelay () can ensure that only one thread executes the autoSave () method at the same time.

//路由表信息
public class RouterTable {

  //Key:接口名
  //Value:路由集合
  ConcurrentHashMap<String, CopyOnWriteArraySet<Router>> 
    rt = new ConcurrentHashMap<>(); 
       
  //路由表是否发生变化
  volatile boolean changed;
  
  //将路由表写入本地文件的线程池
  ScheduledExecutorService ses= Executors.newSingleThreadScheduledExecutor();
  
  //启动定时任务
  //将变更后的路由表写入本地文件
  public void startLocalSaver(){
    ses.scheduleWithFixedDelay(()->{
      autoSave();
    }, 1, 1, MINUTES);
  }
  
  //保存路由表到本地文件
  void autoSave() {
    if (!changed) {
      return;
    }
    changed = false;
    //将路由表写入本地文件
    //省略其方法实现
    this.save2Local();
  }
  
  //删除路由
  public void remove(Router router) {
    Set<Router> set=rt.get(router.iface);
    if (set != null) {
      set.remove(router);
      //路由表已发生变化
      changed = true;
    }
  }
  
  //增加路由
  public void add(Router router) {
    Set<Router> set = rt.computeIfAbsent(
      route.iface, r -> new CopyOnWriteArraySet<>());
    set.add(router);
    //路由表已发生变化
    changed = true;
  }
}

Balking mode has a very typical application scenario is single initialization , the following sample code is its implementation. In this implementation, we declare init () as a synchronous method, so that only one thread can execute the init () method at the same time; the init () method will set inited to true when the first execution is completed, so Subsequent threads that execute the init () method will no longer execute doInit ().

class InitTest{

  boolean inited = false;
  
  synchronized void init(){
    if(inited){
      return;
    }
    //省略doInit的实现
    doInit();
    inited=true;
  }
}

The thread-safe singleton mode is essentially a single initialization, so you can use the Balking mode to implement the thread-safe singleton mode. The following sample code is its implementation. Although this implementation has no functional problems, the performance is very poor, because the mutex lock is synchronized to serialize the getInstance () method, is there any way to optimize its performance?

class Singleton{
  private static Singleton singleton;
  //构造方法私有化  
  private Singleton(){}
  //获取实例(单例)
  public synchronized static Singleton getInstance(){
    if(singleton == null){
      singleton=new Singleton();
    }
    return singleton;
  }
}

Of course there are ways, that is the classic ** Double Check (Double Check) ** program, the following sample code is its detailed implementation. In the double-check scheme, once the Singleton object is successfully created, the code related to synchronized (Singleton.class) {} will not be executed, that is to say, the execution path of the getInstance () method is lock-free at this time, thus solving Performance issues. However, it should be noted that volatile is used in this scheme to prohibit compilation and optimization. As for the second check after acquiring the lock, it is out of responsibility for security.

class Singleton{
  private static volatile Singleton singleton;
  //构造方法私有化  
  private Singleton() {}
  
  //获取实例(单例)
  public static Singleton getInstance() {
    //第一次检查
    if(singleton==null){
      synchronize{Singleton.class){
        //获取锁后二次检查
        if(singleton==null){
          singleton=new Singleton();
        }
      }
    }
    return singleton;
  }
}

to sum up

Balking mode and Guarded Suspension mode do not seem to have much relationship from the implementation point of view, Balking mode only needs to be solved with a mutex, and Guarded Suspension mode requires the use of advanced concurrency primitives; but From a perspective, they all solve the "thread-safe if" semantics. The difference is that the Guarded Suspension mode will wait if the condition is true, while the Balking mode will not wait.

The classic implementation of the Balking mode is to use a mutex lock. You can use the built-in synchronized in the Java language, or you can use the SDK to provide Lock. If you are not satisfied with the performance of the mutex lock, you can try the volatile scheme, but using the volatile scheme requires you to be more cautious.

Of course, you can also try to use the double check scheme to optimize performance. The first check in the double check is entirely for performance considerations: avoid performing lock operations, because locking operations are time-consuming. The second check after locking is out of responsibility for safety. The double check scheme is often used in optimizing locking performance.

Demo

1
public class BalkingClient {//阻拦,阻碍,场景就是别人做了我就不做了,类似服务生服务
    public static void main(String[] args) {
        //观察的对象 BalkingData 
        BalkingData balkingData = new BalkingData("\\balking.txt", "===BEGIN====");
        new CustomerThread(balkingData).start();
        new WaiterThread(balkingData).start();
    }
}
2
public class BalkingData {//前面有Future模式,Suspension模式,这里是发现有人做了就不做了 近的服务生做了远的服务生就不做了
    private final String fileName;

    private String content;

    private boolean changed;

    public BalkingData(String fileName, String content) {
        this.fileName = fileName;
        this.content = content;
        this.changed = true;
    }

    public synchronized void change(String newContent) {
        this.content = newContent;
        this.changed = true;
    }

    public synchronized void save() throws IOException {
        if (!changed) {
            return;
        }

        doSave();
        this.changed = false;
    }

    private void doSave() throws IOException {
        System.out.println(Thread.currentThread().getName() +
         " calls do save,content=" + content);
         //实现Closeable接口自动帮你关掉
        try (Writer writer = new FileWriter(fileName, true)) {
            writer.write(content);
            writer.write("\n");
            writer.flush();
        }
    }
}
3
public class CustomerThread extends Thread {

    private final BalkingData balkingData;

    private final Random random = new Random(System.currentTimeMillis());

    public CustomerThread(BalkingData balkingData) {
        super("Customer");
        this.balkingData = balkingData;
    }

    @Override
    public void run() {
        try {
            balkingData.save();
            for (int i = 0; i < 20; i++) {
                balkingData.change("No." + i);
                Thread.sleep(random.nextInt(1000));
                balkingData.save();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
4
public class WaiterThread extends Thread {

    private final BalkingData balkingData;

    public WaiterThread(BalkingData balkingData) {
        super("Waiter");
        this.balkingData = balkingData;
    }

    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            try {
                balkingData.save();
                Thread.sleep(1_000L);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Published 138 original articles · won praise 3 · Views 7205

Guess you like

Origin blog.csdn.net/weixin_43719015/article/details/105693538
Recommended