There are several ways to write the singleton pattern in the interview

Are there several ways to write the tangled singleton pattern useful? It is somewhat useful. In interviews, one or several of these writing methods are often chosen as the beginning of the conversation. While examining design patterns and coding styles, it is easy to expand to other questions. Here are some commonly used writing methods for monkeys, but don't try to memorize "the writing method of fennel beans". The greatest joy in programming lies in "know everything, control everything".

It can be roughly divided into 4 categories, and their basic forms, variants and characteristics are described below.

full man mode

Satisfaction is the singleton pattern with the most variants. We start from the rich man, and gradually understand the issues that need to be paid attention to when implementing the singleton pattern through its variants.

basic fat man

A full man means that he is full, he is not in a hurry to eat again, and he eats when he is hungry. So he does not initialize the singleton first, and then initializes it when it is used for the first time, that is, "lazy loading".

// 饱汉
// UnThreadSafe
public class Singleton1 {
  private static Singleton1 singleton = null;
  private Singleton1() {
  }
  public static Singleton1 getInstance() {
    if (singleton == null) {
      singleton = new Singleton1();
    }
    return singleton;
  }
}

The trouble to write is not a big problem, and the readability is good. Therefore, in a single-threaded environment, the basic fat man is the favorite writing method of the monkey. But in a multi-threaded environment, the basics are completely unavailable. The following variants are all attempts to solve the basic problem of thread unsafety. The core of the satan mode is lazy loading. The advantage is that it is faster to start, saves resources, and does not need to initialize the singleton until the instance is accessed for the first time; the minor disadvantage is that it is troublesome to write, the major disadvantage is that the thread is not safe, and the if statement has race conditions.

Full Man – Variant 1

The most brutal offense is to modify the getInstance() method with the synchronized keyword, which can achieve absolute thread safety.

// 饱汉
// ThreadSafe
public class Singleton1_1 {
  private static Singleton1_1 singleton = null;
  private Singleton1_1() {
  }
  public synchronized static Singleton1_1 getInstance() {
    if (singleton == null) {
      singleton = new Singleton1_1();
    }
    return singleton;
  }
}

Full Man – Variant 2

The advantage of variant 1 is that it is simple to write and absolutely thread-safe; the disadvantage is that the concurrency performance is extremely poor, in fact, it completely degenerates to serial. A singleton only needs to be initialized once, but even after initialization, the synchronized lock cannot be avoided, so getInstance() has completely become a serial operation. It is recommended to use in performance-insensitive scenarios.

Variant 2 is the "notorious" DCL 1.0.

In view of the problem that the lock cannot be avoided after the singleton initialization in variant 1, variant 2 has another layer of check in the outer layer of variant 1, plus the check in the inner layer of synchronized, which is the so-called "Double Check Lock" (Double Check Lock). , referred to as DCL).

// 饱汉
// UnThreadSafe
public class Singleton1_2 {
  private static Singleton1_2 singleton = null;
  private Singleton1_2() {
  }
  public static Singleton1_2 getInstance() {
    // may get half object
    if (singleton == null) {
      synchronized (Singleton1_2.class) {
        if (singleton == null) {
          singleton = new Singleton1_2();
        }
      }
    }
    return singleton;
  }
}


Reference: The role and principle of the volatile keyword . The core of variant 2 is DCL, and it seems that variant 2 has achieved the desired effect: lazy loading + thread safety. Sadly, as said in the comments, DCL is still thread-unsafe, and you may end up with "half an object" due to instruction reordering. After reading Variant 3 in detail, you can refer to a previous article by Monkey, which will not be repeated here.

Full Man – Variant 3

Variant 3 specifically targets Variant 2, which can be described as DCL 2.0.

In response to the "half object" problem of variant 3, variant 3 adds the volatile keyword to the instance. See the above reference for the principle.

// 饱汉
// ThreadSafe
public class Singleton1_3 {
  private static volatile Singleton1_3 singleton = null;
  private Singleton1_3() {
  }
  public static Singleton1_3 getInstance() {
    if (singleton == null) {
      synchronized (Singleton1_3.class) {
        // must be a complete instance
        if (singleton == null) {
          singleton = new Singleton1_3();
        }
      }
    }
    return singleton;
  }
}


Variant 3 is more suitable for performance-sensitive scenarios in Hungry Mode multi-threaded environment. But as we'll see later, there are still ways to break a singleton, even if it's thread-safe.

In contrast to the full man, the hungry man is very hungry and just wants to eat as soon as possible. So he initializes the singleton at the earliest opportunity, that is, when the class is loaded, and returns directly when accessing it later.

// 饿汉
// ThreadSafe
public class Singleton2 {
  private static final Singleton2 singleton = new Singleton2();
  private Singleton2() {
  }
  public static Singleton2 getInstance() {
    return singleton;
  }
}

