设计模式之【单例模式】全解,单例模式实现方式,暴力打破单例模式与解决方案,你真的认识单例模式吗?

文章目录

全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我

什么是单例模式

单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

单例模式中的"单例"概念其实有些笼统,很多博文中只介绍了一个进程内单例模式。其实单例模式有很多种,线程单例、进程单例、还是集群单例?

接下来咱们一起来学习学习吧~

单例模式的应用场景

处理有线程冲突的资源

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

// 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());
  }
}

如上记录日志的方式,两个请求同时写同一个日志文件,完全有可能造成日志被覆盖的情况,log.txt应该是共享资源。

对于线程不安全的问题,我们通常情况下都是加一把锁,但是此处加锁明显不是最优解,最好的办法就是将日志类定义为单例:

java 复制代码
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());
  }
}

表示全局唯一类

比如说java的Runtime类就是使用饿汉式实现的单例,表示全局唯一,一个进程中只能存在一个对象。

java 复制代码
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

	// other code...
}

单例模式的实现方式

1、饿汉式之静态常量

饿汉式就是在JVM加载这个类的时候就直接创建出该单例对象。

java 复制代码
/**
 * 饿汉式单例模式(静态变量)
 * 1.构造器私有化
 * 2.本类内部创建对象实例
 * 3.提供一个公有的静态方法,返回实例对象
 **/
public class Hungry {

    private Hungry() {

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }
}

2、饿汉式之静态代码块

静态代码块的方式与静态常量的方式其实是一样的,都是在类加载的时候直接初始化。

java 复制代码
/**
 * 静态代码块饿汉式
 **/
public class Hungry2 {

    private Hungry2() {

    }

    private static Hungry2 uniqueInstance;

    // 在静态代码块中创建单例对象
    static {
        uniqueInstance = new Hungry2();
    }


    public static Hungry2 getInstance() {
        return uniqueInstance;
    }
}

有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我个人并不认同这样的观点。

如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

3、懒汉式之线程不安全方式(不推荐)

懒汉式其实就是懒加载的方式。

java 复制代码
/**
 **/
public class LazyMan{

    // 创建一个静态变量来记录Singeleton类的唯一实例
    private static LazyMan uniqueInstance;

    // 私有化构造器,保证只有Singelton类内才可以调用
    private LazyMan() {}

    public static LazyMan getInstance() {

        if (uniqueInstance == null) {
            uniqueInstance = new LazyMan();
        }

        return uniqueInstance;
    }
}

上面的代码我们很明显的可以看出,单线程下似乎没有什么问题,但是多线程下,多个线程同时执行到if (uniqueInstance == null) ,就有可能创建出多个实例。

4、懒汉式之加锁方式(不推荐)

java 复制代码
/**
 * 懒汉式单例模式,效率低
 **/
public class LazyMan2 {

    private static LazyMan2 uniqueInstance;

    private LazyMan2 () { }

    // 通过synchronized在静态方法上加锁,使得每个线程在进入这个方法前都要等待其他线程的离开
    public static synchronized LazyMan2 getInstance() {

        if (uniqueInstance == null) {

            uniqueInstance = new LazyMan2();
        }

        return uniqueInstance;
    }
}

虽然多线程下安全了,但是加入了synchronized 锁,每次获取对象都要加一把锁,严重降低了性能。

5、懒汉式之双重锁检查

java 复制代码
/**
 7. 懒汉式单例模式
 8. 双重检测锁: 效率高、线程安全且避免了内存浪费,但是不易理解(对新手不太友好)
 **/
public class LazyMan4 {
	
	// volatile关键字可以确保uniqueInstance变量被初始化成LazyMan4实例时,多个线程正确处理uniqueInstance变量。
    private volatile static LazyMan4 uniqueInstance;

    private LazyMan4() {
        System.out.println(Thread.currentThread().getName() + " is ok");
    }

    public static LazyMan4 getInstance() {
		
		// 判断后续线程是否需要继续加锁
        if (uniqueInstance == null) {

            // 试图通过减少同步代码块的方式提高效率
            synchronized (LazyMan4.class) {
                // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                if (uniqueInstance == null) {
                    uniqueInstance = new LazyMan4();
                }
            }
        }

