1 单例模式的优点:
1 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
2 避免对资源的多重占用(比如写文件操作)。
2 饿汉式的写法 缺点 以及解决办法
//code 1
public class Singleton {
//在类内部实例化一个实例
private static Singleton instance = new Singleton();
//私有的构造函数,外部无法访问
private Singleton() {
}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
return instance;
}
}
//code 3 饿汉式变种
public class Singleton2 {
//在类内部定义
private static Singleton2 instance;
static {
//实例化该实例
instance = new Singleton2();
}
//私有的构造函数,外部无法访问
private Singleton2() {
}
//对外提供获取实例的静态方法
public static Singleton2 getInstance() {
return instance;
}
}
- 优点
1 第一次使用时就已经被初始化了.当需要到数仓查询一些数据到内存中时,希望项目启动的时候就可以把数据加载完成,而且数仓查询缓慢,懒汉式会导致第一次查询耗时很长.
2 该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。
- 缺点
1 不能延迟加载。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。
2 如果这个类被多次加载的话也会造成多次实例化。
- 解决办法
1 通过静态内部类的方法
2 懒汉式(下文有介绍)
//code 4
public class Singleton{
//在静态内部类中初始化实例对象
private static class SingletonHolder {
private static final SingletonINSTANCE = new Singleton();
}
//私有的构造方法
private Singleton() {
}
//对外提供获取实例的静态方法
public static final SingletongetInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式同样利用了
classloder
的机制来保证初始化instance
时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton
类被装载了,那么instance
就会被实例化(没有达到lazy loading
效果),而这种方式是Singleton
类被装载了,instance
不一定被初始化。因为SingletonHolder
类没有被主动使用,只有显示通过调用getInstance
方法时,才会显示装载SingletonHolder
类,从而实例化instance
。想象一下,如果实例化instance
很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton
类加载时就实例化,因为我不能确保Singleton
类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式更加合理。
3 懒汉式的写法 缺点 以及解决办法
//code 5
public class Singleton {
//定义实例
private static Singleton instance;
//私有构造方法
private Singleton(){}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 优点
1 实例化延迟到第一次被引用的时候。防止不必要的浪费
- 缺点
1 在多线程情况下,有可能两个线程同时进入if语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。
- 解决办法1-同步
//code 6
public class SynchronizedSingleton {
//定义实例
private static SynchronizedSingleton instance;
//私有构造方法
private SynchronizedSingleton(){}
//对外提供获取实例的静态方法,对该方法加锁
public static synchronized SynchronizedSingleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
1 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的延迟加载,
2 效率很低,因为99%情况下不需要同步。(因为上面的synchronized的加锁范围是整个方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入if语句中的情况,根本不需要同步操作,可以直接返回instance。)
- 解决办法2-双重检测
那么如何缩小锁的范围呢?相比于同步方法,同步代码块的加锁范围更小
//code 7
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
1 通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。
2 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。(指令重排)
线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有传到B使用的内存(缓存一致性)),程序很可能会崩溃。
- 解决方法3 -
volatile
在J2SE 5.0中,这一问题被修正了。volatile
关键字保证多个线程可以正确处理单件实例
//code 8
public class VolatileSingleton {
private static volatile VolatileSingleton singleton;
private VolatileSingleton() {
}
public static VolatileSingleton getSingleton() {
if (singleton == null) {
synchronized (VolatileSingleton.class) {
if (singleton == null) {
singleton = new VolatileSingleton();
}
}
}
return singleton;
}
}
1 上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。
2 即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候
- 解决办法 - 加上
readResolve
方法
//code 11
package com.hollis;
import java.io.Serializable;
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
- 为什么?
在jdk中ObjectInputStream的类中有readUnshared()方法,如果被反序列化的对象的类存在readResolve这个方法,他会调用这个方法来返回一个“array”(我也不明白),然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。
4 单例懒汉式如何使用final
//code 9
class FinalWrapper<T> {
public final T value;
public FinalWrapper(T value) {
this.value = value;
}
}
public class FinalSingleton {
private FinalWrapper<FinalSingleton> helperWrapper = null;
public FinalSingleton getHelper() {
FinalWrapper<FinalSingleton> wrapper = helperWrapper;
if (wrapper == null) {
synchronized (this) {
if (helperWrapper == null) {
helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
}
wrapper = helperWrapper;
}
}
return wrapper.value;
}
}
5 为什么饿汉式会线程安全,枚举类创建的单例会线程安全吗?
当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)
6 怎样破坏单例?为什么?
1 反射可以破坏单例,这个是毋庸置疑的
2 序列化也可以破坏单例
- 序列化破坏的原因
对象的序列化过程通过
ObjectOutputStream
和ObjectInputputStream
来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream
的readObject
该方法通过反射
的方式调用无参构造方法新建一个对象。
6 为什么枚举可避免反序列化破坏单例
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject等方法。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
可以参考:ObjectInputputStream
的readObject
的代码
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题
网友评论