单例模式
确保一个类只有一个实例,并提供该实例的全局访问点。
用途
单例模式有以下两个优点:
在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。
避免对资源的多重占用(比如写文件操作)。
实现方式
我们知道,一个类的对象的产生是由类构造函数来完成的。如果一个类对外提供了public
的构造方法,那么外界就可以任意创建该类的对象。所以,如果想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使外面的类不能通过引用来产生对象。同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法。
饿汉式
下面是一个简单的单例的实现:
public class Singleton {
//在类内部实例化一个实例
private static Singleton instance = new Singleton();
//私有的构造函数,外部无法访问
private Singleton() {
}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
return instance;
}
}
通过static
的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton
的实例被创建出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。
饿汉模式 - 变种
public class Singleton2 {
//在类内部定义
private static Singleton2 instance;
static {
//实例化该实例
instance = new Singleton2();
}
//私有的构造函数,外部无法访问
private Singleton2() {
}
//对外提供获取实例的静态方法
public static Singleton2 getInstance() {
return instance;
}
}
饿汉式单例,在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。而且,如果这个类被多次加载的话也会造成多次实例化。其实解决这个问题的方式有很多,下面提供两种解决方式,第一种是使用静态内部类的形式。第二种是使用懒汉式。
静态内部内方式
public class StaticInnerClassSingleton {
//在静态内部类中初始化实例对象
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
//私有的构造方法
private StaticInnerClassSingleton() {
}
//对外提供获取实例的静态方法
public static final StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式同样利用了classloder的机制来保证初始化instance
时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton
类被装载了,那么instance
就会被实例化(没有达到lazy loading效果),而这种方式是Singleton
类被装载了,instance
不一定被初始化。因为SingletonHolder
类没有被主动使用,只有显示通过调用getInstance
方法时,才会显示装载SingletonHolder
类,从而实例化instance
。想象一下,如果实例化instance
很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton
类加载时就实例化,因为我不能确保Singleton
类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance
显然是不合适的。这个时候,这种方式相比饿汉式更加合理。
懒汉式 - 线程不安全
这种单例叫做懒汉式单例。懒汉,就是不会提前把实例创建出来,将类对自己的实例化延迟到第一次被引用的时候。getInstance
方法的作用是希望该对象在第一次被使用的时候被new
出来。
有没有发现,其实code 5这种懒汉式单例其实还存在一个问题,那就是线程安全问题。在多线程情况下,有可能两个线程同时进入if
语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。
public class Singleton {
//定义实例
private static Singleton instance;
//私有构造方法
private Singleton(){}
//对外提供获取实例的静态方法
public static Singleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式 - 线程安全
public class SynchronizedSingleton {
//定义实例
private static SynchronizedSingleton instance;
//私有构造方法
private SynchronizedSingleton(){}
//对外提供获取实例的静态方法,对该方法加锁
public static synchronized SynchronizedSingleton getInstance() {
//在对象被使用的时候才实例化
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
以上方法虽然是线程安全的,但是它使用synchronized
同步的方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入if
语句中的情况,根本不需要同步操作,可以直接返回instance
。
双重校验锁
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;
}
}
以上方法,通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。
这里我们分析创建一个对象的过程:
- 虚拟机遇到new指令,到常量池定位到这个类的符号引用。
- 检查符号引用代表的类是否被加载、解析、初始化过。
- 虚拟机为对象分配内存。
- 虚拟机将分配到的内存空间都初始化为零值。
- 虚拟机对对象进行必要的设置。
- 执行方法,成员变量进行初始化。
- 将对象的引用指向这个内存区域。
简化过程:
- Jvm检查该类是否被加载过,连接,初始化过
- 虚拟机为对象分配内存
- 虚拟机将分配到的内存初始化为零值
- 在内存M中初始化对象
- 将内存M的地址赋给singleton变量
在这里第4和第5步是会由于编译器进行指令优化而发生指令重排。
由于指令重排,线程A在执行对象的初始化方法时,singleton变量获得的是未初始化完成的内存M的地址,线程B此时进行判断if (singleton == null)
为false
,则此时线程B获得的是一个未完成初始化的对象。
在线程B继续运行时,会发生程序崩溃。
双重校验锁 - 改进
使用volatile
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;
}
}
使用volatile
关键字,由于volatile
的效果,禁止指令重排和编译器优化。
**上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。**但是,即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候。
上面方法问题要关注序列化对单例模式的破坏
枚举式
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 单例测试
Singleton firstSingleton = Singleton.INSTANCE;
firstSingleton.setObjName("firstName");
System.out.println(firstSingleton.getObjName());
Singleton secondSingleton = Singleton.INSTANCE;
secondSingleton.setObjName("secondName");
System.out.println(firstSingleton.getObjName());
System.out.println(secondSingleton.getObjName());
// 反射获取实例测试
try {
Singleton[] enumConstants = Singleton.class.getEnumConstants();
for (Singleton enumConstant : enumConstants) {
System.out.println(enumConstant.getObjName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
评论区