Geek Time-The Beauty of Design Patterns Singleton Mode (Part 2): How to design and implement a distributed singleton mode in a cluster environment?

Aiming at the singleton mode, we explained the application scenarios of the singleton, several common code implementations and existing problems, and gave a rough idea of ​​how to replace the singleton mode, such as factory mode and IOC container. Today, let’s extend it further and discuss the following issues together:

● How to understand the uniqueness in the singleton pattern?

● How to realize the singleton of thread?

● How to implement a singleton in a cluster environment?

● How to implement a multi-case pattern?

Today's content is a bit "brain-burning", I hope you will think more about it as you read it. Not much to say, let's officially start today's learning!

How to understand the uniqueness in the singleton pattern?

First, let’s revisit the definition of a singleton: "A class is only allowed to create only one object (or instance), then this class is a singleton class. This design pattern is called the singleton design pattern, or singleton pattern for short. ."

The definition mentions that "a class is only allowed to create only one object". What is the scope of the uniqueness of the object? Does it mean that only one object is allowed to be created in a thread, or does it mean that only one object is allowed to be created in a process? The answer is the latter, that is, the object created by the singleton pattern is unique to the process. It is a bit difficult to understand here, let me explain in detail.

The code we write, compiled, linked, and organized together, constitutes a file that the operating system can execute, which is what we usually call an "executable file" (such as an exe file under Windows). An executable file is actually a set of instructions that the code is translated into an operating system understandable. You can simply understand it as the code itself.

When we use the command line or double-click to run the executable file, the operating system will start a process and load the executable file from the disk into its own process address space (you can understand the memory storage area allocated by the operating system for the process to Store code and data). Then, the process executes the code contained in the executable file one by one. For example, when the process reads the User user = new User(); statement in the code, it creates a user temporary variable and a User object in its address space.

The address space is not shared between processes. If we create another process in one process (for example, there is a fork() statement in the code, a new process will be created when the process executes this statement), the operating system A new address space will be allocated to the new process, and all the contents of the address space of the old process will be copied to the address space of the new process. These contents include code and data (such as user temporary variables, User objects).

Therefore, the singleton class exists in the old process and only one object can exist, and it will also exist in the new process and only one object can exist. Moreover, these two objects are not the same object, which means that the scope of the uniqueness of the object in the singleton class is within the process, and is not unique between processes.

How to implement the only singleton thread?

We just mentioned that singleton objects are unique to a process, and a process can only have one singleton object. So how to implement a singleton that is unique to a thread?

Let's first take a look at what is the only singleton of a thread, and the difference between "thread only" and "process only".

"Process unique" refers to unique within a process, not unique among processes. By analogy, "thread uniqueness" refers to the uniqueness within a thread, and the threads may not be unique. In fact, "process unique" also means unique within and between threads. This is also the difference between "process unique" and "thread unique". This passage sounds a bit like a tongue twister, let me explain it with an example.

Assume that IdGenerator is the only singleton class for a thread. In thread A, we can create a singleton object a. Because the thread is unique, no new IdGenerator objects can be created in thread A, and threads can be non-unique. Therefore, in another thread B, we can also recreate a new singleton object b.

Although the concept is complicated to understand, the code implementation of the thread only singleton is very simple, as shown below. In the code, we use a HashMap to store objects, where key is the thread ID and value is the object. In this way, we can achieve that different threads correspond to different objects, and the same thread can only correspond to one object. In fact, the Java language itself provides the ThreadLocal tool class, which makes it easier to implement the thread unique singleton. However, the underlying implementation principle of ThreadLocal is also based on the HashMap shown in the following code.


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

  private static final ConcurrentHashMap<Long, IdGenerator> instances
          = new ConcurrentHashMap<>();

  private IdGenerator() {
    
    }

  public static IdGenerator getInstance() {
    
    
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }

  public long getId() {
    
    
    return id.incrementAndGet();
  }
}

How to implement a singleton in a cluster environment?

We just talked about the "process only" singleton and the "thread only" singleton. Now, let's look at the "cluster only" singleton.

