设计模式-单例模式详解

逻辑织光使
• 阅读 1608

单例模式

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

​ 需要隐藏其所有构造方法

​ 优点:

​ 在内存中只有一个实例,减少了内存开销

​ 可以避免对资源的多重占用

​ 设置全局访问点,严格控制访问

​ 缺点:

​ 没有接口,扩展困难

​ 如果要扩展单例对象,只有修改代码,没有别的途径

应用场景

​ ServletContext

​ ServletConfig

​ ApplicationContext

​ DBPool

常见的单例模式写法

饿汉式单例

​ 饿汉式就是在初始化的时候就初始化实例

​ 两种代码写法如下:

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

    private HungrySingleton() {

    }

    private static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }
}
public class HungryStaticSingleton {
    private static final HungryStaticSingleton HUNGRY_SINGLETON;

    static {
        HUNGRY_SINGLETON = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {

    }

    private static HungryStaticSingleton getInstance() {
        return HUNGRY_SINGLETON;
    }
}

​ 如果没有使用到这个对象,因为一开始就会初始化实例,这种方式会浪费内存空间

懒汉式单例

​ 懒汉式单例为了解决上述问题,则是在用户使用的时候才初始化单例

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判断保证初只会初始化一次
        if (lazySimpleSingleton == null) {
            lazySimpleSingleton = new LazySimpleSingleton();//11行
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式,线程不安全,如果两个线程同时进入11行,那么会创建两个对象,需要如下,给方法加锁

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public synchronized static LazySimpleSingleton getInstance() {
        //加上空判断保证初只会初始化一次
        if (lazySimpleSingleton == null) {
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式虽然解决了线程安全问题,但是整个方法都是锁定的,性能比较差,所以我们使用方法内加锁的方式解决提高性能

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判断保证初只会初始化一次
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {//11行
                lazySimpleSingleton = new LazySimpleSingleton();
            }
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式如果两个线程同时进入了11行,一个线程a持有锁,一个线程b等待,当持有锁的a线程释放锁之后到return的时候,第二个线程b进入了11行内部,创建了一个新的对象,那么这时候创建了两个线程,对象也并不是单例的。所以我们需要在12行位置增加一个对象判空的操作。

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判断保证初只会初始化一次
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {
                if (lazySimpleSingleton != null) {
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }
}

​ 上述方式还是有风险的,因为CPU执行时候会转化成JVM指令执行:

​ 1.分配内存给对象

​ 2.初始化对象

​ 3.将初始化好的对象和内存地址建立关联,赋值

​ 4.用户初次访问

​ 这种方式,在cpu中3步和4步有可能进行指令重排序。有可能用户获取的对象是空的。那么我们可以使用volatile关键字,作为内存屏障,保证对象的可见性来保证我们对象的单一。

public class LazySimpleSingleton {
    private static volatile LazySimpleSingleton lazySimpleSingleton = null;

    private LazySimpleSingleton() {

    }

    public static LazySimpleSingleton getInstance() {
        //加上空判断保证初只会初始化一次
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {
                if (lazySimpleSingleton != null) {
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }
}

静态内部类单例

​ 还有一种懒汉式单例,利用静态内部类在调用的时候等到外部方法调用时才执行,巧妙的利用了内部类的特性,jvm底层逻辑来完美的避免了线程安全问题

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton() {

    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

​ 这种方式虽然能够完美单例,但是我们如果使用反射的方式如下所示,则会破坏单例

public class LazyInnerClassTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<?> clazz = LazyInnerClassSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);
        Object o1 = c.newInstance();
        Object o2 = LazyInnerClassSingleton.getInstance();
        System.out.println(o1 == o2);
    }
}

​ 怎么办呢,我们需要一种方式控制访问者的行为,通过异常的方式去限制使用者的行为,如下所示


public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton() {
        throw new RuntimeException("不允许构建多个实例");
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

​ 还有一种方式会破坏单例,那就是序列化破坏我们的单例,如下所示

序列化破坏单例

​ 我们写一个序列化的方法来尝试一下上述写法是否是满足序列化的。

public class SeriableSingletonTest {
    public static void main(String[] args) {
        SeriableSingleton seriableSingleton = SeriableSingleton.getInstance();
        SeriableSingleton s2;
        FileOutputStream fos = null;
        FileInputStream fis = null;

        try {
            fos = new FileOutputStream("d.o");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(seriableSingleton);
            oos.flush();
            oos.close();
            fis = new FileInputStream("d.o");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s2 = (SeriableSingleton) ois.readObject();
            ois.close();
            System.out.println(seriableSingleton);
            System.out.println(s2);
            System.out.println(s2 == seriableSingleton);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

​ 为什么序列化会破坏单例呢,我们查看ObjectInputStream的源码

首先,我们查看ObjectInputStream的readObject方法

设计模式-单例模式详解

查看readObject0方法

设计模式-单例模式详解

查看checkResolve(readOrdinaryObject(unshared)方法可以看到

设计模式-单例模式详解

​ 红框内三目运算符内如果desc.isInstantiable()为真就创建新对象,不为空就返回空,此时我们查看desc.isInstantiable()方法

设计模式-单例模式详解

此处cons是设计模式-单例模式详解

如果有构造方法就会返回true,当然我们一个类必然会有构造方法的,所以这就是为什么序列化会破坏我们的单例

那么怎么办呢,我们只需要重写readResolve方法就行了

public class SeriableSingleton implements Serializable {
    private SeriableSingleton() {
        throw new RuntimeException("不允许构建多个实例");
    }

    public static final SeriableSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final SeriableSingleton LAZY = new SeriableSingleton();
    }

    private Object readResolve() {
        return getInstance();
    }
}

为什么重写这个readResolve 的方法就能够避免序列化破坏单例呢

回到上述readOrdinaryObject方法,可以看到有一个hasReadResolveMethod方法

设计模式-单例模式详解

点进去

设计模式-单例模式详解

设计模式-单例模式详解

可以看到 readResolveMethod在此处赋值

设计模式-单例模式详解

也就是我们如果类当中有此方法则在hasReadResolveMethod当中返回的是true

那么会进入readOrdinaryObject的如下部分

设计模式-单例模式详解

并且如下所示,调用我们的readResolve方法获取对象,来保证我们对象是单例的

设计模式-单例模式详解

​ 但是重写readResolve方法,只不过是覆盖了反序列化出来的对象,但是还是创建了两次,发生在JVM层面,相对来说比较安全,之前反序列化出来的对象会被GC回收

注册式单例

枚举单例

​ 枚举式单例属于注册式单例,他把每一个实例都缓存到统一的容器中,使用唯一标识获取实例。也是比较推荐的一种写法,如下所示:

public enum EnumSingleton {
    INSTANCE;
    private Object data;

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

​ 反编译上述文件,可以看到

设计模式-单例模式详解

​ 那么序列化能不能破坏枚举呢

​ 在ObjectInputStream的readObject方法中有针对枚举的判断

设计模式-单例模式详解

设计模式-单例模式详解

上述通过一个类名和枚举名字值来确定一个枚举值。从而枚举在序列化上是不会破坏单例的。

我们尝试使用反射来创建一个枚举对象

public enum EnumSingleton {
    INSTANCE;
    private Object data;

    EnumSingleton() {

    }

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Class clazz = EnumSingleton.class;
        try {
            Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
            c.newInstance("dd", 1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

抛出异常

设计模式-单例模式详解

查看Constructor源码可以看到

设计模式-单例模式详解

可以看到jdk层面如果判断是枚举会抛出异常,所以枚举式单例是一种比较推荐的单例的写法。

容器式单例

这种方式是通过容器的方式来保证我们对象的单例,常见于Spring的IOC容器

public class ContainerSingleton {
    private ContainerSingleton() {

    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className) {
        if (!ioc.containsKey(className)) {
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();//12
                ioc.put(className, obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }
        return ioc.get(className);
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    Object o = ContainerSingleton.getBean("com.zzjson.singleton.register.ContainerSingleton");
                    System.out.println(o + "");
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        executorService.shutdown();

    }

}

这种方式测试可见

设计模式-单例模式详解

出现了几次不同对象的情况因为我们线程在12行可能同时进入,这时候我们需要加一个同步锁如下,这样创建对象才是只会创建一个的

public class ContainerSingleton {
    private ContainerSingleton() {

    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(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;
            }
        }
        return ioc.get(className);
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        final CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    Object o = ContainerSingleton.getBean("com.zzjson.singleton.register.ContainerSingleton");
                    System.out.println(o + "");
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        executorService.shutdown();

    }

}

ThreadLocal单例

这种方式只能够保证在当前线程内的对象是单一的

public class ThreadLocalSingleton {
    private ThreadLocalSingleton() {
    }

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

    };

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

文中源码地址设计模式

点赞
收藏
评论区
推荐文章
3A网络 3A网络
3年前
Golang 常见设计模式之单例模式
之前我们已经看过了Golang常见设计模式中的装饰和选项模式,今天要看的是Golang设计模式里最简单的单例模式。单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。Go语言实现单例模式的方法有很多种,下面我们就一起来看一下。饿汉式饿汉式实现单例模式非
Wesley13 Wesley13
3年前
java设计模式1
1:单例模式简介  单例模式是一种常用的软件设计模式,它确保某个类只有一个实例,而且自行实例化并向整个系统提供唯一的实例。总而言之就是在系统中只会存在一个对象,其中的数据是共享的  特点:    单例类只能有一个实例,所以一般会用static进行修释。    单例类必须自己创建自己的唯一实例。也就是在类中要new一个自己。    单例类必
Wesley13 Wesley13
3年前
java 23种设计模式(五、单例模式)
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。单例模式的结构  单例模式的特点:单例类只能有一个实例。单例类必须自己创建自己的唯一实例。单例类必须给所有其他对象提供这一实例。  饿汉式单例类publicclassEagerSingleton
红烧土豆泥 红烧土豆泥
4年前
创建型模式之单例设计模式
什么是单例设计模式?顾名思义,只有一个实例。单例模式它主要是确保一个类只有一个实例,并且可以提供一个全局的访问点。废话少说,直接上干货了单例模式之饿汉式所谓饿汉式,顾名思义,“它很饿”。所以说,它一旦被加载进来,就会直接实例化一个对象。例如:languageclassSingleton{privatestaticfin
Wesley13 Wesley13
3年前
JAVA设计模式之单例设计模式
    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。  在JAVA中实现单例,必须了解JAVA内存机制,JAVA中实例对象存在于堆内存中,若要实现单例,必须满足两个条件:  1.限制类实例化对象。即只能产生一个对象。
Wesley13 Wesley13
3年前
Java单例模式
什么是单例模式  单例模式是在程序中,一个类保证只有一个实例,并提供统一的访问入口。为什么要用单例模式节省内存节省计算如对象实例中的一样的,那就不用每次都创建一个对象方便管理因为单例提供一个统一的访问入口,不需要创建N多个对象,很多工具类都用了单例实现,如日志、字符串工具类
Wesley13 Wesley13
3年前
PHP单例模式(精讲)
首先我们要明确单例模式这个概念,那么什么是单例模式呢?单例模式顾名思义,就是只有一个实例。作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类我们称之为单例类。单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例
Wesley13 Wesley13
3年前
(面试常问)4种单例设计模式的总结(内含代码以及分析)
单例设计模式:  单例模式,是一种常见的软件设计模式.在它的核心结构中只包含了一个被称为单例的特殊类.通过单例模式可以保证系统中只有该类的一个实例对象.优点:  实例控制:单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问的是唯一的实例   灵活性:因为类控制了实例化过程,所以类可以很灵活的更改实
Stella981 Stella981
3年前
C#设计模式(1)——单例模式(Singleton)
单例模式即所谓的一个类只能有一个实例,也就是类只能在内部实例一次,然后提供这一实例,外部无法对此类实例化。单例模式的特点:1、只能有一个实例;2、只能自己创建自己的唯一实例;3、必须给所有其他的对象提供这一实例。普通单例模式(没有考虑线程安全)  ///<summary///单例模式
Stella981 Stella981
3年前
Python设计模式
对于很多开发人员来说,单例模式算是比较简单常用、也是最早接触的设计模式了,仔细研究起来单例模式似乎又不像看起来那么简单。我们知道单例模式适用于提供全局唯一访问点,频繁需要创建及销毁对象等场合,的确方便了项目开发,但是单例模式本身也有一定的局限性,如果滥用则会给后续软件框架的扩展和维护带来隐患。单例模式的实现有很多种,应用场合也各有不同,但必须保证实例唯一
Wesley13 Wesley13
3年前
23种设计模式(1):单例模式
定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。类型:创建类模式类图:!23种设计模式(1):单例模式第1张|快课网(http://static.oschina.net/uploads/img/201407/05200605_0dij.gif"23种设计模式(1):单例模式
逻辑织光使
逻辑织光使
Lv1
阶下青苔与红树,雨中寥落月中愁。
文章
3
粉丝
0
获赞
0