单例模式是我们实际开发中常用到的开发模式,目的是保证实例的唯一性,确保这个类在内存中只会存在一个对象,但我们现在用到的单例模式相关代码可能不是最优的,今天让我们探索一下单例模式的正确写法。
单例模式通常分为饿汉式和懒汉式,我们这里来一个最简单的代码:
饿汉式相关代码:
public class SingletonPattern {
//无参构造私有化,不允许直接new获得实例
private SingletonPattern() {
}
//创建静态啊私有实例
private static SingletonPattern hungerSingleton =new SingletonPattern();
//同过公共静态方法获取实例,确保唯一
public static SingletonPattern getHungerInstance(){
return hungerSingleton;
}
}
缺点:
提前创建类实例,无论是否需要,只要类加载就进行了实例化,浪费资源.
懒汉式相关代码:
//懒汉式,
private static SingletonPattern lazySingleton;
public static SingletonPattern getLazyInstance(){
//判断如果没有实例创建,则创建实例
if(lazySingleton == null){
lazySingleton =new SingletonPattern();
}
return lazySingleton;
}
缺点:
需要的时候创建类实例,没有考虑到多线程,多线程环境下无法保证单例效果,会多次执行SingletonPattern instance=new SingletonPattern()
懒汉式和饿汉式主要区别是否先创建类的实例,一个是拿时间换空间,一个是拿空间换时间,懒汉式只有我需要他的时候才去加载它,懒加载机制,饿汉式不管需不需要我先在内存中开辟空间。这两种是最基本的单列模式,接下来我们就以懒汉式为例,分析如何正确创建和使用单例。
我们分析上面的懒汉式代码的问题是:多线程情况下无法保证单例,也就是说:如果同时多个线程访问可能会创建多个实例的情况出现.那我们首先想到的是-synchronized,来进行同步方法或同步块,代码如下:
//懒汉式加锁同步
private static SingletonPattern syncSingleton;
public static SingletonPattern getSyncyInstance(){
if(syncSingleton == null){
//在此处加锁同步比在方法出加锁同步缩小了范围,性能稍高
synchronized (SingletonPattern.class){
syncSingleton =new SingletonPattern();
}
}
return syncSingleton;
}
缺点:
虽然使用了synchronized进行了线程的同步,还是会存在多次执行的可能SingletonPattern instance=new SingletonPattern()
进一步优化采用DCL(Double Check Lock)双重检查来确定单例模式的线程安全和唯一,相关代码如下:
//懒汉式双层检查
private static SingletonPattern dclSingleton;
public static SingletonPattern getDCLInstance(){
if(dclSingleton == null){//第一层检查
//在此处加锁同步比在方法出加锁同步缩小了范围,性能稍高
synchronized (SingletonPattern.class){
if(dclSingleton == null){//第二层检查
dclSingleton =new SingletonPattern();
}
}
}
//此处有可能多线返回null对象。导致崩溃
return dclSingleton;
}
缺点:
首先我们先了解一下:通常我们进行实例化包含以下几步:
1.给 Singleton 实例分配内存,将函数压栈,并且申明变量类型;
2.初始化构造函数以及里面的字段,在堆内存开辟空间;
3.将 instance 对象指向分配的内存空间;
java 编译器允许执行无序,并且 jdk1.5之前不限制处理器重排序,不能保证按序执行,处理器会进行指令重排序优化导致程序崩溃。举例:正常顺序是1-2-3,优化重排后执行顺序可能为:1-3-2, 这时假如有 A 和 B 两条线程, A线程执行到3的步骤,但是未执行2,这时候 B 线程来了抢了权限,判断不为空,直接取走 instance可能会造成程序崩溃。
也就是说在jdk1.5之前有两个问题:
- 线程间共享变量不可见性;
- 无序性(执行顺序无法保证);
为了解决jdk1.5存在的上述问题,我们需要在一个线程进行初始化等操作的时候对其他进入的线程可见,执行完毕后,其他线程在进行操作,这就又引出一个关键字-volatile
相关代码:
//volatile+懒汉式双层检查(DCL,Double Check Lock)
private static volatile SingletonPattern volatileSingleton;
public static SingletonPattern getVolatileInstance(){
if(volatileSingleton == null){//第一层检查
//在此处加锁同步比在方法出加锁同步缩小了范围,性能稍高
synchronized (SingletonPattern.class){
if(volatileSingleton == null){//第二层检查
volatileSingleton =new SingletonPattern();
}
}
}
return dclSingleton;
}
分析:
先说一下volatile关键字的两大作用:
- 可以保证在多线程环境下,变量的修改可见性
- 提供内存屏障,来保证某些指令顺序处理器不能够优化重排,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
所以使用volatile关键字可以保证实例化的赋值操作是最后一步完成,实现了正确的单例模式。
其他单例的实现方法:
- 静态内部类
- 枚举
采用静态内部类也是一种不错的选择,理由是静态内部类在没有显示调用的时候是不会进行加载的,当执行了return 后才加载初始化。
相关代码:
public static SingletonPattern InnerSingletonInstance(){
return staticSingleInstance.staticSingleton;
}
private static class staticSingleInstance{
private static SingletonPattern staticSingleton=new SingletonPattern();
}
枚举类是java1.5才出现的,采用枚举的方式除了写起来很简便,还有个好处是安全:因为JVM会保证enum不能被反射并且构造器方法只执行一次,但枚举会很耗内存,所以看情况而定吧
相关代码如下:
//枚举实现
public static SingletonPattern getEnumInstance(){
return EnumSingleton.INSTANCE.getEnumSingleton();
}
private enum EnumSingleton {
INSTANCE;
private SingletonPattern enumSingleton;
//JVM会保证此方法绝对只调用一次
private SingletonPattern getEnumSingleton() {
enumSingleton = new SingletonPattern();
return enumSingleton;
}
}
关于单例模式的实现,今天就说这么多,其实核心就是:构造私有,并且通过静态方法获取一个实例,在这个过程中必须保证线程的安全性。
告辞。
网友评论