我的GOF23之单例模式

蚀纹重载
• 阅读 1783

我的GOF23之单例模式

写在最前面

作为一个电气工程师,研究等离子体方向,最近在自学设计模式,此为整理博客。
设计模式可以分为三大类,分别是创建型设计模式、行为型设计模式以及结构型设计模式。

单例模式是创建型的设计模式的一种。

心法:

  1. 构造器私有

  2. 私有单例对象

  3. 公有静态方法获取单例对象

各种实现方式

饿汉式

public class Singleton{
    private static Singleton instance = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return instance;
    }
}

特点:线程安全,但是没有实现lazy load.所谓的lazy load,根据Bob Lee的说法,

In production, you typically want to eagerly load all your singletons so you catch errors early and take any performance hit up front, but in tests and during development, you only want to load what you absolutely need so as not to waste time.

那么如何实现lazy load呢?

懒汉式

public class Singleton{
    private static Singleton instance;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式的确实现了lazy load的问题,但是它也带来了新的问题,线程安全无法保证。我们可以在getInstance()方法上加synchronized,当然也可以给instance加volatile,这同样是Bob Lee的看法,他说

But volatile isn't that much faster than synchronized, synchronized is pretty fast nowadays.

但是与此同时,我还看到了更深入的讨论,如

http://stackoverflow.com/a/46... So the overall cost of a volatile read will roughly equivalent of a memory load and can be as cheap as a L1 cache access. However if another core is writing to the volatile variable, the cache-line will be invalidated requiring a main memory or perhaps an L3 cache access. The actual cost will depend heavily on the CPU architecture.

这就太深入了,暂时不考虑,但是下面的建议还是不错的:

Nevertheless you shouldn't make a variable volatile unless you know that it will be accessed from multiple threads outside of synchronized blocks. Even then you should consider whether volatile is the best choice versus synchronized, AtomicReference and its friends, the explicit Lock classes, etc.

因此我并未在如下的代码中添加volatile关键字

线程安全的懒汉式

public class Singleton{
    private static Singleton instance;
    
    private Singleton(){
    }
    
    public synchronized static Singleton getInstance(){
        if(instance == null){
                instance = new Singleton();
        }
        return instance;
    }
}

双锁机制Double-Checked Locking(DCL)可以对性能进行进一步的优化

双锁机制

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

双锁机制的问题:

So what's broken about DCL?
DCL relies on an unsynchronized use of the resource field. That appears to be harmless, but it is not. To see why, imagine that thread A is inside the synchronized block, executing the statement resource = new Resource(); while thread B is just entering getResource(). Consider the effect on memory of this initialization. Memory for the new Resource object will be allocated; the constructor for Resource will be called, initializing the member fields of the new object; and the field resource of SomeClass will be assigned a reference to the newly created object.
However, since thread B is not executing inside a synchronized block, it may see these memory operations in a different order than the one thread A executes. It could be the case that B sees these events in the following order (and the compiler is also free to reorder the instructions like this): allocate memory, assign reference to resource, call constructor. Suppose thread B comes along after the memory has been allocated and the resource field is set, but before the constructor is called. It sees that resource is not null, skips the synchronized block, and returns a reference to a partially constructed Resource! Needless to say, the result is neither expected nor desired.
When presented with this example, many people are skeptical at first. Many highly intelligent programmers have tried to fix DCL so that it does work, but none of these supposedly fixed versions work either. It should be noted that DCL might, in fact, work on some versions of some JVMs -- as few JVMs actually implement the JMM properly. However, you don't want the correctness of your programs to rely on implementation details -- especially errors -- specific to the particular version of the particular JVM you use.
Other concurrency hazards are embedded in DCL -- and in any unsynchronized reference to memory written by another thread, even harmless-looking reads. Suppose thread A has completed initializing the Resource and exits the synchronized block as thread B enters getResource(). Now the Resource is fully initialized, and thread A flushes its local memory out to main memory. The resource's fields may reference other objects stored in memory through its member fields, which will also be flushed out. While thread B may see a valid reference to the newly created Resource, because it didn't perform a read barrier, it could still see stale values of resource's member fields.

简单说来,DCL导致的问题是,初始化实例的写入操作和实例字段的写入操作能够被编译器或者缓冲区重排序,重排序可能会导致返回部分构造的一些东西。就是我们读取到了一个没有初始化的对象。
对此,有更好的解决方法,即保证了线程安全,又实现了lasy load——Initialization on Demand Holder即静态内部类实现

静态内部类

public class Singleton{
    private Singleton(){
    }

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

以上方法都有可能通过反射或序列化与反序列化来破解,具体内容参见 反射与(反)序列化问题,而枚举类则没有如上问题

最理想的实现——枚举

public enum Singleton{
    INSTANCE;
}

反射与(反)序列化问题

反射是如何破解单例的

