设计模式之单例模式

1.单例模式介绍

1.1背景

因为有些对象只需要一个,比如线程池、缓存、对话框等程序对象,如果创建出多个实例就会导致许多问题的产生,例如:程序行为异常,资源使用过量,或者不一致的结果等。为了解决这一问题,单例模式出现了。

1.2优点

只创建一个实例,并且在使用的时候才创建,处理非常耗资源且的对象非常合适,因为如果在启动时就创建耗资源的对象,但是在程序开始执行到使用它之前这段时间又一直没有使用它,那就非常浪费资源了。同时也能避免一个全局使用的类频繁地创建与销毁。

1.3缺点

没有接口,无法继承,违背了类的单一职责原则,一个类只应该做一件事。它不止负责管理自己的实例(并提供全局访问),还在应用程序中承担角色。

1.4应用场景

配置文件访问类,不用每次使用时都new一个。
数据库连接池 保证项目中只有一个连接池存在。

2.单例模式五种实现

2.1饿汉式

/*
 * @author smilesboy
 * @Date 2020/5/10
 * 饿汉式
 */

public class Singleton {
	/**
     * 类变量在类准备阶段就初始化了然后放在<clinit>构造方法中
     * 一旦外部调用了静态方法,那么就会初始化完成。
     * 一个类的<clinit>只会执行一次 保证多线程情况下不会创建多个实例
     */
	private static final Singleton INSTANCE = new Singleton();
	
	/*
	 * 构造函数私有化
	 */
	private Singleton() {}
	
	/**
     *  提供公共方法以获取实例对象
     * @return instance 实例对象
     */
	public static Singleton getInstance() {
		return INSTANCE;
	}
}

这种方式创建的单例模式,类加载是就创建,由classloder保证了线程安全。

2.2静态内部类

/*
 * @author smilesboy
 * @Date 2020/5/10
 * 静态内部类
 */

public class Singleton {
	
	private static class SingletonHolder {
		/**
	     * 类变量在类准备阶段就初始化了然后放在<clinit>构造方法中
	     * 一旦外部调用了静态方法,那么就会初始化完成。
	     * 一个类的<clinit>只会执行一次 保证多线程情况下不会创建多个实例
	     */
		private static final Singleton INSTANCE = new Singleton();
	}
	
	/*
	 * 构造函数私有化
	 */
	private Singleton() {}
	
	/**
     * 提供公共方法以获取实例对象
     * @return instance 实例对象
     */
	public static Singleton getInstance() {
		return SingletonHolder.INSTANCE;
	}
}

这种方式实现的单例模式,实现了类使用时才加载,由classloder保证了线程安全。

饿汉式/静态内部类是如何保证线程安全的,在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

2.3懒汉式

/*
 * @author smilesboy
 * @Date 2020/5/10
 * 懒汉式
 */

public class Singleton {
	private static Singleton instance;
	
	private Singleton() {}
	
	/**
     * synchronized 保证线程安全 但效率低
     *
     * @return instance单例对象
     */
	public static synchronized Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

这种方式实现的单例模式,实现了使用类时才创建,使用synchronized保证线程安全,但效率低。

2.4双重校验锁

/*
 * @author smilesboy
 * @Date 2020/5/10
 * 双重校验锁
 */

public class Singleton {
    /**
     * volatile关键字禁止指令重排序
     * 保证多线程下不会获取到未完全初始化的实例
     */
	private volatile static Singleton instance;
	
	private Singleton() {}
	
    /**
     * 双重if校验 缩小synchronized代码块范围
     * 若instance不为空 就可直接return
     *
     * @return instance 实例对象
     */
	public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					//非原子操作
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

这种方式实现的单例:实现了类使用时才创建实例。synchronized保证了线程安全,volatile禁止指令重排序保证了多线程获取时不为空,JDK1.5以上才行。具体细节请参考volatile关键字在单例模式(双重校验锁)中的作用

2.5枚举

/*
 * @author smilesboy
 * @Date 2020/5/10
    * 枚举式
 *避免反射攻击
   * 序列化及反序列化安全
 */

enum EnumSingleton {
	/**
             * 定义一个枚举的元素,它就是singleton的一个实例
     */
	INSTANCE;
	public void doOtherThing() {
		System.out.println("枚举生成单例模式");
	}
}

public class Singleton {
	public static void main(String[] args) {
		EnumSingleton singleton = EnumSingleton.INSTANCE;
		singleton.doOtherThing();
	}
}

这种方式也是《Effective Java 》的作者推荐的方式。为什么会推荐这种方式呢?因为前面4种都存在一个序列化和反序列化时的安全问题。将单例对象序列化后,在反序列化时会重新创建一个单例对象,违背了单例模式的初衷。而枚举式单例则没有这个问题。详细信息请查看为什么要用枚举实现单例模式(避免反射、序列化问题)

当我们使用单例模式时可以根据不同场合选择具体的实现方式,一般情况下比较常使用的是静态内部类或者双重校验锁方式。

参考
Java设计模式(一)---单例模式
为什么要用枚举实现单例模式(避免反射、序列化问题)

posted @ 2020-05-10 16:45  smilesboy  阅读(115)  评论(0)    收藏  举报