Geek Time-The Beauty of Design Patterns Singleton Mode (Part 1): Why is it said that the double detection that supports lazy loading is not better than the hungry style?

There are many articles explaining the singleton pattern on the Internet, but most of them focus on explaining how to implement a thread-safe singleton. I will also talk about the implementation methods of various singletons today, but this is not the focus of our column study. My focus is still to show you the following questions (the first question will be explained today, the next three This question will be explained in the next class).

● Why use singleton?

● What are the problems with the singleton?

● The difference between singleton and static class?

● What are the alternative solutions?

Why use singleton?

Singleton design pattern (Singleton Design Pattern) is very easy to understand. A class is only allowed to create one object (or instance), then this class is a singleton class. This design pattern is called singleton design pattern, or singleton pattern for short.

For the concept of singleton, I don't think there is any need to explain too much, you can understand it at a glance. Let’s focus on it, why do we need a singleton design pattern? What problems can it solve? Next, I will explain through two actual combat cases.

Practical case 1: Dealing with resource access conflicts

Let's look at the first example first. In this example, we have implemented a custom Logger class that prints logs to a file. The specific code implementation is as follows:


public class Logger {
    
    
  private FileWriter writer;
  
  public Logger() {
    
    
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message) {
    
    
    writer.write(mesasge);
  }
}

// Logger类的应用示例:
public class UserController {
    
    
  private Logger logger = new Logger();
  
  public void login(String username, String password) {
    
    
    // ...省略业务逻辑代码...
    logger.log(username + " logined!");
  }
}

public class OrderController {
    
    
  private Logger logger = new Logger();
  
  public void create(OrderVo order) {
    
    
    // ...省略业务逻辑代码...
    logger.log("Created an order: " + order.toString());
  }
}

After reading the code, don't worry about reading my explanation below. You can think about what is wrong with this code.

In the above code, we noticed that all logs are written to the same file /Users/wangzheng/log.txt. In UserController and OrderController, we create two Logger objects respectively. In the Servlet multi-threaded environment of the Web container, if two Servlet threads execute the login() and create() functions at the same time, and write logs to the log.txt file at the same time, there may be log information overwriting each other Happening.

Why do they cover each other? We can understand by analogy. In a multithreaded environment, if two threads add 1 to the same shared variable at the same time, because the shared variable is a competition resource, the final result of the shared variable may not be increased by 2, but only by 1. In the same way, the log.txt file here is also competing for resources. If two threads write data to it at the same time, there is a possibility that each other may overwrite each other.

Insert picture description here
How to solve this problem? The first thing we thought of was the way of locking: adding a mutex lock to the log() function (the synchronized keyword can be used in Java), and only one thread is allowed to call and execute the log() function at a time. The specific code implementation is as follows:


public class Logger {
    
    
  private FileWriter writer;

  public Logger() {
    
    
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message) {
    
    
    synchronized(this) {
    
    
      writer.write(mesasge);
    }
  }
}

However, if you think about it, can this really solve the problem of overwriting each other when multiple threads write logs? the answer is negative. This is because this kind of lock is an object-level lock. If an object calls the log() function in different threads at the same time, it will be forced to execute sequentially. However, different objects do not share the same lock. In different threads, the log() function is called and executed by different objects, the lock will not work, and there may still be the problem of writing logs overwriting each other.

Insert picture description here
Let me add a little bit here. In the explanation and code given just now, I deliberately "concealed" a fact: it doesn't really matter whether we add object-level locks to the log() function. Because FileWriter itself is thread-safe, its internal implementation itself adds object-level locks. Therefore, when the write() function is called on the outer layer, adding object-level locks is actually unnecessary. Because different Logger objects do not share the FileWriter object, locks at the FileWriter object level cannot solve the problem of data writing overwriting each other.

How can we solve this problem? In fact, it is not difficult to solve this problem. We only need to replace the object-level lock with the class-level lock. Let all objects share the same lock. This avoids the log coverage problem caused by calling log() function between different objects at the same time. The specific code implementation is as follows:


public class Logger {
    
    
  private FileWriter writer;

  public Logger() {
    
    
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message) {
    
    
    synchronized(Logger.class) {
    
     // 类级别的锁
      writer.write(mesasge);
    }
  }
}

