【我的设计模式学习笔记】单例模式(Singleton)

一、前言

有些时候,允许自由创建某个类的实例是没有意义,还可能造成系统性能下降(因为创建对象所带来的系统开销问题)。例如整个Windows系统只有一个窗口管理器,只有一个回收站等。在Java EE应用中可能只需要一个数据库引擎访问点,Hibernate访问时只需要一个SessionFactory实例,如果在系统中为它们创建多个实例就没有太大的意义。

如果一个类始终只能创建一个实例,则这个类被称为单例类,这种模式就被称为单例模式。

对Spring框架而言,可以在配置Bean实例时指定scope="singleton"类配置单例模式。不仅如此,如果配置<bean .../>元素时没有指定scope属性,则该Bean实例默认是单例的行为方式。

Spring推荐将所有业务逻辑组件、DAO组件、数据源组件等配置成单例的行为方式,因为这些组件无须保存任何用户状态,故所有客户端都可以共享这些业务逻辑组件、DAO组件,因此推荐奖这些组件配置成单例的行为方式。

二、基本定义

在阎宏博士的《JAVA与模式》一书中开头是这样描述单例模式的:

  作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

单例模式有以下几个特点:

 

    1. 单例类只能有一个实例。
    2. 单例类必须自己创建自己的唯一实例。
    3. 单例类必须给所有其他对象提供这一实例。

单例模式的模式结构:

 单例模式可以说是最简单的设计模式了,它仅有一个角色Singleton。

 

 

         Singleton:单例。

 

 

三、单例模式的具体实现

1、饿汉式实现单例模式

public class EagerSingleton {
	private EagerSingleton() {
	}

	private static EagerSingleton instance = new EagerSingleton();

	public static EagerSingleton getInstance() {
		return instance;
	}
}

上面的例子中,在这个类被加载时,静态变量instance会被初始化,此时类的私有构造子会被调用。这时候,单例类的唯一实例就被创建出来了。

  饿汉式其实是一种比较形象的称谓。既然饿,那么在创建对象实例的时候就比较着急,饿了嘛,于是在装载类的时候就创建对象实例。

 饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间。

2、懒汉式实现单例模式

public class LazySingleton {
	private LazySingleton() {
	}

	private static LazySingleton instance = null;

	public static synchronized LazySingleton getInstance() {
		if (instance==null) {
			instance=new LazySingleton();
		}
		return instance;
	}
}

上面的懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。
  懒汉式其实是一种比较形象的称谓。既然懒,那么在创建对象实例的时候就不着急。会一直等到马上要使用对象实例的时候才会创建,懒人嘛,总是推脱不开的时候才会真正去执行工作,因此在装载对象的时候不创建对象实例。

懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。

  由于懒汉式的实现是线程安全的,这样会降低整个访问的速度,而且每次都要判断。那么有没有更好的方式实现呢?

3、双重检查加锁实现单例模式

可以使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?

  所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

  “双重检查加锁”机制的实现会使用关键字volatile它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

  注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。

public class DoubleCheckLockSingleton {
	private DoubleCheckLockSingleton() {
	}

	/**
	 * 关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,
	 * 所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量
	 */
	private static volatile DoubleCheckLockSingleton instance = null;

	public static DoubleCheckLockSingleton getInstance() {
		//先检查实例是否存在,如果不存在才进入下面的同步块
		if (instance == null) { 
			 //同步块,线程安全的创建实例
			synchronized (DoubleCheckLockSingleton.class) { 
				//再次检查实例是否存在,如果不存在才真正的创建实例
				if (instance == null) {
					instance = new DoubleCheckLockSingleton();
				}
			}
		}
		return instance;
	}
}

这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

  由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

(更多关于双重检查锁定单例可以参考:我的Java开发学习之旅------>Java双重检查锁定及单例模式详解

  根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?

4、静态内部类实现单例模式

public class InnerClassSingleton {
	private InnerClassSingleton() {
	}

	/**
	 * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,
	 *  而且只有被调用到时才会装载,从而实现了延迟加载。
	 */
	private static class InnerClassSingletonHolder {
		/**
		 * 静态初始化器,由JVM来保证线程安全
		 */
		private static InnerClassSingleton instance = new InnerClassSingleton();
	}

	public static InnerClassSingleton getInstance() {
		return InnerClassSingletonHolder.instance;
	}
}

 

  当getInstance方法第一次被调用的时候,它第一次读取InnerClassSingletonHolder.instance,导致InnerClassSingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建InnerClassSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

 这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

5、枚举实现单例模式

public enum EmunSingleton {
	/**
          * 定义一个枚举的元素,它就代表了EmunSingleton的一个实例。
        */
	INSTANCE;
}

使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,

是更简洁、高效、安全的实现单例的方式。但是失去了类的一些特性,没有延迟加载。
 

6、利用AtomicRefrence实现单例模式

最近在读RxJava的源码时,见到了一种新的单例模式,可能是自己见识太少,之前对这种方式真的没见过,也可以说闻所未闻。由此引发了一些对Atomic**相关原子类的思考的研究,先看代码:

import java.util.concurrent.atomic.AtomicReference;


public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    private Singleton() {}

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
    }

}