    public class Reflect{
        public static void main(String[] args) throws Exception{
            Class<Singleton> clazz = (Class<Singleton>)Class.forName("bin.pattern.Singleton");
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = (Singleton)c.newInstance();
            System.out.println(s1 == s2); 
        }
    }

返回false,说明产生了新的对象。修复方法:Singleton.java

public class Singleton{
    private static Singleton instance;
    private Singleton(){
        if(instance != null){
            throw new RuntimeException();
        }
    }
}

序列化与反序列化是如何破解单例的

public class Serialization{
    public static void main(String[] args) throws IOException,ClassNotFoundException{
        Singleton s1 = Singleton.getInstance();
        FileOutputStream fos = new FileOutputStream("/Users/bin/Documents/workspace/myjdk/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/bin/Documents/workspace/myjdk/a.txt"));
        Singleton s2 = (Singleton)ois.readObject();
        
        System.out.println(s1 == s2);
    }
}

输出false,说明又跪了。解决方法:Singleton.java

public class Singleton implements Serializable{
    //添加方法
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}
点赞
收藏
评论区
推荐文章
3A网络 3A网络
2年前
Golang 常见设计模式之单例模式
之前我们已经看过了Golang常见设计模式中的装饰和选项模式,今天要看的是Golang设计模式里最简单的单例模式。单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。Go语言实现单例模式的方法有很多种,下面我们就一起来看一下。饿汉式饿汉式实现单例模式非
Wesley13 Wesley13
3年前
java中饿汉与懒汉的故事(单例设计模式)
java中的单例设计模式关于设计模式,这其实是单独存在的东西,它不属于java,但是在java中使用较多,所以今天我就给大家介绍下单例设计模式中的饿汉和懒汉这俩朴素的打工人。首先我先说明下单例设计模式是啥(如果不想了解,可以直接划下去看饿汉和懒汉):类的单例设计模式就是采用一定的方法保证在整个软件系统中,对某个类只能存在一
海军 海军
4年前
JavaScript设计模式之单例模式
<sectionid"nice"datatool"mdnice编辑器"datawebsite"https://www.mdnice.com"style"fontsize:16px;color:black;lineheight:1.6;wordspacing:0px;letterspacing:0px;word
红烧土豆泥 红烧土豆泥
4年前
创建型模式之单例设计模式
什么是单例设计模式?顾名思义,只有一个实例。单例模式它主要是确保一个类只有一个实例,并且可以提供一个全局的访问点。废话少说,直接上干货了单例模式之饿汉式所谓饿汉式,顾名思义,“它很饿”。所以说,它一旦被加载进来,就会直接实例化一个对象。例如:languageclassSingleton{privatestaticfin
Wesley13 Wesley13
3年前
PHP单例模式
<?php/设计模式之单例模式$_instance必须声明为静态的私有变量构造函数和析构函数必须声明为私有,防止外部程序new类从而失去单例模式的意义getInstance()方法必须设置为公有的,必须调用此方法以返回实例的一个引
Wesley13 Wesley13
3年前
JAVA设计模式之单例设计模式
    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。  在JAVA中实现单例,必须了解JAVA内存机制,JAVA中实例对象存在于堆内存中,若要实现单例,必须满足两个条件:  1.限制类实例化对象。即只能产生一个对象。
Wesley13 Wesley13
3年前
Java设计模式:Singleton(单例)模式
概念定义Singleton(单例)模式是指在程序运行期间,某些类只实例化一次,创建一个全局唯一对象。因此,单例类只能有一个实例,且必须自己创建自己的这个唯一实例,并对外提供访问该实例的方式。单例模式主要是为了避免创建多个实例造成的资源浪费,以及多个实例多次调用容易导致结果出现不一致等问题。例如,一个系统只能有一个窗口管理器或文件系统,一个程
Wesley13 Wesley13
3年前
23种设计模式(1):单例模式
定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。类型:创建类模式类图:!23种设计模式(1):单例模式第1张|快课网(http://static.oschina.net/uploads/img/201407/05200605_0dij.gif"23种设计模式(1):单例模式
Wesley13 Wesley13
3年前
Java Design Patterns
java的设计模式大体上分为三大类:创建型模式(5种):工厂方法模式,抽象工厂模式,单例模式,建造者模式,原型模式。结构型模式(7种):适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式。行为型模式(11种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模
Wesley13 Wesley13
3年前
23种设计模式(面向对象语言)
一、设计模式的分类总体来说设计模式分为三大类:创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。创建型模式是用来创建对象的模式,抽象了实例化的过程,帮助一个系统独立于其他关联对象的创建、组合和表示方式。所有的创建型模式都有两个主要功能:  1.将系统所使用的具体类的信息封装起来  2.隐藏
设计模式-单例模式概述 | 京东云技术团队
我们常把23种经典的设计模式分为三类:创建型、结构型、行为型,其中创建型设计模式主要解决“对象的创建”问题,将创建和使用代码解耦,结构型设计模式主要解决“类或对象的组合或组装”问题,将不同功能代码解耦,行为型设计模式主要解决“类或对象之间的交互”问题,将不