单例模式(singleton Pattern)
1. 单例模式
单例模式是设计设计模式中使用最为普遍的模式之一,它是一种对象创建模式。用于产生一个对象的具体实例,可以确保系统中一个类只产生一个实例
。
单例模式的定义
- 单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:
- Ensure a class has only one instance, and provide a globalpoint of access to it.(确保某一个类只有一个实例, 自行实例化并向整个系统提供这个实例。)
使用单例模式的优缺点
优点:
-
由于单例模式只生成一个实例,所以减少了系统的性能开销。(特别是对于频繁创建和销毁或者重量级对象)
-
单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
-
单例模式可以减轻GC的压力,缩短GC停顿时间。
-
减少对内存的占用。
缺点: -
单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。(违背开闭原则,对扩展开放,对修改关闭)
-
单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。
2. 线程不安全的单例
/**
* 最简单的单例
*/
public class SimpleSingleton {
private static SimpleSingleton instance;
private SimpleSingleton() {
System.out.println("我是独生子,我是帮程序员爸爸/妈妈做事的。");
}
/**
* 获取单例对象
* @return
*/
public static SimpleSingleton getInstance() {
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
/**
* 处理其他业务的方法
*/
public void doSomething(){
System.out.println("这些事情只有我能做");
}
}
测试:
@Test
public void test01(){
//获取对象
SimpleSingleton instance = SimpleSingleton.getInstance();
//处理其他业务
instance.doSomething();
}
运行结果:
特点
上述代码有两个特点:
- 所有的单例构造器都要被声明为私有的(private)
- 通过声明静态(static)方法实现全局访问获得该单例实例。
线程不安全
上述代码在并发量比较小的程序中基本上是不会有问题的。但是在高并发情况下,这段代码就会产生线程安全问题。
我们对代码稍作修改。
/**
* 最简单的单例
*/
public class SimpleSingleton {
private static SimpleSingleton instance;
private SimpleSingleton() {
System.out.println(Thread.currentThread().getName()+":我是独生子,我是帮程序员爸爸/妈妈做事的。");
}
/**
* 获取单例对象
* @return
*/
public static SimpleSingleton getInstance(){
try {
if (instance == null) {
//模拟一个耗时比较长的创建过程
Thread.currentThread().sleep(1000);
instance = new SimpleSingleton();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
/**
* 处理其他业务的方法
*/
public void doSomething(){
System.out.println("这些事情只有我能做");
}
}
主要变化实在判空后,创建对象之前添加了Thread.currentThread().sleep(1000);
来模拟一个复杂且耗时较长的对象创建过程。
测试:
@Test
public void test02() throws InterruptedException {
Thread thread1 = new Thread(() -> {
//获取对象
SimpleSingleton instance = SimpleSingleton.getInstance();
//处理其他业务
instance.doSomething();
System.out.println(instance);
});
Thread thread2 = new Thread(() -> {
//获取对象
SimpleSingleton instance = SimpleSingleton.getInstance();
//处理其他业务
instance.doSomething();
System.out.println(instance);
});
//模拟并发
thread1.start();
thread2.start();
//等待其他线程结束
Thread.currentThread().sleep(2000);
System.out.println(SimpleSingleton.getInstance());
}
- 在程序开始时,由于之前getInstance方法没有被调用过,instance为null。
- 因为业务上的需要,thread1和thread2都需要获取instance,并通过instance的doSomething方法来执行一些业务操作。thread1和thread2几乎同时调用了getInstance方法。
- instance 为null,第一个线程需要创建一个实例。由于创建实例的过程比较复杂、耗时比较长。在第二个线程执行判断
if (instance == null)
时,对象实例没有被创建出来,instance 也为null,所以第二个线程也需要创建一个实例。 - 一个单例对象被创建了两次,这显然不合理。违背了单例模式的定义(确保某一个类只有一个实例)。
if (instance == null) {
//模拟一个耗时比较长的创建过程
Thread.currentThread().sleep(1000);
instance = new SimpleSingleton();
}
- 为了就解决线程安全的问题,产生了饿汉式单例和懒汉式单例。
3.饿汉式单例
public class EHSingleton {
private static final EHSingleton instance = new EHSingleton();
private EHSingleton() {
System.out.println("我才是真正的的独生子");
}
/**
* 获取单例对象
* @return
*/
public static EHSingleton getInstance(){
return instance;
}
/**
* 处理其他业务的方法
*/
public void doSomething(){
System.out.println("这些事情只有我能做");
}
}
测试
@Test
public void test03(){
//我是饿汉式,我要提前加载
EHSingleton instance = EHSingleton.getInstance();
instance.doSomething();
}
- 为了解决线程安全问题,一共有两种方案,其中一种就是在类加载时就把单例实例创建出来(饿汉式单例)。实例创建之迫切,就像一个饿汉看到这碗面有大又圆……
- 饿汉式的特点:
- 类加载是就创建实例(没有进行懒加载)
- 避免了懒汉式使用同步锁和判断实力是否 创建的额外检查,执行效率高
- 线程安全
4.懒汉式单例
单检锁的懒汉式
public class LHSingletonOne {
private static LHSingletonOne instance = null;
private LHSingletonOne(){
System.out.println("我是懒汉使用时加载");
}
public static LHSingletonOne getInstance(){
//同步锁,解决线程安全问题
synchronized (LHSingletonOne.class){
if (instance == null){
instance = new LHSingletonOne();
}
}
return instance;
}
public void doSomething(){
System.out.println("懒汉起来干活了");
}
}
双检锁的懒汉式
public class LHSingletonTwo {
private static LHSingletonTwo instance = null;
private LHSingletonTwo(){
System.out.println("双检锁懒汉式效率高");
}
public static LHSingletonTwo getInstance(){
if (instance==null){
synchronized (LHSingletonTwo.class){
if (instance==null){
instance = new LHSingletonTwo();
}
}
}
return instance;
}
public void doSomething(){
System.out.println("双检锁懒汉式效率高");
}
}
懒汉式的特点
- 线程安全
- 第一次使用时,才创建单例实例-懒加载
- 使用同步锁和检查对象是否被创建,效率比较慢
- 双检锁只有第一次getInstance被调用时,需要使用同步锁;单检锁每次调用getInstance都会使用同步锁。所以双检锁懒汉式比单检锁懒汉式效率更高
5. 使用静态内部类来实现单例
如果你想要实现延迟加载,又不想让同步锁影响程序的性能,可以使用静态内部类来显现。
public class StaticSingleton {
private StaticSingleton(){
System.out.println("StaticSingleton is create");
}
//静态内部类实现延迟加载
private static class SingletonHolder{
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance(){
return SingletonHolder.instance;
}
public void doSomething(){
System.out.println("doSomething……");
}
}
- 延迟加载
- 没有同步锁
- 没有线程安全问题
- 当StaticSingleton被加载时,其内部类并不会被初始化;而当 getInstance方法被调用时才会加载SingletonHolder,从而初始化instance。