参考链接:

https://www.jianshu.com/p/d9c9c947678a

https://www.jianshu.com/p/6df1b0e7be7f

 

四、单例模式的优缺点

1、优点

  • 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

2、缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

 

五、单例模式的使用场景

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  • 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。

 

============================================================

  作者:欧阳鹏  欢迎转载,与人分享是进步的源泉!

  转载请保留原文地址http://blog.csdn.net/ouyang_peng

============================================================

 

 

PS: 今天看到一篇不错的公众号文章,现在copy下来。

下面的文章copy自:月薪5万程序员眼中的单例模式

https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw==&mid=2247484702&idx=1&sn=064399af930e9915023b2805e84cee7d&chksm=fbb28ce0ccc505f6ca630d91a5b777584b1e46f07810ca822d516a528dbe5148fe5a8fe39ebb&mpshare=1&scene=1&srcid=0505eFGES83LBWYanUA8E2da#rd

作者:孤独烟,来自:http://rjzheng.cnblogs.com/

引言

 

其实写这篇文章之前,我犹豫了一下,毕竟单例大家都知道,写这么一篇文章会不会让人觉得老掉牙。后来想想,就当一种记录吧。先来一副漫画吧,如下图所示

 

 

ok,我们回顾下小灰的遭遇,上述漫画所提出的那些问题主要有以下三点:

  1. 为什么静态内部类的单例模式是最推荐的?

  2. 如何在反射的情况下保证单例?

  3. 如何在反序列化中保证单例?

 

针对上述三个问题有了这篇文章,以一种循序渐进的方式,引出最后一种单例设计模式,希望对大家能够有所帮助。

单例设计模式

1、饿汉式

这种其实大家都懂,不多说,上代码。

package singleton;
public class Singleton1 {
    private static Singleton1 instance = new Singleton1();
    private Singleton1 (){}
    public static Singleton1 getInstance() {
        return instance;
    }
}
优点就是线程安全啦,缺点很明显,类加载的时候就实例化对象了,浪费空间。于是乎,就提出了懒汉式的单例模式

2、懒汉式

(1)懒汉式v1

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

然而这一版线程是不安全的,于是乎为了线程安全,就在getInstance()方法上加synchronized修饰符,于是getInstance()方法如下所示

public static synchronized LazySingleton1 getInstance() {
        if (instance == null) {
            instance = new LazySingleton1();
        }
        return instance;
    }

然而,将synchronized加在方法上性能大打折扣(syncrhonized会造成线程阻塞),于是乎又提出一种双重校验锁的单例设计模式,既保证了线程安全,又提高了性能。双重校验锁的getInstance()方法如下所示

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

 

(2)懒汉式v2

懒汉式v1的最后一个双重校验锁版,不管性能再如何优越,还是使用了synchronized修饰符,既然使用了该修饰符,那么对性能多多少少都会造成一些影响,于是乎懒汉式v2版诞生。不过在讲该版之前,我们先来复习一下内部类的加载机制,代码如下

package test;
public class OuterTest {
    static {
        System.out.println("load outer class...");
    }
    // 静态内部类
    static class StaticInnerTest {
        static {
            System.out.println("load static inner class...");
        }
        static void staticInnerMethod() {
            System.out.println("static inner method...");
        }
    }
    public static void main(String[] args) {
        OuterTest outerTest = new OuterTest(); // 此刻其内部类是否也会被加载?
        System.out.println("===========分割线===========");
        OuterTest.StaticInnerTest.staticInnerMethod(); // 调用内部类的静态方法
    }
}

输出如下

load outer class...
===========分割线===========
load static inner class...
static inner method

因此,我们有如下结论

  1. 加载一个类时,其内部类不会同时被加载。

  2. 一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。。

基于上述结论,我们有了懒汉式V2版,代码如下所示

package singleton;
public class LazySingleton2 {
    private LazySingleton2() {
    }
    static class SingletonHolder {
        private static final LazySingleton2 instance = new LazySingleton2();
    }
    public static LazySingleton2 getInstance() {
        return SingletonHolder.instance;
    }
}

