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

news2025/2/23 18:47:08

文章目录

  • 什么是单例模式
  • 单例模式的应用场景
    • 处理有线程冲突的资源
    • 表示全局唯一类
  • 单例模式的实现方式
    • 1、饿汉式之静态常量
    • 2、饿汉式之静态代码块
    • 3、懒汉式之线程不安全方式(不推荐)
    • 4、懒汉式之加锁方式(不推荐)
    • 5、懒汉式之双重锁检查
    • 6、静态内部类方式
    • 7、枚举
    • 8、Spring IOC容器
    • 9、使用CAS实现
  • 暴力打破单例模式的方式
    • 1、反射打破饿汉式的方式
    • 2、通过序列化打破饿汉式的方式
    • 3、通过反射打破懒汉式的方式
    • 4、通过序列化打破懒汉式的方式
    • 5、通过反射打破静态内部类的方式
    • 6、通过反射破坏枚举
  • 如何实现线程唯一
  • 实现多集群下的单例
  • 实现一个多例模式
  • 参考资料

什么是单例模式

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

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

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

单例模式的应用场景

处理有线程冲突的资源


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应该是共享资源。

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


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类就是使用饿汉式实现的单例,表示全局唯一,一个进程中只能存在一个对象。

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加载这个类的时候就直接创建出该单例对象。

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

    private Hungry() {

    }

    private final static Hungry HUNGRY = new Hungry();

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

2、饿汉式之静态代码块

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

/**
 * 静态代码块饿汉式
 **/
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、懒汉式之线程不安全方式(不推荐)

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

/**
 **/
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、懒汉式之加锁方式(不推荐)

/**
 * 懒汉式单例模式,效率低
 **/
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、懒汉式之双重锁检查

/**
 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、静态内部类方式

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》里面的推荐写法。

/**
 * 枚举自带单例模式
 **/
public enum EnumSingleton {

    INSTANCE;

    public static EnumSingleton getInstance() {

        return INSTANCE;
    }

}

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

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

我们继续看这一行代码:

T result = enumType.enumConstantDirectory().get(name);
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容器可以完美实现单例模式。

大致的逻辑参考如下:

/**
 * 注册式单例,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实现

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、反射打破饿汉式的方式

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;
    }
}

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

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

//饿汉式 单例模式
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、通过序列化打破饿汉式的方式


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()方法即可,来看优化后的代码:

//饿汉式 单例模式,序列化破解的方式只有实现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、通过反射打破懒汉式的方式

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

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


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方法。

private Object readResolve(){
    return singleton_1;
}

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

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

}

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

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

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、通过反射破坏枚举

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()方法的底层源码:

    @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;
    }

关键代码:

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

但是,为什么提示我们java.lang.NoSuchMethodException呢?
我们查看一下枚举继承的抽象类Enum的底层源码,发现其中会有这么一个带双参的构造方法,而不是无参:

 protected Enum(String name, int ordinal) {
	this.name = name;
	this.ordinal = ordinal;
}

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

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

获取到我们想要的异常啦!
在这里插入图片描述
所以,枚举实现起来又方便又安全,推荐这种方式!

如何实现线程唯一

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

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

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采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

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 个对象。如果用代码来简单示例一下的话,就是下面这个样子:


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 获取到的对象实例是不同的。


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");

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

参考资料

https://blog.csdn.net/Qizhi_Hu/article/details/106451236
王争老师《设计模式之美》
https://blog.csdn.net/mnimxq/article/details/124526216

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

波司登的高端化后遗症

&#xff08;题图&#xff09; 文|螳螂观察 作者| 青月 受“三重”拉尼娜现象的影响&#xff0c;2022年冬天可能会因为阶段性冷空气的影响出现阶段性低温&#xff0c;且极端寒潮爆发的可能性大。 极端天气越来越多&#xff0c;年年冷冬&#xff0c;有望催化以羽绒服为代表的…

【uni-app从入门到实战】打包

小程序打包发布 1、小程序的打包发布很简单&#xff0c;只需要将程序运行到微信开发者工具中&#xff0c;然后点击右上角的上传按钮即可 我们这里的上传按钮不能点击是因为没有配置微信小程序AppID 打开项目的 manifest.json&#xff0c;选中微信小程序配置&#xff0c;填入微…

Java并发编程——线程间通信

线程间通信一、volatile 关键字二、等待/通知机制三、管道通信四、Thread.join一、volatile 关键字 为什么volatile关键字可以&#xff1f;因为之前说过了&#xff0c;此关键字能保证变量的可见性&#xff0c;也就是说变量一旦被修改&#xff0c;立马能被其他线程所感知 例子如…

拓端tecdat|R语言代做泰坦尼克号随机森林模型案例数据分析

全文链接&#xff1a;http://tecdat.cn/?p4281 原文出处&#xff1a;拓端数据部落公众号 视频&#xff1a;从决策树到随机森林&#xff1a;R语言信用卡违约分析信贷数据实例 从决策树到随机森林&#xff1a;R语言信用卡违约分析信贷数据实例&#xff0c;时长10:11 如果我们对…

Linux:环境变量

基本概念 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。 环境变量通常具有某些特殊用途&#xff0c;通常具有全局特性&#xff0c;可以被子进程继承下去 常见的环境变量 PATH : 指定命令的搜索路径 HOME : 指定用户的主工作目录(即用户登陆到Linux系统中…

[Linux]----文件操作(复习C语言+文件描述符)

文章目录前言一、基础概念二、回顾C语言2.1 对文件进行写操作2.2 追加写文件2.3 读文件2.4 简易cat功能总结stdin&stdout&stderr打开文件的方式三、系统文件I/O接口介绍open介绍使用open接口closewriteread四、文件描述符先验证0,1,2就是标准的IO标准输入流标准输出流标…

基于寄生-捕食算法的函数寻优算法

文章目录一、理论基础1、寄生-捕食算法&#xff08;1&#xff09;初始化&#xff08;2&#xff09;筑巢阶段(鸟窝)&#xff08;3&#xff09;寄生阶段(乌鸦-布谷鸟)&#xff08;4&#xff09;捕食阶段(乌鸦-猫)2、PPA算法伪代码二、仿真实验与结果分析三、参考文献一、理论基础…

QCC51XX---QACT用户指南

更新记录链接:QCC51XX---系统学习目录_嵌入式学习_force的博客-CSDN博客 QACT安装包不要放在有中文路径下,否则—直会安装报错。适用V7,V7.1 V7.2版本 打开QACT. 打开QACT. 点击 connection configuration 进去之后 点击1,然后点2,选择kalaccess.dll文件, workspace …

【C++】vector的模拟实现不会怎么办?看过来

&#x1f308;欢迎来到C专栏~~vector的模拟实现 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort&#x1f393;&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤&#x1f914;&…

MySQL是如何保证主从一致的

一&#xff1a;什么是binlog Binary log(二进制日志)&#xff0c;简称Binlog。 Binlog是记录所以数据表结构变更以及表数据修改的二进制日志&#xff0c;不会记录select和show这类操作。Binlog是以事件形式记录&#xff0c;还包括语句所执行的消耗时间。Binlog是MySql Server自…

0082 时间复杂度,冒泡排序

/* * 排序也称排序算法&#xff08;Sort Algorithm&#xff09; * 排序是将一组数据&#xff0c;依指定的顺序进行排列的过程。 * * 排序分类 * 1.内部排序&#xff1a;将需要处理的所有数据都加载到内存存储器中进行排序&#xff08;使用内存&#xff09; * 插…

Keil MDK的sct分散加载文件详解

sct 分散加载文件简介 MDK 生成一个以工程名命名的后缀为 *.sct 的分散加载文件 (Linker Control File&#xff0c;scatter loading)&#xff0c;链接器根据该文件的配置分配各个节区地址&#xff0c;生成分散加载代码&#xff0c;因此我们通过修改该文件可以定制具体节区的存…

Spring源码:Spring源码阅读环境搭建

本篇内容包括&#xff1a;Mac 环境下 gradle 的安装和配置、源码克隆、新建测试类&#xff0c;测试Spring源码 等内容&#xff01; 第一步&#xff1a;Mac 环境下 gradle 的安装和配置 1、下载安装包 # 到 GitHub 的 Spring 仓库选定 Spring 版本&#xff0c;查看对应版本 Sp…

Linux项目自动化构建工具make/makefile

1.背景 会不会写makefile&#xff0c;从一个侧面说明了一个人是否具备完成大型工程的能力一个工程中的源文件不计其数&#xff0c;其按类型&#xff0c;功能&#xff0c;模块分别放在若干目录中&#xff0c;makefile定义了一系列的规则来制定&#xff0c;那些文件需要先编译&a…

C艹笔记--面向对象程序设计

文章目录类与对象简介类与结构的区别定义成员函数继承继承小总结[C中::和:&#xff0c; .和->的作用和区别](https://zhuanlan.zhihu.com/p/165992745)符号::和&#xff1a;的作用和区别:::一般用来表示继承符号.和->的作用和区别#include#include""和#include…

STM32入门——基本 GPIO 的输出控制

文章目录1 什么是 GPIO &#xff1f;1.1 GPIO 简介1.2 GPIO 硬件解析1.2.1 保护二极管1.2.2 P-MOS、N-MOS 管1.2.3 数据输入输出寄存器1.2.4 复用功能输出1.2.5 模拟输入输出1.3 GPIO 的工作模式1.3.1 输入模式 (模拟/浮空/上拉/下拉)1.3.2 输出模式 (推挽/开漏)1.3.3 复用功能…

基于Nodejs+vue开发实现酒店管理系统

作者简介&#xff1a;Java、前端、Pythone开发多年&#xff0c;做过高程&#xff0c;项目经理&#xff0c;架构师 主要内容&#xff1a;Java项目开发、毕业设计开发、面试技术整理、最新技术分享 项目编号&#xff1a;BS-QD-KS-002 一&#xff0c;项目简介 本项目使用纯前端技…

mysql约束

文章目录mysql约束非空约束唯一性约束主键约束使用自增列&#xff1a;AUTO_INCREMENTFOREIGN KEY约束CHECK约束mysql约束 为什么需要约束&#xff1f;为了保证数据的完整性什么叫约束&#xff1f;对表中字段的限制约束的分类&#xff1a; 角度1&#xff1a;约束的字段个数&…

吴峰光杀进 Linux 内核

【编者按】吴峰光&#xff0c;Linux 内核守护者&#xff0c;学生时代被同学戏称为“老神仙”&#xff0c;两耳不闻窗外事&#xff0c;一心只搞 Linux。吴峰光的 Linux 内核之路&#xff0c;是天赋、兴趣、耐心、坚持的综合&#xff0c;这从一个补丁前后迭代了 16 个版本后还进行…

【初识Netty使用Netty实现简单的客户端与服务端的通信操作Netty框架中一些重要的类以及方法的解析】

一.Netty是什么&#xff1f; Netty 由 Trustin Lee(韩国&#xff0c;Line 公司)2004 年开发 本质&#xff1a;网络应用程序框架 实现&#xff1a;异步、事件驱动 特性&#xff1a;高性能、可维护、快速开发 用途&#xff1a;开发服务器和客户端 Netty的性能很高&#xff0…