        return uniqueInstance;
    }
}

该方式引入了volatile轻量级锁,相对于直接使用synchronized来说的确是提升了性能,并且只有第一次初始化的时候才会使用到synchronized ,后续只需要返回实例对象即可。

关于volatile的使用这里就不赘述了。

6、静态内部类方式

java 复制代码
public class Singleton5 {

    private static class SingletonHolder {
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    private Singleton5() {}

    public static Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }

} 

静态内部类方式可以轻松实现懒加载+线程安全(JVM类装载外部类的时候不会装载内部类)。

7、枚举

枚举天然就是一个单例的,也是Java四大名著中《Effective Java》里面的推荐写法。

java 复制代码
/**
 * 枚举自带单例模式
 **/
public enum EnumSingleton {

    INSTANCE;

    public static EnumSingleton getInstance() {

        return INSTANCE;
    }

}

我们来研究一下枚举的底层实现,我们在再一次点开枚举继承的抽象类Enum的底层源码,并且找到其中的valueOf()方法:

java 复制代码
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
	T result = enumType.enumConstantDirectory().get(name);
	if (result != null)
		return result;
	if (name == null)
		throw new NullPointerException("Name is null");
	throw new IllegalArgumentException(
		"No enum constant " + enumType.getCanonicalName() + "." + name);
}

我们继续看这一行代码:

java 复制代码
T result = enumType.enumConstantDirectory().get(name);
java 复制代码
Map<String, T> enumConstantDirectory() {
	if (enumConstantDirectory == null) {
		T[] universe = getEnumConstantsShared();
		if (universe == null)
			throw new IllegalArgumentException(
				getName() + " is not an enum type");
		Map<String, T> m = new HashMap<>(2 * universe.length);
		for (T constant : universe)
			m.put(((Enum<?>)constant).name(), constant);
		enumConstantDirectory = m;
		}
	return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;

这个时候我们会发现枚举常量字典enumConstantDirectory为Map<String, T>类型,其中key为String类型,而value是一个泛型对象。其中key就是由我们自定义的,如上文中的INSTANCE;。所以,枚举是通过这个String类型的key,去拿到这个value,这才保证了单例模式的实现。但是我们发现了枚举常量字典中的常量二字。既然是常量的话,那么就意味着在类加载的时候就会赋值。这个时候,我们尴尬的发现,我们又回到了最初的起点 ---> 饿汉式单例模式,JVM加载类的时候就初始化完成了。

8、Spring IOC容器

使用Spring容器可以完美实现单例模式。

大致的逻辑参考如下:

java 复制代码
/**
 * 注册式单例,Spring中的做法
 **/
public class ContainerSingleton {

    // 私有化构造器
    private ContainerSingleton() {

    }

    // 声明一个Map
    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                // 如果map中不存在这个全限定类名的key,那么需要放入新的数据
                Object obj = null;
                try {
                    // 利用全限定类名获取反射对象,再实现类的实例化
                    obj = Class.forName(className).newInstance();
                    // 把全限定类名以及对象,以key-vaule的形式放入map中
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                // 如果map中存在这个全限定类名的key,直接通过这个key返回对应的对象
                return ioc.get(className);
            }
        }
    }
}

我们在Spring中使用起来也非常的方便,具体请查阅Spring中Bean的创建方式。

9、使用CAS实现

java 复制代码
public class Singleton9 {

    private static final AtomicReference<Singleton9> INSTANCE = new AtomicReference<Singleton9>();

    private Singleton9() {
    }