In addition to using class-level locks, there are actually many ways to solve the problem of resource competition. Distributed locks are the most commonly heard solution. However, it is not easy to implement a safe, reliable, bug-free, and high-performance distributed lock. In addition, concurrent queues (such as BlockingQueue in Java) can also solve this problem: multiple threads write logs to the concurrent queue at the same time, and a single thread is responsible for writing the data in the concurrent queue to the log file. This method is also slightly more complicated to implement.

Compared with these two solutions, the solution idea of ​​singleton mode is simpler. Compared with the previous class-level lock, the advantage of the singleton mode is that there is no need to create so many Logger objects. On the one hand, it saves memory space and on the other hand, it saves system file handles. (For operating systems, file handles are also a kind of resource and cannot be wasted. ).

We design Logger as a singleton class. Only one Logger object is allowed to be created in the program. All threads share this Logger object and share a FileWriter object. FileWriter itself is object-level thread-safe, which avoids multiple In the case of threads, writing logs will cover each other.

According to this design idea, we implemented the Logger singleton class. The specific code is as follows:


public class Logger {
    
    
  private FileWriter writer;
  private static final Logger instance = new Logger();

  private Logger() {
    
    
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public static Logger getInstance() {
    
    
    return instance;
  }
  
  public void log(String message) {
    
    
    writer.write(mesasge);
  }
}

// Logger类的应用示例:
public class UserController {
    
    
  public void login(String username, String password) {
    
    
    // ...省略业务逻辑代码...
    Logger.getInstance().log(username + " logined!");
  }
}

public class OrderController {
    
      
  public void create(OrderVo order) {
    
    
    // ...省略业务逻辑代码...
    Logger.getInstance().log("Created a order: " + order.toString());
  }
}

Actual case 2: Representing the globally unique class

From the business concept, if some data should only be saved in the system, it is more suitable to design as a singleton.

For example, configuration information class. In the system, we only have one configuration file. When the configuration file is loaded into the memory, it exists in the form of an object, and there should be only one copy.

Another example is the unique incremental ID number generator. If there are two objects in the program, there will be a situation of generating duplicate IDs. Therefore, we should design the ID generator class as a singleton.


import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
    
    
  // AtomicLong是一个Java并发库中提供的一个原子变量类型,
  // 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
  // 比如下面会用到的incrementAndGet().
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {
    
    }
  public static IdGenerator getInstance() {
    
    
    return instance;
  }
  public long getId() {
    
     
    return id.incrementAndGet();
  }
}

// IdGenerator使用举例
long id = IdGenerator.getInstance().getId();

In fact, the two code examples (Logger, IdGenerator) mentioned today are not elegantly designed, and there are still some problems. As for what is wrong and how to reform it, today I will be closing it for the time being, and I will explain it in detail in the next lesson.

How to implement a singleton?

Although there are many articles describing how to implement a singleton pattern, in order to ensure the integrity of the content, I will briefly introduce several classic implementation methods. In summary, to implement a singleton, we need to pay attention to the following points:

● The constructor needs to have private access rights, so as to avoid external creation of instances through new;

● Consider thread safety issues when creating objects;

● Consider whether to support lazy loading;

● Consider whether the performance of getInstance() is high (whether it is locked).

If you are already familiar with this area, you can use it as a review. Note that the following singleton implementations are for the Java language syntax. If you are familiar with other languages, you may wish to compare these implementations of Java, and try to summarize by yourself, how to implement it using the language you are familiar with .

1. Hungry Chinese

The implementation of the hungry man style is relatively simple. When the class is loaded, the static instance of the instance has been created and initialized, so the creation process of the instance instance is thread-safe. However, this implementation does not support lazy loading (create an instance when IdGenerator is actually used), as we can see from the name. The specific code implementation is as follows:


public class IdGenerator {
    
     
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {
    
    }
  public static IdGenerator getInstance() {
    
    
    return instance;
  }
  public long getId() {
    
     
    return id.incrementAndGet();
  }
}

Some people feel that this implementation is not good because it does not support lazy loading. If the instance takes up a lot of resources (for example, a lot of memory) or initialization takes a long time (for example, it needs to load various configuration files), initializing the instance in advance is a waste of resources. . The best way is to initialize it when it is used. However, I personally do not agree with this view.

If the initialization takes a long time, then we'd better not wait until the time to actually use it before performing this time-consuming initialization process, which will affect the performance of the system (for example, when responding to client interface requests, do this initialization Operation, the response time of this request will become longer or even time out). Using the hungry-Chinese implementation method, the time-consuming initialization operation is completed before the program starts, so as to avoid the performance problems caused by the initialization when the program is running.

