您好,登录后才能下订单哦!
本篇内容介绍了“怎么使用单例模式”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
保证一个实例很简单,只要每次返回同一个实例就可以,关键是如何保证实例化过程的线程安全
?
这里先回顾下类的初始化
。
在类实例化之前,JVM会执行类加载
。
而类加载的最后一步就是进行类的初始化,在这个阶段,会执行类构造器<clinit>
方法,其主要工作就是初始化类中静态的变量,代码块。
而<clinit>()
方法是阻塞的,在多线程环境下,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
,其他线程都会被阻塞。换句话说,<clinit>
方法被赋予了线程安全的能力。
再结合我们要实现的单例,就很容易想到可以通过静态变量
的形式创建这个单例,这个过程是线程安全的,所以我们得出了第一种单例实现方法:
private static Singleton singleton = new Singleton();
public static Singleton getSingleton() {
return singleton;
}
很简单,就是通过静态变量实现唯一单例,并且是线程安全
的。
看似比较完美的一个方法,也是有缺点的,就是有可能我还没有调用getSingleton方法
的时候,就进行了类的加载,比如用到了反射或者类中其他的静态变量静态方法。所以这个方法的缺点就是有可能会造成资源浪费,在我没用到这个单例的时候就对单例进行了实例化。
在同一个类加载器下,一个类型只会被初始化一次,一共有六种能够触发类初始化的时机:
1、虚拟机启动时,初始化包含 main 方法的主类; 2、new等指令创建对象实例时 3、访问静态方法或者静态字段的指令时 4、子类的初始化过程如果发现其父类还没有进行过初始化 5、使用反射API 进行反射调用时 6、第一次调用java.lang.invoke.MethodHandle实例时
这种我不管你用不用,只要我这个类初始化了,我就要实例化这个单例,被类比为 饿汉方法
。(是真饿了,先实例化出来放着吧,要吃的时候就可以直接吃了)
缺点就是 有可能造成资源浪费(到最后,饭也没吃上,饭就浪费了)
但其实这种模式一般也够用了,因为一般情况下用到这个实例的时候才会去用这个类,很少存在需要使用这个类但是不使用其单例的时候。
当然,话不能说绝了,也是有更好的办法来解决这种可能的资源浪费
。
在这之前,我们先看看Kotlin的 饿汉实现
。
object Singleton
没了?嗯,没了。
这里涉及到一个kotlin中才有的关键字:object(对象)
。
关于object主要有三种用法:
主要用于创建一个继承自某个(或某些)类型的匿名类的对象。
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*……*/ }
override fun mouseEntered(e: MouseEvent) { /*……*/ }
})
主要用于单例。也就是我们今天用到的用法。
object Singleton
我们可以通过Android Studio 的 Show Kotlin Bytecode
功能,看到反编译后的java代码:
public final class Singleton {
public static final Singleton INSTANCE;
private Singleton() {
}
static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}
很显然,跟我们上一节写的饿汉差不多,都是在类的初始化阶段就会实例化出来单例,只不过一个是通过静态代码块,一个是通过静态变量。
类内部的对象声明可以用 companion
关键字标记,有点像静态变量,但是并不是真的静态变量。
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
//使用
MyClass.create()
反编译成Java代码:
public final class MyClass {
public static final MyClass.Factory Factory = new MyClass.Factory((DefaultConstructorMarker)null);
public static final class Factory {
@NotNull
public final MyClass create() {
return new MyClass();
}
private Factory() {
}
// $FF: synthetic method
public Factory(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
其原理还是一个静态内部类
,最终调用的还是这个静态内部类的方法,只不过省略了静态内部类的名称。
要想实现真正的静态成员需要 @JvmField
修饰变量。
说回正题,即然饿汉有缺点,我们就想办法去解决,有什么办法可以不浪费这个实例呢?也就是达到 按需加载
单例?
这就要涉及到另外一个知识点了,静态内部类
的加载时机。
刚才说到类的加载时候,初始化过程只会加载静态变量和代码块,所以是不会加载静态内部类的。
静态内部类是延时加载
的,意思就是说只有在明确用到内部类
时才加载。只使用外部类时不加载。
根据这个信息,我们就可以优化刚才的 饿汉模式
,改成静态内部类模式(java和kotlin版本)
:
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton() {
return SingletonHolder.INSTANCE;
}
companion object {
val instance = SingletonHolder.holder
}
private object SingletonHolder {
val holder = SingletonDemo()
}
同样是通过类的初始化<clinit>()
方法保证线程安全,并且在此之上,将单例的实例化过程向后移,移到静态内部类。所以就变成了当调用getSingleton方法的时候才会去初始化这个静态内部类,也就是才会实例化静态单例。
如此一整,这种方法就完美了...吗?好像也有缺点啊,比如我调用getSingleton方法
创建实例的时候想传入参数怎么办呢?
可以,但是需要一开始就设置好参数值,无法通过调用getSingleton
方法来动态设置参数。比如这样写:
private static class SingletonHolder {
private static String test="123";
private static Singleton INSTANCE = new Singleton(test);
}
public static Singleton getSingleton() {
SingletonHolder.test="12345";
return SingletonHolder.INSTANCE;
}
最终实例化进去的test只会是123,而不是12345。因为只要你开始用到SingletonHolder
内部类,单例INSTANCE
就会最开始完成了实例化,即使你赋值了test,也是单例实例化之后的事了。
这个就是 静态内部类方法的缺点了。如果不用动态传参数,那么这个方法已经足够了。
如果需要传参数呢?
那就正常写呗,也就是调用getSingleton
方法的时候,去判断这个单例是否已存在,不存在就实例化即可。
private static Singleton singleton;
public static Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
这个倒是看的很清楚,需要的时候才去创建实例,这样的话就保证了在需要吃饭的时候才去做饭,比较中规中矩的一个做法,但是在饿汉的思维里就会觉得这个人好懒啊,都不先准备好饭。
所以这个方法被称为 懒汉式
。
但是这个方法的弊端也是很明显,就是线程不安全
,不同线程同时访问getSingleton方法有可能导致对象实例化出错。
所以,加锁。
加锁怎么加,也是个问题。
首先肯定的是,我们加的锁肯定是类锁
,因为要针对这个类进行加锁,保证同一时间只有一个线程进行单例的实例化操作。
那么类锁就有两种加法了,修饰静态方法和修饰类对象:
//方法1,修饰静态方法
public synchronized static Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
//方法2,代码块修饰类对象
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
方法2这种方式就是我们常说的双重校验
的模式。
比较下两种方式其实区别也就是在这个双重校验,首先判断单例是否为空,如果为空再进入加锁阶段,正常走单例的实例化代码。
那么,为什么要这么做呢?
第一个判断,是为了性能
。当这个singleton已经实例化之后,我们再取值其实是不需要再进入加锁阶段的,所以第一个判断就是为了减少加锁。把加锁只控制在第一次实例化这个过程中,后续就可以直接获取单例即可。第二个判断,是防止重复创建对象
。当两个线程同时走到
synchronized
这里,线程A获得锁,进入创建对象。创建完对象后释放锁,然后线程B获得锁,如果这时候没有判断单例是否为空,那么就会再次创建对象,重复了这个操作。到这里,看似问题都解决了。
等等,new Singleton()
这个实例化过程真的没问题吗?
在JVM中,有一种操作叫做指令重排
:
JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,会将指令进行重新排序,但是这种重新排序不会对单线程程序产生影响。
简单的说,就是在不影响最终结果的情况下,一些指令顺序可能会被打乱。
再看看在对象实例化中的指令主要有这三步操作:
如果我们将第二步和第三步重排一下,结果也是不影响的:
这种情况下,就有问题了:
当线程A进入实例化阶段,也就是new Singleton()
,刚完成第二步分配好内存地址。这时候线程B调用了getSingleton()
方法,走到第一个判空,发现不为空,返回单例,结果用的时候就有问题了,对象都没有初始化完成。
这就是指令重排有可能导致的问题。
所以,我们需要禁止指令重排,volatile
登场。
volatile 主要有两个特性:
所以再加上volatile
对变量进行修饰,这个双重校验的单例模式也就完整了。
private volatile static Singleton singleton;
//不带参数
class Singleton private constructor() {
companion object {
val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
Singleton() }
}
}
//带参数
class Singleton private constructor(private val context: Context) {
companion object {
@Volatile private var instance: Singleton? = null
fun getInstance(context: Context) =
instance ?: synchronized(this) {
instance ?: Singleton(context).apply {
instance = this
}
}
}
}
诶?不带参数的这个写法也太简便了点吧?Volatile也没有了?确定没问题?
没问题,奥秘就在这个延迟属性lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED)
中,我们进去瞧瞧:
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
看到了吧,其实内部还是用到了Volatile + synchronized
双重校验。
“怎么使用单例模式”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。