ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

设计模式(五)—— 单例模式

2026/8/7 17:19:43 拓冰建站 浏览量
设计模式(五)—— 单例模式 一、为什么需要单例模式有一些对象我们只需要一个比如线程池连接池注册表对话框缓存等的对象。并且这些对象也只能有一个因为一旦出现多个就会导致许多问题产生。一是为了防止多线程各自去new再一个是防止单线程前面new一个了后面忘了又去new了一个所以有单例模式。饿汉模式在类被加载的时候就创建对象。但是如果这个对象十分浪费资源比如一个超级大的数组而程序在本次执行过程中刚好没用到这个对象不就浪费了吗优点无需处理同步问题(多线程时候)缺点占用资源懒汉模式第一次用到对象的时候再创建对象。优点节省资源缺点要处理同步问题(多线程时候)如何选择呢虽然两种模式各有优缺点但是根据之前的技术选型经验肯定有用的最多/无脑选择的一种就比如负载均衡有很多实现方式但最常用的是轮询。在这两种模式里用的最多的是懒汉模式。想一想就知道因为计算机领域很多地方都用到的懒汉模式比如web开发时的懒加载、再比如new ArrayList(10);的时候底层只会创建一个空数组Object[] emelentData {}只有当执行第一次add操作时底层才创建了长度为10的数组。那什么时候用饿汉模式呢? 比如你明确知道这个对象每次服务启动绝对会被创建那我不如让他随着类的加载而创建啦这样还不用操心同步问题了比如java.lang.Runtime的源码就是饿汉模式。问如何实现线程安全的单例模式饿汉模式。想一想就知道因为饿汉模式在类加载的时候就创建好了实例那么就不需要多线程创建实例了就不会有两个线程创建同一个实例的问题。但是改进版的懒汉模式也是安全的。二、单例模式推导懒汉把构造器设置为私有的是单例模式能够实现的关键所在。public class Singleton{ //唯一的对象 private static Singleton uniqueInstance; //私有的构造器这是单例模式的秘密所在 private Singleton(){} // public static Singleton getInstance(){ if(uniqueInstance null){ uniqueInstance new Singleton(); } return uniqueInstance; } }这段代码在单线程的时候是可行的但是多线程会出现问题。如果线程A和B同时进入了if(uniqueInstance null)判断语句那么就会创建两个对象。所以我们应该限制只能有一个线程进入getInstance方法也就是加锁2.1 实现方式一synchronizedpublic class Singleton{ //唯一的对象 private static Singleton uniqueInstance; //私有的构造器这是单例模式的秘密所在 private Singleton(){} // public static synchronized Singleton getInstance(){ if(uniqueInstance null){ uniqueInstance new Singleton(); } return uniqueInstance; } }好了这就是多线程下单例模式最笨重的实现方式了但起码线程安全2.2 实现方式二双重检测锁带着锁是很笨重的一个方法一旦加了synchronized以后执行的效率会比之前慢100倍。可以把synchronized搬到方法里面然后在外面再套一层uniqueInstance null的判断。这样以来我们加入线程A走到第一层uniqueInstance null判断时CPU把线程A调度走了把线程B调度来了线程B成功进入synchronized内部创建了uniqueInstance new Singelton()对象当CPU再次调度到线程A时A可以进入A能进入的原因是synchronized在这里锁的是对象而不是一整个代码块synchronized内部但是内部的uniqueInstance null判断不为空这依赖于volatile关键字把线程B对于uniqueInstance的修改及时刷新到主内存然后被A读走就无法创建第二个实例。除了加锁以外我们还需要加volatile关键字起到两个作用如果同时开启线程A和线程BuniqueInstance就属于两个线程的共享变量。如果线程A先使用了getInstance()方法获得实例应该马上刷回主内存让B能够获得这个变量否则B那边的uniqueInstance还是null。禁止指令重排。防止new Singleton()的指令从123-132。如果线程A明明已经创建好对象了但由于new Singleton()指令重排序先执行的指令3给对象赋值为null然后切到线程BB发现对象为null又创建一个。public class Singleton{ //唯一的对象 private static volatile Singleton uniqueInstance; //私有的构造器这是单例模式的秘密所在 private Singleton(){} //双重检测锁 public static Singleton getInstance(){ if(uniqueInstance null){ synchronized(Singleton.class){ if(uniqueInstance null){ uniqueInstance new Singleton(); } } } return uniqueInstance; } }2.3 实现方式三静态内部类这个比较完美生产代码一般用这个。静态内部类实现单例比双重检测锁更优雅、更安全。当 getInstance() 首次被调用时内部类才会被JVM 加载执行类的初始化阶段会获取一把类级别的初始化锁同一时刻只有一个线程能执行 clinit() 方法去初始化单例。这个过程是由 JVM 底层机制保证的代码层面无需任何 synchronized 或 volatilepublic class Singleton { //构造方法依然私有 private Singleton() { } //静态内部类 private static class SingletonInstance { private static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }2.4实现方式四枚举类使用枚举enum实现单例模式是《Effective Java》作者 Joshua Bloch 推荐的最佳方式。常见的单例写法懒汉式、双重检查锁、静态内部类等虽然能做到线程安全但面对以下两种攻击时单例性会被破坏反射攻击即使把构造器私有化仍可通过Constructor.setAccessible(true)强行创建新实例。序列化破坏反序列化时会绕过构造器生成新对象除非你精确实现readResolve()方法。public enum Singleton { INSTANCE; }2.5 特殊的懒汉模式1. java.util.Currency货币用的是懒汉模式但用的是特殊的同步机制并没有用到synchronized字段。他的思路是维护了一个全局层面的ConcurrentMap曾经创建过的每种货币对象都存在里面。就算两个线程同时进入了getInstance()方法同时调用了new Currency()构造方法但是put进ConcurrentMap的时候也会因为ConcurrentMap的key唯一性而put不进去这样就保证了同一种货币的实例在ConcurrentMap里只有一个。这个对于key唯一性判断的操作就相当于synchronized字段里面那一层对于if(uniqueInstance null)的判断。源码解析维护了一个ConcurrentMap三、饿汉模式饿汉模式很好理解既然我们想在类加载的阶段就创建出实例那么用static修饰实例变量就好啦class Single{ //用static修饰使得类加载的时候就创建实例 private static Single instance new Single(); //单例模式的核心private修饰构造器 private Single() { } public static Single getSingle() { return instance; } } public class Demo16 { public static void main(String[] args) { Single instance Single.getSingle(); } }