设计模式
单例模式
定义与特点
单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 标准中的 Servlet Context 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
单例模式有 3 个特点:
-
单例类只有一个实例对象;
-
该单例对象必须由单例类自行创建;
-
单例类对外提供一个访问该单例的全局访问点。
这里主要介绍:饿汉式单例、懒汉式单例、静态内部类、枚举。
单例模式的结构
单例模式的主要角色如下:
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类
一、饿汉式单例
public class EagerSingleton {
public static void main(String[] args) {
Eager eager = Eager.getInstance();
Eager eager2 = Eager.getInstance();
System.out.println(eager);
System.out.println(eager == eager2);
}
}
/**
* 饿汉式
*/
class Eager {
private static final Eager eager = new Eager();
private Eager() {
}
public static Eager getInstance() {
return eager;
}
}
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance() 方法之前单例已经存在了,是线程安全的
二、懒汉式单例
public class LazySingleton {
public static void main(String[] args) {
TwinCheck tc = TwinCheck.getInstance();
TwinCheck tc2 = TwinCheck.getInstance();
System.out.println(tc == tc2);
}
}
/**
* 三、双重校验
*/
class TwinCheck {
/**
* 注意: volatile 关键字不可省
*/
private volatile static TwinCheck twinCheck= null;
/**
* 构造方法私有,避免在外部实例化对象
*/
private TwinCheck() {
}
public static TwinCheck getInstance() {
if (null == twinCheck) {
synchronized (TwinCheck.class) {
if (null == twinCheck) {
twinCheck = new TwinCheck();
}
}
}
return twinCheck;
}
}
/**
* 二、同步方法
*/
class SyncMethod {
private static SyncMethod syncMethod = null;
/**
* 构造方法私有,避免在外部实例化对象
*/
private SyncMethod() {
}
public static synchronized SyncMethod getInstance() {
if (syncMethod == null) {
syncMethod = new SyncMethod();
}
return syncMethod;
}
}
/**
* 一、非线程安全
*/
class Singleton {
private static Singleton single = null;
private Singleton() {
}
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance() 方法时才去创建这个单例。
注意以上三种懒汉式单例中,建议试用第三种【双重校验】,第一种非线程安全,第二种效率很低【synchronized 方法】。
双重校验单例:线程安全、但是加入了 synchronized 同步块,对性能会有一定影响。
注意
volatile
是必不可少的,用于禁止指令重排:由于
twinCheck = new TwinCheck()
不是一个原子性操作,将其步骤分为:
- 1、分配内存空间
- 2、执行构造方法,初始化对象
- 3、把这个对象指向分配的内存空间
按我们的想法行顺序是:1->2->3,但指令重排后,执行顺序可能是 1->3->2【并不会影响执行结果】,单线程下这是完全没有问题的。
但是,当多线程的情况下就可能照成一个问题:
线程 A 通过
getInstance()
创建实例对象,这时由于指令重排,指令的执行顺序为 1->3->2,此时线程 A 执行完 3【将对象指向了内存空间,对象不再为 null】 还没有执行 2;线程 B 进来通过getInstance()
方法获取对象,在判断null == twinCheck
时,由于操作 3 已经执行完毕,twinCheck
对象不为空,返回twinCheck
实例;但此时操作 2 还未执行,twinCheck
对象还没初始化,这时获取到的twinCheck
对象就会存在问题。所以需要加入
volatile
禁止指令重排,避免产生以上的问题。
三、静态内部类
/**
* 静态内部类
*/
public class StaticInnerClass {
private StaticInnerClass() {
}
private static class LazyModel {
private static StaticInnerClass INSTANCE = new StaticInnerClass();
}
public static StaticInnerClass getInstance() {
return LazyModel.INSTANCE;
}
public static void main(String[] args) {
StaticInnerClass sic = StaticInnerClass.getInstance();
StaticInnerClass sic2 = StaticInnerClass.getInstance();
System.out.println(sic == sic2);
}
}
既实现了线程安全,又避免了同步带来的性能影响。
四、枚举
/**
* 枚举
*/
public enum EnumSingleton {
INSTANCE(0, "t");
EnumSingleton(int code, String message) {
this.code = code;
this.msg = message;
}
private int code;
private String msg;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public static void main(String[] args) {
EnumSingleton es = EnumSingleton.INSTANCE;
EnumSingleton es2 = EnumSingleton.INSTANCE;
System.out.println(es == es2);
}
}
既实现了线程安全,又避免了同步带来的性能影响。比前两种更加安全,双重校验与静态内部类的单例可以通过反射破环,而枚举不能。
总结
单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。