    public static Singleton9 getInstance() {
        for (; ; ) {
            Singleton9 current = INSTANCE.get();

            if (current != null) {
                return current;
            }

            current = new Singleton9();

            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

}

该方式用的比较少,写法比较麻烦,但是也算是一种方式。

暴力打破单例模式的方式

1、反射打破饿汉式的方式

java 复制代码
import java.lang.reflect.Constructor;
 
/**
 * 反射:程序运行阶段,获取某一个类的所有属性和方法
 * 所以 反射是对单例模式起到破坏的作用
 * 下面以饿汉式为例,进行演示
 */
public class DestroySingleton {
    public static void main(String [] args){
        /*反射对单例模式的破坏*/
        //1、获取类对象
        Class<Singleton_1> singleton_1Class = Singleton_1.class;
        //2、获取私有的构造方法
        try {
            Constructor<Singleton_1> declaredConstructor = singleton_1Class.getDeclaredConstructor();
            //3、取消Java语言的访问检查 暴力访问
            declaredConstructor.setAccessible(true);
            //4、通过构造 创建对象
            Singleton_1 singleton_1 = declaredConstructor.newInstance();
            Singleton_1 singleton_2 = declaredConstructor.newInstance();
            System.out.println(singleton_1 == singleton_2);
 
        } catch (Exception e) {
            e.printStackTrace();
        }
 
 
    }
}
//饿汉式 单例模式
class Singleton_1{
    //构造方法私有
    private Singleton_1(){
        
    };
    //属性私有
    private static Singleton_1 singleton_1 = new Singleton_1();
    //提供对外的访问方法
    public static Singleton_1 getInstance(){
        return singleton_1;
    }
}

我们可以发现以上代码可以打破单例模式。

如何解决这种情况呢?我们可以在私有构造方法中加入判断:

java 复制代码
//饿汉式 单例模式
class Singleton_1{
    //构造方法私有
    private Singleton_1(){
        //防止反射对单例模式的破坏
        if (singleton_1 != null){
            throw new RuntimeException("不允许反射访问。。。");
        }
    };
    //属性私有
    private static Singleton_1 singleton_1 = new Singleton_1();
    //提供对外的访问方法
    public static Singleton_1 getInstance(){
        return singleton_1;
    }
}

再次通过反射创建单例的时候,会直接抛出异常了~

2、通过序列化打破饿汉式的方式

java 复制代码
import java.io.*;
public class DestroySingleton {
    public static void main(String [] args){
        /*序列化对单例模式的破坏*/
        Singleton_1 s1 = null;
        Singleton_1 s2 = Singleton_1.getInstance();

        FileOutputStream fos = null;
        try {

            fos = new FileOutputStream("E:\\Singleton_1.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("E:\\Singleton_1.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (Singleton_1) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }

 
    }
}
//饿汉式 单例模式,序列化破解的方式只有实现Serializable才可以
class Singleton_1 implements Serializable {
    //构造方法私有
    private Singleton_1(){
        //防止反射对单例模式的破坏,为了线程安全可以引入synchronized 锁定这里
        if (singleton_1 != null){
            throw new RuntimeException("不允许反射访问。。。");
        }
    };
    //属性私有
    private static Singleton_1 singleton_1 = new Singleton_1();
    //提供对外的访问方法
    public static Singleton_1 getInstance(){
        return singleton_1;
    }
}

我们发现,序列化方式打破单例更加暴力,即使在构造方法抛出异常也不能规避。

如何解决呢?我们只要加上readResolve()方法即可,来看优化后的代码:

java 复制代码
//饿汉式 单例模式,序列化破解的方式只有实现Serializable才可以
class Singleton_1 implements Serializable {
    //构造方法私有
    private Singleton_1(){
        //防止反射对单例模式的破坏
        if (singleton_1 != null){
            throw new RuntimeException("不允许反射访问。。。");
        }
    };
    //属性私有
    private static Singleton_1 singleton_1 = new Singleton_1();
    //提供对外的访问方法
    public static Singleton_1 getInstance(){
        return singleton_1;
    }

    // 具体原理就是 ObjectInputStream类的readObject()方法,感兴趣可以研究研究。
    private Object readResolve(){
        return singleton_1;
    }
}

虽然增加了readResolve()方法返回实例解决了单例模式破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有返回而已,如果创建对象的动作发生频率加快,就意味着内存分配也会随之增大。

3、通过反射打破懒汉式的方式

上面我们通过反射打破饿汉式,通过在构造方法抛异常的方式可以解决。

那么这种方式可以解决懒汉式的这种问题吗?

java 复制代码
import java.lang.reflect.Constructor;

public class DestroySingleton {
    public static void main(String[] args) {
        /*反射对单例模式的破坏*/
        //1、获取类对象
        Class<Singleton_1> singleton_1Class = Singleton_1.class;
        //2、获取私有的构造方法
        try {
            Constructor<Singleton_1> declaredConstructor = singleton_1Class.getDeclaredConstructor();
            //3、取消Java语言的访问检查 暴力访问
            declaredConstructor.setAccessible(true);
            //4、通过构造 创建对象
            Singleton_1 singleton_1 = declaredConstructor.newInstance();
            Singleton_1 singleton_2 = declaredConstructor.newInstance();
            System.out.println(singleton_1 == singleton_2);

        } catch (Exception e) {
            e.printStackTrace();
        }


    }
}

//懒汉式 单例模式
class Singleton_1 {
    //构造方法私有
    private Singleton_1() {
        //防止反射对单例模式的破坏
        if (singleton_1 != null) {
            throw new RuntimeException("不允许反射访问。。。");
        }
    }

    //属性私有
    private volatile static Singleton_1 singleton_1;

    //提供对外的访问方法
    public static Singleton_1 getInstance() {
        // 判断后续线程是否需要继续加锁
        if (singleton_1 == null) {

            // 试图通过减少同步代码块的方式提高效率
            synchronized (Singleton_1.class) {
                // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                if (singleton_1 == null) {
                    singleton_1 = new Singleton_1();
                }
            }
        }

        return singleton_1;
    }
}

我们发现执行结果是false,并不会解决这个问题。

要想彻底不想通过反射打破懒汉式的单例,解决起来还是很麻烦的,这里就不深入追究了(可以加个内部变量等方法)。

4、通过序列化打破懒汉式的方式

序列化方式打破懒汉式也是很暴力,

解决办法跟饿汉式一样,加一个readResolve方法。

java 复制代码
private Object readResolve(){
    return singleton_1;
}

5、通过反射打破静态内部类的方式

java 复制代码
import java.lang.reflect.Constructor;

/**
 * 静态内部类
 * 1.类装载的时候,静态内部类是不会被装载(懒加载,以外部类的装载不会导致内部类的装载)
 * 2.当调用getInstance()方法的时候,会导致静态内部类被装载,而且只会被装载一次
 * 3.在装载的时候线程是安全的。(JVM底层类装载机制)
 **/
public class Holder {
	
	// 私有化构造器
    private Holder() {

    }
	
	// 提供一个全局访问点,返回静态内部类中的静态常量
    public static Holder getInstance() {
        return InnerClass.HOLDER;
    }
	
	// 在静态内部类中创建一个静态常量并将一个外部类的实例赋值给它。
    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }

    public static void main(String[] args) throws Exception {
        // 用提供的唯一全局访问点获取实例对象
        Holder instance = Holder.getInstance();
        // 获取Holder的反射对象
        Class<Holder> clazz = Holder.class;
        // 通过反射对象获取Holder的构造器
        Constructor<Holder> declaredConstructor = clazz.getDeclaredConstructor();
        // 私有访问授权
        declaredConstructor.setAccessible(true);
        // 创建Holder的实例对象
        Holder instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);
    }

}

