设计模式详解--单例模式

设计模式详解--单例模式

指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点

如ServletContext、ServletConfig、ApplicationContext、DBPool

隐藏其所有的构造方法,属于创建型模式

上面给出的定义是十分理想化的单例模式,也是单例模式的最终目标。但是实际开发中往往会遇到各种问题从而实现伪“单例”

也会遇到各种情况导致单例模式被破坏。

创建单例模式的方法常见的有四种,分别是饿汉式、懒汉式、注册式、ThreadLocal单例,下面我将一一介绍

单例模式常见写法:

一、饿汉式单例

所谓的饿汉式单例,即在类首次加载时就创建实例(类的加载初始化等问题请查看相关文章,这里不做过于深入的探讨)。给人的感觉像是饿了许久人一看到食物就迫不及待的扑上去大快朵颐一般,因此而得名。

优点:绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题

缺点:有可能浪费内存空间,因为有可能用不到也初始化了

饿汉式单例也有多种实现方式

1.

public class HungrySingleton {
   
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return  hungrySingleton;
    }
}

在创建实例的时候可以看到语句中用到了static  final修饰词,这两个没有一个是没用的

static自不必说静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。,这是单例的保障。那么final呢?我看网上有的加了这个关键字有的没加有的加了也是不明觉厉。好奇之下看了下。

fina是从cpu角度考虑保证了单例模式的准确性。

CPU处理通过缓存降低延迟,但是由于CPU主频超过访问cache时,会产生cache wait。从而造成性能瓶颈。针对这种情况,多种架构采用了一种将对指令重新排序的功能。对应于创建单例模式中

final的作用即:

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用

变量,这两个操作之间不能重排序

2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

保证对象的安全发布,防止对象引用被其他线程在对象被完全构造完成前拿到并使用

要分析上面例子中存在的问题,就要从instance = new Singleton()这句开始,对java来说,创建新的对象并不是一个原子操作,这个过程分成了3步:

1,给 instance 分配内存

2,调用 Singleton 的构造函数来初始化成员变量

3,将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

关键:
1,在JVM的即时编译器中,存在一个设定,叫做指令重排序。

2,在上面的例子中,2操作依赖1操作,但3操作并不依赖2操作,也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3 也可能是1-3-2。如果是后者,则在3执行完毕,2未执行之前,被线程二抢占了,这时instance已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

3,JDK1.5以后,因为内存模型的优化,上面的例子不会再因为指令重排序而出现问题。

(参考资料 https://blog.csdn.net/lkforce/article/details/70332129 从单例模式挖到内存模型(二)----指令重排序)

但是jdk1.5后修复了也就不存在这个问题了,所以final可加可不加。


2.饿汉式静态块单例

 

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySingleton;
    static {
        hungrySingleton = new HungryStaticSingleton();
    }
    private HungryStaticSingleton(){}
    public static HungryStaticSingleton getInstance(){
        return  hungrySingleton;
    }
}

 

和第一种没什么区别。

二、懒汉式单例

被外部类调用时才创建实例,给人一种懒洋洋的感觉,你调用我我才创建,你不吱声我就不创建。

缺点:有可能造成线程不安全导致创建不止一个实例。原因有可能多个线程同时进入非空判断为

true,所以创建多个。

1.

public class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    //静态块,公共内存区域
    private static LazySimpleSingleton lazy = null;
    public  static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}

 

这是最简单的懒汉式创建方式,但是有可能有线程安全问题,即1线程在执行if(lazy==null)后,失去了cpu的占有,2线程完成了对象的实例化,这时候CPU时间片给到线程1,线程1继续执行并覆盖原有的对象(把引用指向新的对象)。更夸张一点有可能线程2创建完了实例后去用这个实例去执行一些操作然后线程1才覆盖,就形成了线程不安全的问题。另外,就算没等到线程2去执行其他操作就被线程1覆盖,从本质上讲也破坏的单例的唯一性。而且所谓的覆盖也只是把引用指向新的对象,原有的对象并不会马上删除。

那么如何解决这个问题呢,加上个synchronized关键字锁住方法即可

代码

ublic class LazySimpleSingleton {
    private LazySimpleSingleton(){}
    //静态块,公共内存区域
    private static LazySimpleSingleton lazy = null;
    public synchronized static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}

2.

线程安全还能保证是单例,那么这就是最完美的单例模式了吗?答案显然是否定的,这种单例模式虽然是线程安全的,但是因为加上了synchronized关键字会使得性能下降。具体下降多少就需要看并发量了。比如同时100个线程并发的访问这个实例getInstance,那么99个就会被阻塞了。

完全规避synchronized是不现实的,聪明的前辈们想出了一个双重锁的懒汉式单例来减小锁竞争几率

代码如下:

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazy = null;

    private LazyDoubleCheckSingleton(){}
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazy == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazy == null){
                    lazy = new LazyDoubleCheckSingleton();
                    
                }
            }
        }
        return lazy;
    }
}

乍一看就是多加了次if条件判断并且把锁加在两次判断中间。但是这样就如何减小锁竞争几率了呢?其实我们在后期调用实例的时候很多情况下都是已经实例化好了的,但是第一种情况还需要加锁来判断下,实际上第一种情况中讨论的并发交替生成对象概率本来就不高,那么就判断等于null的时候加锁判断下就好了,如果lazy!=null那么一定不会有出现多次赋值的情况。

这样100个并发可能也就10个线程锁竞争一下,大大减小了锁竞争的概率。

3.静态内部类懒汉式单例模式

这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题。完美地屏蔽了这两个缺点。

代码

public class LazyInnerClassSingleton {
   