由于对象实例化是在内部类加载的时候构建的,因此该版是线程安全的(因为在方法中创建对象,才存在并发问题,静态内部类随着方法调用而被加载,只加载一次,不存在并发问题,所以是线程安全的)。

另外,在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。当LazySingleton2类加载的时候,其静态内部类SingletonHolder并没有被加载,因此instance对象并没有构建。

而我们在调用LazySingleton2.getInstance()方法时,内部类SingletonHolder被加载,此时单例对象才被构建。因此,这种写法节约空间,达到懒加载的目的,该版也是众多博客中的推荐版本。

ps:其实枚举单例模式也有类似的性能,但是因为可读性的原因,并不是最推荐的版本。

(3)懒汉式v3

然而,懒汉式v2版在反射的作用下,单例结构是会被破坏的,测试代码如下所示

package test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import singleton.LazySingleton2;
/**
 * @author zhengrongjun
 */
public class LazySingleton2Test {
    public static void main(String[] args) {
        //创建第一个实例
        LazySingleton2 instance1 = LazySingleton2.getInstance();
        //通过反射创建第二个实例
        LazySingleton2 instance2 = null;
        try {
            Class<LazySingleton2> clazz = LazySingleton2.class;
            Constructor<LazySingleton2> cons = clazz.getDeclaredConstructor();
            cons.setAccessible(true);
            instance2 = cons.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //检查两个实例的hash值
        System.out.println("Instance 1 hash:" + instance1.hashCode());
        System.out.println("Instance 2 hash:" + instance2.hashCode());
    }
}

输出如下

Instance 1 hash:1694819250
Instance 2 hash:1365202186

根据哈希值可以看出,反射破坏了单例的特性,因此懒汉式V3版诞生了

package singleton;
public class LazySingleton3 {
    private static boolean initialized = false;
    private LazySingleton3() {
        synchronized (LazySingleton3.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("单例已被破坏");
            }
        }
    }
    static class SingletonHolder {
        private static final LazySingleton3 instance = new LazySingleton3();
    }
    public static LazySingleton3 getInstance() {
        return SingletonHolder.instance;
    }
}

此时再运行一次测试类,出现如下提示

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at test.LazySingleton3Test.main(LazySingleton3Test.java:21)
Caused by: java.lang.RuntimeException: 单例已被破坏
    at singleton.LazySingleton3.<init>(LazySingleton3.java:12)
    ... 5 more
Instance 1 hash:359023572

这里就保证了,反射无法破坏其单例特性

(3)懒汉式v4

在分布式系统中,有些情况下你需要在单例类中实现 Serializable 接口。这样你可以在文件系统中存储它的状态并且在稍后的某一时间点取出。

让我们测试这个懒汉式V3版在序列化和反序列化之后是否仍然保持单例。

先将

public class LazySingleton3

修改为

public class LazySingleton3 implements Serializable 

上测试类如下

package test;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import singleton.LazySingleton3;
public class LazySingleton3Test {
    public static void main(String[] args) {
        try {
            LazySingleton3 instance1 = LazySingleton3.getInstance();
            ObjectOutput out = null;
            out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
            out.writeObject(instance1);
            out.close();
            //deserialize from file to object
            ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
            LazySingleton3 instance2 = (LazySingleton3) in.readObject();
            in.close();
            System.out.println("instance1 hashCode=" + instance1.hashCode());
            System.out.println("instance2 hashCode=" + instance2.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出如下

instance1 hashCode=2051450519
instance2 hashCode=1510067370

显然,我们又看到了两个实例类。为了避免此问题,我们需要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。

因此,我们提供懒汉式V4版代码如下

package singleton;
import java.io.Serializable;
public class LazySingleton4 implements Serializable {
    private static boolean initialized = false;
    private LazySingleton4() {
        synchronized (LazySingleton4.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("单例已被破坏");
            }
        }
    }
    static class SingletonHolder {
        private static final LazySingleton4 instance = new LazySingleton4();
    }
    public static LazySingleton4 getInstance() {
        return SingletonHolder.instance;
    }
    private Object readResolve() {
        return getInstance();
    }
}

此时,在运行测试类,输出如下

instance1 hashCode=2051450519
instance2 hashCode=2051450519

这表示此时已能保证序列化和反序列化的对象是一致的

总结

本文给出了多个版本的单例模式,供我们在项目中使用。实际上,我们在实际项目中一般从懒汉式v2、懒汉式v3、懒汉式v4中,根据实际情况三选一即可,并不是非要选择懒汉式v4作为单例来实现。最后,希望大家有所收获。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

字节卷动

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值