我们发现可以使用反射轻松打破。

此时我们可以在私有构造方法中加判断:

java 复制代码
public class Holder {
	
	// 私有化构造器
    private Holder() {
        // 为了线程安全,可以加synchronized锁
        if (InnerClass.HOLDER != null) {
            throw new RuntimeException("小朋友,不要试图用反射搞破坏!");
        }
    }
	
	// 提供一个全局访问点,返回静态内部类中的静态常量
    public static Holder getInstance() {
        return InnerClass.HOLDER;
    }
	
	// 在静态内部类中创建一个静态常量并将一个外部类的实例赋值给它。
    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }

    public static void main(String[] args) throws Exception {
        // 获取Holder的反射对象
        Class<Holder> clazz = Holder.class;
        // 通过反射对象获取Holder的构造器
        Constructor<Holder> declaredConstructor = clazz.getDeclaredConstructor();
        // 私有访问授权
        declaredConstructor.setAccessible(true);
        // 创建Holder的实例对象
        Holder instance2 = declaredConstructor.newInstance();
        Holder instance3 = declaredConstructor.newInstance();
        System.out.println(instance3 == instance2);
    }

}

这种写法虽然干脆利落,却直接封杀了反射的可能性,甚至通过反射来第一次获取单例对象都不可以了,只能通过getInstance方法来获取。