The advantage of Hungry is that it is inherently thread-safe (thanks to the class loading mechanism), it is super simple to write, and there is no delay when using it; the disadvantage is that it may cause waste of resources (if the singleton is never used after the class is loaded).

It is worth noting that in a single-threaded environment, there is no difference in performance between a hungry man and a hungry man; but in a multi-threaded environment, because the hungry man needs to be locked, the performance of the hungry man is better.

Holder mode

We not only hope to take advantage of the convenience and thread safety of static variables in the hungry mode, but also hope to avoid resource waste through lazy loading. Holder mode satisfies these two requirements: the core is still static variables, which are convenient enough and thread-safe; the static Holder class holds real instances, which indirectly realizes lazy loading.

// Holder模式
// ThreadSafe
public class Singleton3 {
  private static class SingletonHolder {
    private static final Singleton3 singleton = new Singleton3();
    private SingletonHolder() {
    }
  }
  private Singleton3() {
  }
  public synchronized static Singleton3 getInstance() {
    return SingletonHolder.singleton;
  }
}


Compared with the Hungry Han mode, the Holder mode only increases the cost of a static inner class, which is equivalent to the Satisfied Man's variant 3 (slightly better), and is a more popular implementation. Also recommended for consideration.

Implementing the singleton pattern with enums is quite easy to use, but readability does not exist.

basic enumeration

Make the static member variable of the enum an instance of the singleton:

// 枚举
// ThreadSafe
public enum Singleton4 {
  SINGLETON;
}


Ugly but useful syntactic sugar with less code than Hungry pattern. But users can only directly access the instance Singleton4.SINGLETON - in fact, this access method is also appropriate to use as a singleton, but it sacrifices the advantages of static factory methods, such as the inability to achieve lazy loading.

Java's enums are an "ugly but useful syntactic sugar".

The essence of the enumerated singleton pattern

By decompiling (jad, optimization of source code | String splicing operation "+"? Also used) to open the syntactic sugar, we can see the essence of the enumeration type, which is simplified as follows:

// 枚举
// ThreadSafe
public class Singleton4 extends Enum<Singleton4> {
  ...
  public static final Singleton4 SINGLETON = new Singleton4();
  ...
}

Implementing some tricks with enums is essentially the same as the Hungry pattern, the only difference being the public static member variables.

This part has nothing to do with singletons and can be skipped. If you choose to read, be aware of the fact that while enums are quite flexible, how to use them properly can be difficult. A typical example that is simple enough is the TimeUnit class, and it is recommended to have time to read it patiently.

As you have seen above, the essence of an enumerated singleton is still an ordinary class. In fact, we can add anything that a normal class can do to an enumerated singleton. The point is that the initialization of an enumeration instance can be understood as instantiating an anonymous inner class. To be more obvious, we define a normal private member variable, a normal public member method, and a public abstract member method in Singleton4_1, as follows:

// 枚举
// ThreadSafe
public enum Singleton4_1 {
  SINGLETON("enum is the easiest singleton pattern, but not the most readable") {
    public void testAbsMethod() {
      print();
      System.out.println("enum is ugly, but so flexible to make lots of trick");
    }
  };
  private String comment = null;
  Singleton4_1(String comment) {
    this.comment = comment;
  }
  public void print() {
    System.out.println("comment=" + comment);
  }
  abstract public void testAbsMethod();
  public static Singleton4_1 getInstance() {
    return SINGLETON;
  }
}

In this way, each enumeration instance in the enumeration class Singleton4_1 not only inherits the member method print() of the parent class Singleton4_1, but also must implement the abstract member method testAbsMethod() of the parent class Singleton4_1.

Summarize

The above analysis ignores the problems of reflection and serialization. Through reflection or serialization, we can still access private constructors, create new instances and break the singleton pattern. At this time, only the enumeration mode can naturally prevent this problem. Reflection and serialization monkeys are not well understood, but the basic principles are not difficult and can be implemented manually on other patterns.

Let's continue to ignore the problems of reflection and serialization, and make a summary and aftertaste:

Method to realize key point waste of resources thread safety The performance of the multi-threaded environment is optimized enough
basic satiety lazy loading no no -
full man variant 1 Lazy loading, synchronization no Yes no
full man variant 2 Lazy loading, DCL no no -
full man variant 3 Lazy loading, DCL, volatile no Yes Yes
hungry man Static variable initialization Yes Yes Yes
Holder Static variable initialization, holder no Yes Yes
enumerate Enumeration essence, static variable initialization no Yes Yes

The singleton pattern is a common test point in interviews, and it is very simple to write. On the one hand, check the correctness, see the analysis of this article; on the other hand, check the coding style, refer to: Several basic rules that programmers should remember .

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325398174&siteId=291194637