If the instance occupies a lot of resources, according to the fail-fast design principle (problems are exposed early), then we also hope to initialize the instance when the program starts. If the resources are not enough, an error will be triggered when the program starts (such as PermGen Space OOM in Java), and we can fix it immediately. This can also avoid that after the program has been running for a period of time, the initialization of this instance will suddenly occupy too much resources, causing the system to crash and affecting the availability of the system.

2. Lazy man

There is the hungry man style, and the corresponding, there is the lazy man style. The advantage of the lazy man over the hungry man is that it supports lazy loading. The specific code implementation is as follows:


public class IdGenerator {
    
     
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {
    
    }
  public static synchronized IdGenerator getInstance() {
    
    
    if (instance == null) {
    
    
      instance = new IdGenerator();
    }
    return instance;
  }
  public long getId() {
    
     
    return id.incrementAndGet();
  }
}

However, the shortcomings of the lazy style are also obvious. We added a large lock (synchronzed) to the getInstance() method, resulting in low concurrency of this function. To quantify it, the concurrency is 1, which is equivalent to a serial operation. And this function is always called during the singleton usage period. If this singleton class is occasionally used, then this implementation is acceptable. However, if it is used frequently, problems such as frequent locks, lock releases, and low concurrency will cause performance bottlenecks, and this implementation is not desirable.

3. Double detection

The hungry style does not support lazy loading, and the lazy style has performance problems and does not support high concurrency. Then let's look at a singleton implementation that supports both lazy loading and high concurrency, that is, double detection implementation.

In this implementation, as long as the instance is created, even if the getInstance() function is called again, it will no longer enter the locking logic. Therefore, this implementation method solves the problem of low concurrency of the lazy man. The specific code implementation is as follows:


public class IdGenerator {
    
     
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private IdGenerator() {
    
    }
  public static IdGenerator getInstance() {
    
    
    if (instance == null) {
    
    
      synchronized(IdGenerator.class) {
    
     // 此处为类级别的锁
        if (instance == null) {
    
    
          instance = new IdGenerator();
        }
      }
    }
    return instance;
  }
  public long getId() {
    
     
    return id.incrementAndGet();
  }
}

Some people on the Internet say that there are some problems with this implementation. Because of the reordering of instructions, it may cause the IdGenerator object to be new and assigned to the instance before it can be initialized (execute the code logic in the constructor) before being used by another thread.

To solve this problem, we need to add the volatile keyword to the instance member variable to prohibit instruction reordering. In fact, only very low versions of Java have this problem. The high version of Java we are using has solved this problem in the internal implementation of the JDK (the solution is very simple, as long as the object new operation and initialization operation are designed as atomic operations, reordering is naturally prohibited). The detailed explanation on this point is related to a specific language, so I won't go into it, and interested students can study it by themselves.

4. Static inner class

Let's look at a simpler implementation method than double detection, which is to use Java's static inner classes. It's a bit similar to the hungry man, but it can do lazy loading. How exactly did it do it? Let's first look at its code implementation.


public class IdGenerator {
    
     
  private AtomicLong id = new AtomicLong(0);
  private IdGenerator() {
    
    }

  private static class SingletonHolder{
    
    
    private static final IdGenerator instance = new IdGenerator();
  }
  
  public static IdGenerator getInstance() {
    
    
    return SingletonHolder.instance;
  }
 
  public long getId() {
    
     
    return id.incrementAndGet();
  }
}

SingletonHolder is a static internal class. When the external class IdGenerator is loaded, the SingletonHolder instance object will not be created. Only when the getInstance() method is called, the SingletonHolder will be loaded and the instance will be created at this time. The uniqueness of the instance and the thread safety of the creation process are guaranteed by the JVM. Therefore, this implementation method not only ensures thread safety, but also enables lazy loading.

5. Enumeration

Finally, we introduce one of the simplest implementations, a singleton implementation based on enumerated types. This implementation ensures the thread safety of instance creation and the uniqueness of the instance through the characteristics of the Java enumeration type itself. The specific code is as follows:


public enum IdGenerator {
    
    
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() {
    
     
    return id.incrementAndGet();
  }
}

Guess you like

Origin blog.csdn.net/zhujiangtaotaise/article/details/110442711