单利模式(singleton)
定义
单例模式保证了全局对象的唯一性,确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这种模式主要用于当你想控制资源访问,避免创建多个实例导致资源浪费或状态不一致的情况。
比如系统启动读取配置文件就需要单例保证配置的一致性。
特点
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。
单例类必须给所有其他对象提供这一实例。
注意事项
在获取单例的时候,要保证不能产生多个实例对象,getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
在使用单例对象的时候,要注意单例对象内的实例变量是会被多线程共享的,推荐使用无状态的对象,不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。
解决的问题
要求生产唯一序列号。
WEB中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
需要频繁的进行创建和销毁的对象。
创建对象时耗时过多或耗费资源过多,但又经常用到的对象。
工具类对象
Windows的Task Manager(任务管理器)。
windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
角色
单例方法的实现
1. 饿汉式(静态常量)
public class Singleton {
//定义一个唯一实例,实例静态代表类加载的时候就创建了对象
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
//获取实例
public static Singleton getInstance(){
return INSTANCE;
}
}
优点
这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点
在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
2. 饿汉式(静态代码块)
public class Singleton {
//声明(静态代码块只能使用静态变量)
private static Singleton instance;
//静态代码块在类加载器装载时创建对象
static {
instance = new Singleton();
}
private Singleton() {}
//返回实例对象
public static Singleton getInstance() {
return instance;
}
}
3. 懒汉式(线程不安全)
public class Singleton {
//声明变量
private static Singleton singleton;
private Singleton() {}
//返回实例对象
public static Singleton getInstance() {
//判断该对象是否被创建
if (singleton == null) {
//创建对象
singleton = new Singleton();
}
return singleton;
}
}
这种写法起到了Lazy Loading的效果, 但是只能在单线程下使用。 如果在多线程下, 一个线程进入了if (singleton == null)判断语句块, 还未来得及往下执行, 另一个线程也通过了这个判断语句, 这时便会产生多个实例。 所以在多线程环境下不可使用这种方式。
4. 懒汉式(线程安全,同步方法)
public class Singleton {
//声明变量
private static Singleton singleton;
private Singleton() {}
//在获取对象方法上添加synchronized关键字,保证线程安全
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
解决了上第三种实现方式的线程不安全问题, 添加了synchronized关键字,保证线程安全,是线程同步。
缺点
这种方法效率太低了,每个线程在想获得类的实例时候, 执行getInstance()方法都要进行同步。 而其实这个方法只执行一次实例化代码就够了, 后面的想获得该类实例, 直接return就行了, 方法进行同步效率太低要改进。
5. 懒汉式(线程安全,同步代码块,synchronized锁方法,不建议使用)
- 构造器私有化
- 类的内部创建对象
- 向外暴露一个静态的公共方法,加入同步处理的代码块
public class Singleton {
// 1、构造器私有化
private Singleton() {
}
// 2、类的内部声明对象
private static Singleton instance;
// 3、向外暴露一个静态的公共方法,加入同步处理的代码,解决线程安全问题
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
优缺点:
这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块 但是这种同步并不能起到线程同步的作用。跟第4种实现方式遇到的情形一致,假如一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例 结论:在实际开发中,不能使用这种方式
6. 懒汉式(双重检查)
- 构造器私有化
- 类的内部创建对象,同时用volatile关键字修饰修饰
- 向外暴露一个静态的公共方法,加入同步处理的代码块,并进行双重判断,解决线程安全问题
public class Singleton {
//定义静态的变量
private static Singleton singleton;
private Singleton() {}
//获取对象
public static Singleton getInstance() {
//检查是否被创建
if (singleton == null) {
synchronized (Singleton.class) {
//再次进行检查
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
Double-Check概念对于多线程开发者来说不会陌生, 如代码中所示,我们进行了两次if (singleton == null)检查, 这样就可以保证线程安全了。 这样,实例化代码只用执行一次, 后面再次访问时,判断if (singleton == null), 直接return实例化对象。
7. 静态内部类
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。 两者都是采用了类装载的机制来保证初始化实例时只有一个线程。 不同的地方在饿汉式方式是只要Singleton类被装载就会实例化, 没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化, 而是在需要实例化时,调用getInstance方法, 才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化, 所以在这里, JVM帮助我们保证了线程的安全性, 在类进行初始化时, 别的线程是无法进入的。
8. 枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
这种方式是Effective Java作者Josh Bloch 提倡的方式, 它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象, 可谓是很坚强的壁垒啊, 在深度分析Java的枚举类型----枚举的线程安全性及序列化问题中有详细介绍枚举的线程安全问题和序列化问题, 不过,个人认为由于1.5中才加入enum特性, 用这种方式写不免让人感觉生疏, 在实际工作中,我也很少看见有人这么写过。
枚举类型更好,但是枚举类型会造成更多的内存消耗。枚举会比使用静态变量多消耗两倍的内存,如果是Android应用,尽量避免。原因的话,是因为枚举类型会在编译时转化为一个类,会涉及很多复杂的操作.
9. CAS方式
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton> ();
private Singleton() {}
public static Singleton getInstance() {
for(;;) {
Singleton instance = INSTANCE.get();
if(instance != null) {
return instance;
}
instance = new Singleton();
if(INSTANCE.compareAndSet(null, instance)) {
return instance;
}
}
}
}
优点
不需要使用传统的锁机制来保证线程安全,CAS 是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
缺点
如果忙等待一直执行不成功(一直在死循环中),会对 CPU 造成较大的执行开销。而且,这种写法如果有多个线程同时执行 singleton = new Singleton(); 也会比较耗费堆内存。
10. Lock机制
// 类似双重校验锁写法
public class Singleton {
private static Singleton instance = null;
private static Lock lock = new ReentrantLock();
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
lock.lock(); // 显式调用,手动加锁
if(instance == null) {
instance = new Singleton();
}
lock.unlock(); // 显式调用,手动解锁
}
return instance;
}
}
比较
| 单例模式 | 是否推荐 | 懒加载 | 反序列化单例 | 反射单例 | 克隆单例 |
|---|---|---|---|---|---|
| 饿汉模式 | 推荐 | ✘ | ✘ | ✘ | ✘ |
| 懒汉模式 | ✘ | ✔️ | ✘ | ✘ | ✘ |
| 枚举 | 推荐 | ✔️ | ✔️ | ✔️ | ✔️ |
| 静态内部类 | 推荐 | ✔️ | ✘ | ✘ | ✘ |
| 双重校验锁 | 可用 | ✔️ | ✘ | ✘ | ✘ |
总结
有两个问题需要注意:
1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。单例与序列化的那些事儿
对第一个问题修复的办法是:
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
对第二个问题修复的办法是:
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {}
private Object readResolve() {
return INSTANCE;
}
}
创建型模式
单例模式
饿汉式:
静态变量
静态代码块
懒汉式:
线程不安全
线程安全 双检锁
内部类
枚举
静态内部类式:利用JVM的特性,即外部类被加载时并不会去创建内部类的实例对象,只有当我们调用这个获取实例对象方法的时候,内部类才会被加载,这个时候才会去初始化它的这个静态成员去创建我们的实例对象。这种方式不需要加锁,线程安全由JVM保证。
枚举式:通过Java枚举类型本身的特性,保证了创建时的线程安全,并且简单易用。
枚举式单例模式相比其他实现方式具有以下优势:
简洁性和易用性:使用枚举实现单例模式代码简洁,易于理解和维护。枚举类型在Java中是一种特殊的类,可以自动处理线程安全性和防止多次实例化的问题,无需编写额外的代码。
线程安全性:枚举式单例在类加载时就会对枚举值进行初始化,因此具有天然的线程安全性,避免了多线程环境下的并发问题。
防止反序列化破坏:枚举类没有构造方法,也不支持反射机制来创建新的实例,因此可以防止通过反序列化等方式产生新的对象,保证了单例的唯一性。
资料
https://zhuanlan.zhihu.com/p/61257918
https://zhuanlan.zhihu.com/p/57570139
https://juejin.im/post/5c728d44518825629f38618b
单例模式的十种写法,你会几个?(重写版) https://zhuanlan.zhihu.com/p/136011856
android开发中 都会用到哪种单例?说说各自使用场景 在 Android 开发中,不同类型的单例模式有着各自的适用场景,下面为你详细介绍常见单例模式及其在 Android 开发里的使用场景: 饿汉式单例 java public class EagerSingleton { private static final EagerSingleton INSTANCE = new EagerSingleton(); private EagerSingleton() {} public static EagerSingleton getInstance() { return INSTANCE; } }
使用场景 全局配置管理:像应用的主题设置、字体大小、网络请求的基础 URL 这类全局配置信息,在应用启动时就需要加载,并且在整个应用生命周期内基本保持不变。饿汉式单例能保证在应用启动时就完成配置类的初始化,后续可直接使用,避免了多次初始化的问题。 资源管理器:例如管理应用的图片资源、音频资源等。在应用启动时就可以将这些资源加载到单例的资源管理器中,方便在应用的各个地方随时获取和使用资源。 懒汉式单例(线程安全,同步方法) java public class LazySingletonSafeSyncMethod { private static LazySingletonSafeSyncMethod INSTANCE; private LazySingletonSafeSyncMethod() {} public static synchronized LazySingletonSafeSyncMethod getInstance() { if (INSTANCE == null) { INSTANCE = new LazySingletonSafeSyncMethod(); } return INSTANCE; } } 使用场景 小型数据缓存:对于一些不常使用且数据量较小的缓存,比如用户的临时偏好设置,在应用启动时不需要立即加载,而是在用户首次访问相关功能时再进行初始化。由于可能存在多线程访问的情况,使用线程安全的懒汉式单例可以保证数据的一致性。 双重检查锁定(DCL)单例 java public class DoubleCheckedLockingSingleton { private static volatile DoubleCheckedLockingSingleton INSTANCE; private DoubleCheckedLockingSingleton() {} public static DoubleCheckedLockingSingleton getInstance() { if (INSTANCE == null) { synchronized (DoubleCheckedLockingSingleton.class) { if (INSTANCE == null) { INSTANCE = new DoubleCheckedLockingSingleton(); } } } return INSTANCE; } } 使用场景 网络请求管理:在 Android 应用中,网络请求是非常常见的操作,而且可能会在多个线程中同时发起。使用 DCL 单例模式来管理网络请求的客户端(如 OkHttp 客户端),可以避免多次创建客户端实例,减少资源消耗。同时,DCL 单例的高性能特点也能满足高并发网络请求的需求。 数据库管理:数据库操作通常需要一个统一的数据库连接对象,以避免多个连接同时操作数据库导致的数据不一致问题。使用 DCL 单例可以确保在多线程环境下只有一个数据库连接实例,并且在需要时才进行初始化,提高了系统的性能和稳定性。 静态内部类单例 java public class StaticInnerClassSingleton { private StaticInnerClassSingleton() {} private static class SingletonHolder { private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton(); } public static StaticInnerClassSingleton getInstance() { return SingletonHolder.INSTANCE; } } 使用场景 工具类单例:例如日期格式化工具、字符串处理工具等。这些工具类在应用中可能会被频繁使用,但在应用启动时不需要立即初始化。静态内部类单例可以实现延迟加载,只有在第一次使用这些工具类时才会进行初始化,同时保证了线程安全和代码的简洁性。 枚举单例 java public enum EnumSingleton { INSTANCE; public void doSomething() { System.out.println("Doing something..."); } } 使用场景 全局状态管理:在 Android 应用中,有些全局状态需要在整个应用生命周期内保持唯一,并且要防止被反序列化和反射攻击。例如,应用的登录状态、支付状态等,使用枚举单例可以确保这些状态的唯一性和安全性。 权限管理:对于应用的权限管理,如用户的不同权限级别(普通用户、管理员等),使用枚举单例可以清晰地定义和管理这些权限,并且保证在多线程环境下的安全性。