    //每一个关键字都不是多余的
    //static 是为了使单例的空间共享
    //保证这个方法不会被重写,重载
    public static final LazyInnerClassSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

 

这里利用了静态内部类可以被外部类通过类名.方法调用的语法规则,从而实现了创建对象的语句一气呵成不存在安全问题又可以调用时再加载。但是这种方式也是可以被反射破坏的。

测试反射破坏单例代码如下:

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) {
        try{
            //很无聊的情况下,进行破坏
            Class clazz = LazyInnerClassSingleton.class;

            //通过反射拿到私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问,强吻,不愿意也要吻
            c.setAccessible(true);

            //暴力初始化
            Object o1 = c.newInstance();

            //调用了两次构造方法,相当于new了两次
            //犯了原则性问题,
            Object o2 = c.newInstance();

            System.out.println(o1 == o2);
//            Object o2 = c.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果:

设计模式详解--单例模式_第1张图片

说明创建了两个实例

解决办法

反射创建对象是调用在构造器里进行判断,如果已有实例了就拒绝再创建。

代码如下

public class LazyInnerClassSingleton {
    //默认使用LazyInnerClassGeneral的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private LazyInnerClassSingleton(){
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    //每一个关键字都不是多余的
    //static 是为了使单例的空间共享
    //保证这个方法不会被重写,重载
    public static final LazyInnerClassSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

再次运行测试类,结果如下:

 

设计模式详解--单例模式_第2张图片

 

也许有人会问,万一对象是先序列化创建出来的,不是正常创建出来的怎么办。其实是无所谓的,只要保证内存中有一个实例就可以了。

既然说到了破坏单例的方法,那么就再说一个

通过序列化的方式破坏单例

此方法不局限于懒汉式还是饿汉式,所以此处就拿饿汉式举例

代码:

//反序列化时导致单例破坏
public class SeriableSingleton implements Serializable {

    //序列化就是说把内存中的状态通过转换成字节码的形式
    //从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
    //内存中状态给永久保存下来了

    //反序列化
    //将已经持久化的字节码内容,转换为IO流
    //通过IO流的读取,进而将读取的内容转换为Java对象
    //在转换过程中会重新创建对象new

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

}

 

测试类:

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();


            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

运行结果:

 

 

设计模式详解--单例模式_第3张图片

可以看到两个对象的内存地址是不一样的,单例被破坏了

那么怎么解决呢?

我看过一位同学大胆的说“不实现serializable就好了”,哈哈是很有道理,但是实际情况下我们往往都需要去实现的,更别提网络传输数据HTTPRestful了。

在本例中,我们可以看到,序列化实例化对象是通过语句

s1 = (SeriableSingleton)ois.readObject();实现的。点进这个方法,层层剥茧,可以找到这么一段代码

 

点进readObject0

 

点进readOrdinaryObject

 

设计模式详解--单例模式_第4张图片

发现有个hasReadResloveMethod,而这个方法的说明是这样的

/**
 * Returns true if represented class is serializable or externalizable and
 * defines a conformant readResolve method.  Otherwise, returns false.
 */

就是说如果依赖的类有readResolve这个方法那么就返回true,否则返回false

下面的给rep赋值就是调用了readResolve方法,那么是不是我们在类中加个readResolve方法,再在这个方法中返回已经创建好的实例对象是不是就可以避免单例被破坏了呢,说干就干,我们来试一下

代码如下

//反序列化时导致单例破坏
public class SeriableSingleton implements Serializable {

    //序列化就是说把内存中的状态通过转换成字节码的形式
    //从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
    //内存中状态给永久保存下来了

    //反序列化
    //将已经持久化的字节码内容,转换为IO流
    //通过IO流的读取,进而将读取的内容转换为Java对象
    //在转换过程中会重新创建对象new

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    private  Object readResolve(){
        return  INSTANCE;
    }

}

 

再跑一遍测试用例,发现姐果果然变成了true,守卫单例成功!

有人奇怪这么个方法时怎么来的。我觉得可能JDK的设计开发人员就考虑到了这一点特意加上这个方法防止单例被序列化破坏的吧。

懒汉式单例也到此结束

三、注册式单例

即将每一个实例都缓存到统一容器中,使用唯一标识获取实例

 

枚举式:

public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

 

首先,枚举式线程安全的。因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

用前面提到的序列化和反射方式测试发现全都报错无法破坏单例。这是为什么呢?

我们可以看一下反射方式newInstance源码

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

内部使用的饿汉式,可避免序列坏破坏单例从 jdk层面就为枚举不被序列化和反射破坏来保驾护航

至于枚举为什么不怕序列化,同样点进readObject方法

点进readEnum

这里是根据类和枚举唯一name读入一个实例值。所以可以保证单例

更多,可以参考这篇文章https://www.cnblogs.com/z00377750/p/9177097.html深度分析Java的枚举类型—-枚举的线程安全性及序列化问题

spring中运用的注册式单例

 

//Spring中的做法,就是用这种注册式单例,对象方便管理也是懒加载,但是存在线程安全问题
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map ioc = new ConcurrentHashMap();
    public static Object getInstance(String className){
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

 

 

四、ThreadLocal单例

这种办法就是通过利用ThreadLocal的线程隔离性来保证线程安全

实现多数据源动态切换

保证线程内部全局唯一,所以天生线程安全

代码

public class ThreadLocalSingleton {
    private static final ThreadLocal threadLocalInstance =
            new ThreadLocal(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

以上就是关于单例模式的全部介绍,欢迎大家留言批评指正

下一篇 设计模式之原型模式详解

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(设计模式)