6、通过反射破坏枚举

java 复制代码
public enum EnumSingleton {

    INSTANCE;

    public static EnumSingleton getInstance() {

        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        // 获取枚举EnumSingleton的反射对象
        Class<EnumSingleton> clazz = EnumSingleton.class;
        // 利用反射对象获取EnumSingleton的构造器
        Constructor<EnumSingleton> declaredConstructor = clazz.getDeclaredConstructor();
        // 私有访问授权
        declaredConstructor.setAccessible(true);
        // 利用反射获得的构造器实现EnumSingleton的实例化
        EnumSingleton instance = declaredConstructor.newInstance();
    }

}

我们执行一下发现,竟然直接报错了!

查看反射创建对象newInstance()方法的底层源码:

java 复制代码
    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

关键代码:

java 复制代码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

但是,为什么提示我们java.lang.NoSuchMethodException呢?

我们查看一下枚举继承的抽象类Enum的底层源码,发现其中会有这么一个带双参的构造方法,而不是无参:

java 复制代码
 protected Enum(String name, int ordinal) {
	this.name = name;
	this.ordinal = ordinal;
}

哦,那我们利用反射获取这个双参的构造方法就好了。再次修改代码如下:

java 复制代码
public static void main(String[] args) throws Exception {
    // 获取枚举EnumSingleton的反射对象
    Class<EnumSingleton> clazz = EnumSingleton.class;
    // 利用反射对象获取EnumSingleton的构造器
    Constructor<EnumSingleton> declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
    // 私有访问授权
    declaredConstructor.setAccessible(true);
    // 利用反射获得的构造器实现EnumSingleton的实例化
    EnumSingleton instance = declaredConstructor.newInstance("ccc", 666);
}

获取到我们想要的异常啦!

所以,枚举实现起来又方便又安全,推荐这种方式!

如何实现线程唯一

"进程唯一"指的是进程内唯一,进程间不唯一。类比一下,"线程唯一"指的是线程内唯一,线程间可以不唯一。实际上,"进程唯一"还代表了线程内、线程间都唯一,这也是"进程唯一"和"线程唯一"的区别之处。

本文以上都是实现的是"进程唯一",那如何实现"线程唯一"呢?

java 复制代码
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();
  }
}

以上代码是使用Map,我们都知道每一个线程都有一个唯一的id,我们用key为线程的Id,就可以实现线程唯一的单例啦!

还有一种方式是,ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间换时间"的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

java 复制代码
public class Singleton8 {

    private static final ThreadLocal<Singleton8> tlSingleton = new ThreadLocal<Singleton8>() {
        @Override
        protected Singleton8 initialValue() {
            return new Singleton8();
        }
    };

    private Singleton8() {}

    public static Singleton8 getInstance() {
        return tlSingleton.get();
    }
    
}

实现多集群下的单例

那恐怕只能使用redis、数据库等,将单例对象存放在公共的资源中了。

实现一个多例模式

"单例"指的是,一个类只能创建一个对象。对应地,"多例"指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。如果用代码来简单示例一下的话,就是下面这个样子:

java 复制代码
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);
  }
}

实际上,对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。这里的"类型"如何理解呢?

我们还是通过一个例子来解释一下,具体代码如下所示。在代码中,logger name 就是刚刚说的"类型",同一个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。

java 复制代码
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");

这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。实际上,它还有点类似享元模式,两者的区别等到我们讲到享元模式的时候再来分析。除此之外,实际上,枚举类型也相当于多例模式,一个类型只能对应一个对象,一个类可以创建多个对象。

参考资料

blog.csdn.net/Qizhi_Hu/ar...

王争老师《设计模式之美》
blog.csdn.net/mnimxq/arti...

相关推荐
爱勇宝3 分钟前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
AskHarries19 分钟前
工具失败时怎么办:重试、回滚、人工确认和风险提示
后端·程序员
苏三说技术2 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎3 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode3 小时前
Redis 在生产项目的使用
前端·后端
用户559822481223 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode3 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战3 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha3 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn3 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端