First of all, let's first explain what is a "cluster only" singleton.

Let's compare it with "process only" and "thread only". "Process uniqueness" refers to the uniqueness within the process and not the uniqueness between processes. "Thread uniqueness" refers to being unique within a thread and not unique among threads. A cluster is equivalent to a collection of multiple processes. "Cluster unique" is equivalent to being unique within a process and unique between processes. In other words, different processes share the same object, and multiple objects of the same class cannot be created.

We know that the classic singleton model is unique within a process, so how to realize a singleton that is also unique between processes? If it is implemented strictly in accordance with the sharing of the same object between different processes, it is a bit difficult to implement the only singleton in the cluster.

Specifically, we need to serialize this singleton object and store it in an external shared storage area (such as a file). When the process uses this singleton object, it needs to read it from the external shared storage area to memory, deserialize it into an object, and then use it. After the use is complete, it needs to be stored back to the external shared storage area.

In order to ensure that only one copy of the object exists between processes at any time, after a process acquires the object, it needs to lock the object to prevent other processes from acquiring it again. After the process finishes using the object, it needs to explicitly delete the object from memory and release the lock on the object.

According to this idea, I used pseudo code to implement this process, as follows:


public class IdGenerator {
    
    
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略,比如文件地址*/);
  private static DistributedLock lock = new DistributedLock();
  
  private IdGenerator() {
    
    }

  public synchronized static IdGenerator getInstance() 
    if (instance == null) {
    
    
      lock.lock();
      instance = storage.load(IdGenerator.class);
    }
    return instance;
  }
  
  public synchroinzed void freeInstance() {
    
    
    storage.save(this, IdGeneator.class);
    instance = null; //释放对象
    lock.unlock();
  }
  
  public long getId() {
    
     
    return id.incrementAndGet();
  }
}

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

How to implement a multi-case pattern?

There is also a multi-case pattern corresponding to the concept of the singleton pattern. How to implement a multi-case pattern?

"Singleton" means that a class can only create one object. Correspondingly, "multiple instances" means that a class can create multiple objects, but the number is limited, for example, only 3 objects can be created. If you use code to give a simple example, it looks like this:


public class BackendServer {
    
    
  private long serverNo;
  private String serverAddress;

  private static final int SERVER_COUNT = 3;
  private static final Map<Long, BackendServer> serverInstances = new HashMap<>();

  static {
    
    
    serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
    serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
    serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
  }

  private BackendServer(long serverNo, String serverAddress) {
    
    
    this.serverNo = serverNo;
    this.serverAddress = serverAddress;
  }

  public BackendServer getInstance(long serverNo) {
    
    
    return serverInstances.get(serverNo);
  }

  public BackendServer getRandomInstance() {
    
    
    Random r = new Random();
    int no = r.nextInt(SERVER_COUNT)+1;
    return serverInstances.get(no);
  }
}

In fact, there is another way to understand the multi-case pattern: only one object of the same type can be created, and multiple objects of different types can be created. How to understand the "type" here?

Let's explain through an example, the specific code is as follows. In the code, the logger name is the "type" just mentioned. The object instances obtained by the same logger name are the same, and the object instances obtained by different logger names are different.


public class Logger {
    
    
  private static final ConcurrentHashMap<String, Logger> instances
          = new ConcurrentHashMap<>();

  private Logger() {
    
    }

  public static Logger getInstance(String loggerName) {
    
    
    instances.putIfAbsent(loggerName, new Logger());
    return instances.get(loggerName);
  }

  public void log() {
    
    
    //...
  }
}

//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");

The way of understanding this multi-case pattern is somewhat similar to the factory pattern. The difference between it and the factory pattern is that the objects created by the multi-case pattern are all objects of the same class, while the factory pattern creates objects of different subclasses. This point will be discussed in the next lesson. In fact, it is somewhat similar to the Flyweight model, and the difference between the two will be analyzed when we talk about the Flyweight model. In addition, in fact, the enumeration type is also equivalent to the multi-case mode, one type can only correspond to one object, and one class can create multiple objects.

Guess you like

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