单例模式:确保一个类只有一个实例,并提供一个全局访问点;
知识点的梳理:
- 在Java中实现单例模式需要私有的构造器,一个静态方法和一个静态变量;
- 确定在性能和资源上的限制之后,在选择合适的方案来实现单例,以解决多线程的问题;
-
如果使用多个类加载器,可能导致单例实现而产生多个实例;
-
为什么需要独一无二的对象?
-
有一些对象其实我们只需要一个,比如:线程池,缓存,对话框,处理偏好设置和注册表的对象,日志对象,充当打印机,显卡等设备的驱动程序的对象;
- 这些对象只能有一个实例,如果制造出多个实例,就会导致许多问题产生,如:程序的行为异常,资源使用过量,或者是不一致的结果;
-
为什么不利用全局变量(静态变量)来做呢?
- 如果将对象赋值给一个全局变量,那么你必须在程序一开始就创建好对象(当然有些JVM的实现是:在用到的时候才创建对象)。如果这个对象非常耗费资源,而程序在这次的执行过程中又一直没用到它,就形成了浪费;
-
-
单例模式的实现
- 先来看看代码
public class Singleton { //利用一个静态变量来记录Singleton类的唯一实例 private static Singleton uniqueInstance; //把构造器声明为私有的,只有自Singleton类内才可以调用构造器 private Singleton(){} //用getInstance()方法实例化对象,并返回这个实例 public static Singleton getInstance(){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); } return uniqueInstance; } //Singleton当然是一个正常的类,它可以具有一些其他涌入的实例变量和方法 } |
-
看深入看下getInstance()方法的实现
-
巧克力工厂
- 现在有一台巧克力锅炉控制器,该控制器需要防止锅炉已经被填满还继续放原料,或者锅炉内还没放原料就开始空烧等情况;
public class ChocolateBoiler { private boolean empty; private boolean boiled; public ChocolateBoiler(){ //代码开始的时候,锅炉是空的 empty = true; boiled = false; } public void fill(){ //在锅炉内填入原料时,锅炉必须是空的。一旦填入原料,就把empty和Boiled标志设置好 if(isEmpty()){ empty = false; boiled = false; //在锅炉内填满巧克力和牛奶的混合物 } } public void drain(){ //锅炉排除时,必须是满的(不可以是空的)而且是煮过的。排除完毕后,把empty标志设回true if(!isEmpty() && isBoiled()){ //排出煮沸的巧克力和牛奶 empty = true; } } public void boil(){ //煮混合物时,锅炉必须是满的,并且是没有煮过的。一旦煮沸后,就把boiled标志设为true if(!isEmpty() && !isBoiled()){ //将炉内物煮沸 boiled = true; } }
public boolean isBoiled() { return empty; }
public boolean isEmpty() { return boiled; } } |
- 改进这个类,让它变为单例模式
public class ChocolateBoiler { private boolean empty; private boolean boiled; public static ChocolateBoiler uniqueInstance; private ChocolateBoiler(){ //代码开始的时候,锅炉是空的 empty = true; boiled = false; } public static ChocolateBoiler getInstance(){ if(uniqueInstance == null){ uniqueInstance = new ChocolateBoiler(); } return uniqueInstance; } public void fill(){ //在锅炉内填入原料时,锅炉必须是空的。一旦填入原料,就把empty和Boiled标志设置好 if(isEmpty()){ empty = false; boiled = false; //在锅炉内填满巧克力和牛奶的混合物 } } public void drain(){ //锅炉排除时,必须是满的(不可以是空的)而且是煮过的。排除完毕后,把empty标志设回true if(!isEmpty() && isBoiled()){ //排出煮沸的巧克力和牛奶 empty = true; } } public void boil(){ //煮混合物时,锅炉必须是满的,并且是没有煮过的。一旦煮沸后,就把boiled标志设为true if(!isEmpty() && !isBoiled()){ //将炉内物煮沸 boiled = true; } } public boolean isBoiled() { return empty; } public boolean isEmpty() { return boiled; } } |
-
定义单例模式
- 我们正在把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。只能通过单例类来获取单例实例;
- 我们提供了这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例。前面的例子利用延迟实例化的方式创建单例。这种做法对资源敏感的对象特别重要;
-
来看看类图:
-
多线程的问题
- 上面示例中的ChocolateBoiler的fill()方法在多线程环境下会出现问题;
- 利用synchronized改善多线程同步问题
public class Singleton { private static Singleton uniqueInstance; //其它有用的实例化的变量 private Singleton(){} //通过增加synchronized关键字到getInstance()方法中,我们迫使每个线程在进入这个方法之前,要先等候别的线程离开该方法。 //也就是说,不会有两个线程可以同时进入这个方法 public static synchronized Singleton getInstance(){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); } return uniqueInstance; } } |
这样可以解决问题,但是同步会降低性能。而且,只有第一次执行此方法时,才真正需要同步。换句话说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。那应该怎么办呢? |
-
如果getInstance()的性能对应用程序不是很关键,就什么都别做;
- 如果应用程序可以接受getInstance()造成的额外负担,就忘了这件事吧;
-
使用"急切"创建实例,而不用延迟实例化的做法;
- 如果应用程序总是创建并使用单例,或者在创建和运行时方面的负担不太繁重,可以使用"饿汉"式创建单例:
public class Singleton { //在静态初始化器中创建单例。这段代码保证了线程安全 private static Singleton uniqueInstance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ //已经有实例了,直接使用它 return uniqueInstance; } } |
这种做法,可以依赖JVM在加载这个类时,马上创建此唯一的单例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例 |
-
用"双重检查加锁",在getInstance()中减少使用同步;
- 利用"双重检查加锁",首先检查是否实例已经创建了,如果尚未创建,"才"进行同步。这样一来,只有第一次会同步,这就对了;
public class Singleton { //volatile关键词确保,当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地处理uniqueInstance //volatile只能在Java1.5以上使用 private volatile static Singleton uniqueInstance; private Singleton(){} //检查实例,如果不存在,就进入同步区块 public static Singleton getInstance(){ //注意,只有第一次才彻底执行这里的代码 if(uniqueInstance == null){ synchronized (Singleton.class){ if(uniqueInstance == null){ //进入区块后,再检查一次。如果仍是null,才创建实例 uniqueInstance = new Singleton(); } } } return uniqueInstance; } } |
这个做法可以保证性能 |
-
将问题带回巧克力工厂
-
上面提了很多的方案,在此实例中,该选择哪个呢?
-
同步getInstance()方法:
- 这是保证可行的最直接做法。如果不对性能有要求可以选择;
-
饿汉式:
- 如果一定只需要一个实例对象,那么就使用这种方式;
-
双重检查加锁:
- 需要保证性能就使用这种方式;
-
-
-
单例模式问题总结
-
是否可以创建一个类,把所有的方法和变量都定义为静态的,把类直接当做一个单例?
- 如果你的类自给自足,而且不依赖于复杂的初始化,那么可以这么做;
- 但,因为静态初始化的控制权是在Java手上,这么做有可能导致混乱,特别是当有许多类牵涉其中的时候。这么做常常会发生一些BUG;除非你有绝对的必要使用类的单例,否则还是建议使用对象的单例,比较保险;
-
那么类加载器(class loader)呢?两个类加载器可能有机会各自创建自己的单例。
- 每个类加载器都定义了一个命名空间,如果有两个以上的类加载器,不同的类加载器可能会加载同一个类,从整个程序来看,同一个类会被加载多次。如果这样的事情发生在单例上,就会产生多个单例并存的怪异现象。所以,如果你的程序有多个类加载器又同时使用了单例模式,这个时候有一个解决办法:自行指定类加载器,并指定同一个类加载器;
-
类应该只做一件事。如果类能做两件事,就会被认为是不好的OO设计。单例有没有违反这个样的观念呢?
- 单例类不只负责管理自己的实例(并提供全局访问),还在应用程序中担当角色,所以也可以被视为是两个责任。
-
单例类是否可当成超类,设计出子类?究竟可以不可以继承单例类?
- 继承单例类会遇到的一个问题,就是构造器是私有的。你不能用私有的构造器来扩展类;
- 所以要把单例的构造器改成公开的或受保护的。但这样就不算是"真正的"单例了。因为别的类也可以实例化它;
- 如果真把构造器的访问权限改了,还有另一个问题。单例的实现是利用静态变量,直接继承会导致所有的派生类共享同一个实例变量。所以,想要让子类能工作顺利,基类必须实现注册表(Registry)功能;
-
全局变量比单件模式差在哪里?
- 在Java中,全局变量基本上就是对对象的静态引用。在这样的情况下使用全局变量会有一些缺点,上面已经提了其中的一个:急切实例化VS.延迟实例化。
- 我们要记住这个模式的目的:确保类只有一个实例并提供全局访问。全局变量可以提供全局访问,但是不能确保只有一个实例。
- 全局变量也会变相鼓励开发人员,用许多全局变量指向许多小对象来造成命名空间的污染。单例不鼓励这样的现象,但单例仍然可能被滥用。
-