什么是单利模式
单例模式,顾名思义就是程序在运行的过程中,有且只有一个实例。
为什么需要它
有些对象我们只需要一个,像是线程池,缓冲等,这类对象只能有一个实例,一旦产生多个实例就会出现问题。所以,我们必须找到一种方法来确保我们的代码中只有一个实例。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
单例模式的定义
确保这个类在内存中只会存在一个对象,而且自行实例化并向整个应用系统提供这个实例。
单例模式的应用场景
一般创建一个对象需要消耗过多的资源,如:访问I0和数据库等资源或者有很多个地方都用到了这个实例。
单例模式的几种基本写法:
饿汉式
public class Singleton {
// 将构造函数私有化,这样就无法通过new 创建对象了
private Singleton(){}
// 静态化,在类加载的时候就将对象创建好
private static final Singleton INSTANCE=new Singleton();
public static Singleton getInstance(){
return INSTANCE;
}
}
饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
优点:简单、线程安全。
缺点:不需要的时候就加载了,造成资源浪费。
懒汉式
public class Singleton{
// 将构造函数私有化,这样就无法通过new 创建对象了
private Singleton(){}
// 将类实例设置为null,在需要的时候加载,节省资源
private static Singleton instance = null;
public static Singleton newInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题,实现如下。
懒汉式适用场景
如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择
缺点: 效率低,第一次加载需要实例化,反应稍慢。每次调用getInstance方法都会进行同步,消耗不必要的资源。
上边的两种是最常见的,顾名思义懒汉式和饿汉式,一个是拿时间换空间,一个是拿空间换时间,懒汉式只有我需要他的时候才去加载它,懒加载机制,饿汉式不管需不需要我先加载了再说,先在内存中开辟一块空间,占用一块地方,等用到了直接就拿来用.这两种是最基本的单例模式。
双重检查单例(DCL实现单例)
public class Singleton {
private Singleton(){}
private volatile static Singleton instance;
public static Singleton getInstance(){
if(instance == null){
// 为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized同步锁,锁住整个类(注意,这里不能使用对象锁)
synchronized (Singleton.class){
// 进入Synchronized 临界区以后,还要再做一次判空。
// 因为当两个线程同时访问的时候,线程A构建完对象,
// 线程B也已经通过了最初的判空验证,不做第二次判空的话,
// 线程B还是会再次构建instance对象
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
优点:线程安全;延迟加载;效率较高
缺点:第一次加载时会稍慢,jdk1.5之之前有可能会加载会失败
这种写法估计是我们在开发中最常用的,这次代码的亮点是是在getInstance()方法中进行了双重的判断,第一层判断的主要避免了不必要的同步,第二层判断是为了在null的情况下再去创建实例;举个简单的列子:假如现在有多个线程同时触发这个方法: 线程A执行到nstance = new Singleton(),它大致的做了三件事:
(1)、给Singleton实例分配内存,将函数压栈,并且申明变量类型。
(2)、初始化构造函数以及里面的字段,在堆内存开辟空间。
(3)、将instance对象指向分配的内存空间。
这种写法也并不是保证完全100%的可靠,由于java编译器允许执行无序,并且jdk1.5之前的jvm(java内存模型)中的Cache,寄存器到主内存的回写顺序规定,第二个和第三个执行是无法保证按顺序执行的,也就是说有可能1-2-3也有可能是1-3-2; 这时假如有A和B两条线程,A线程执行到3的步骤,但是未执行2,这时候B线程来了抢了权限,直接取走instance这时候就有可能报错。
静态内部内实现单例
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
这种方式不仅确保了线程的安全性,也能够保证对象的唯一性,同时也是延迟加载,很多技术大牛也是这样推荐书写。
枚举实现单例
// 有了enum,JVM会阻止反射获取枚举的私有构造方法。
public enum SingletonEnum {
INSTANCE;
public SingletonEnum getSingleton() {
return INSTANCE;
}
}
优点:相对于其他单例来说枚举写法最简单,并且任何情况下都是单例的,JDK1.5之后才有的。
作者:安仔夏天勤奋
链接:https://www.jianshu.com/p/12d1a151982e
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。