1.单例设计模式
设计模式:是解决一类问题最行之有效的方法。是一种思想,是规律的总结。Java中有23种设计模式。单例模式是设计模式中最简单的形式之一。
单例设计模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的目的是解决一个类在内存中只存在一个对象,使其对象成为系统中的唯一实例。
使用场景:
工作中经常需要在应用程序中保持一个唯一的实例,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。eg:IO处理,数据库操作等。
单例设计模式提供主要解决方式:饿汉式、懒汉式、静态内部类的单例方式、枚举实现单例等
Note:一般开发时,使用饿汉式,因为安全,效率高。懒汉式会出现线程安全等问题(当多对象同时加载时……)
保证唯一性的思想及步骤:
为了避免其他程序建立该类对象,先禁止其他程序建立该类对象,即将构造函数私有化,即建立一个私有的构造方法。
为了其他程序访问到该类对象,须在本类中创建一个该类私有对象,即创建一个私有并静态的本类对象。
为了方便其他程序访问到该类对象,可对外提供一个公共访问方式,即建立一个公有并静态的本类方法。
单例模式的优点:
单例模式(Singleton)会控制其实例对象的数量,从而确保访问对象的唯一性。
1)实例控制:单例模式防止其它对象对自己的实例化,确保所有的对象都访问一个实例。
2)伸缩性:因为由类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
单例模式的缺点:
1)系统开销。虽然这个系统开销看起来很小,但是每次引用这个类实例的时候都要进行实例是否存在的检查。这个问题可以通过静态实例来解决。
2)开发混淆。当使用一个单例模式的对象的时候(特别是定义在类库中的),开发人员必须要记住不能使用new关键字来实例化对象。因为开发者看不到在类库中的源代码,所以当他们发现不能实例化一个类的时候会很惊讶。
3)对象生命周期。单例模式没有提出对象的销毁。在提供内存管理的开发语言(比如,基于.NetFramework的语言)中,只有单例模式对象自己才能将对象实例销毁,因为只有它拥有对实例的引用。在各种开发语言中,比如C++,其它类可以销毁对象实例,但是这么做将导致单例类内部的指针指向不明。
使用单例模式的注意事项:
1)使用Singleton模式有一个必要条件:在一个系统要求一个类只有一个实例时才应当使用单例模式。反之,如果一个类可以有几个实例共存,就不要使用单例模式。
2)不要使用单例模式存取全局变量。这违背了单例模式的用意,最好放到对应类的静态成员中。
3)不要将数据库连接做成单例,因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能及时释放连接。Singleton模式由于使用静态成员存储类实例,所以可能会造成资源无法及时释放,带来问题。
单例模式的核心原理:
将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。
Note:具体实现方式取决于项目本身,结合项目实际情况决定使用哪种方式。
根据项目实际业务情况选择使用哪种单例模式:
饿汉
标准饿汉 (安全防护方面 枚举单例更优于标准饿汉)
线程安全,高效,不可以懒加载
枚举单例
线程安全,高效,不可以懒加载(天然避免反射与反序列化)
懒汉 (效率方面 静态内部类更优于标准懒汉)
标准懒汉
线程安全,低效,可以懒加载
双重检测(不推荐,有bug)
线程安全,低效,可以懒加载
静态内部类
线程安全,低效,可以懒加载
2.饿汉式和懒汉式
2.1 饿汉式:先初始化对象(当类加载的时候,就创建对象)
/* 饿汉式:当类加载的时候就创建对象 */ class Single { //描述事物 private int num; public void setNum(int num) { this.num=num; } public int getNum() { return num; } /* 对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步饿汉式代码加上即可 */ private Single(){} //将构造函数私有化。 private static Single s = new Single(); /* 在类中创建一个本类对象。*/ public static Single getInstance() /*提供一个公有静态方法可以使其他类获取到该对象。*/ { return s; } } class SingleDemo { public static void main(String[] args) { Single s1 = Single.getInstance(); Single s2 = Single.getInstance(); s1.setNum(23); System.out.println(s2.getNum()); } }
输出结果:
2.2 懒汉式:对象是 方法被调用时,才初始化,也叫做对象的延时加载。
//懒汉式: //Single类进内存,对象还没有存在,只有调用了getInstance方法时,才建立对象。 class Single { //描述事物 private int num; public void setNum(int num) { this.num=num; } public int getNum() { return num; } /* 对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步懒汉式代码加上即可 */ private static Single s = null; private Single(){} public static Single getInstance() { if(s==null) s = new Single(); return s; } } //记录原则:定义单例,建议使用饿汉式。 class SingleDemo { public static void main(String[] args) { Single s1 = Single.getInstance(); /* 当加载Single类的时候,对象还不存在,s为null; 当调用getInstance()方法时,对象才建立,s才被赋值。 */ Single s2 = Single.getInstance(); s1.setNum(23); System.out.println(s2.getNum()); } }
输出结果:
Note:
此种实现方式只适用于单线程环境,因为在多线程的环境下有可能得到Single类的多个实例,线程不安全。假如同时有两个线程去判断s==null,并且得到的结果为真,那么两线程都会创建Single实例,就违背了单例模式“唯一实例”的初衷。
3.饿汉式和懒汉式的区别
1)饿汉式是类一加载进内存就创建好了对象;
懒汉式则是类才加载进内存的时候,对象还没有存在,只有调用了getInstance()方法时,对象才开始创建。
2)懒汉式是延迟加载,如果多个线程同时操作懒汉式时就有可能出现线程安全问题,解决线程安全问题。可以加同步来解决。但是加了同步之后,每一次都要比较锁,效率就变慢了,所以可以加双重判断来提高程序效率。
Note:开发常用饿汉式,因为饿汉式简单安全。懒汉式多线程的时候容易发生问题。
4.解决懒汉式的线程安全问题
解决懒汉式的线程安全问题可以使用双重判断(加锁)
Note:
下面的实现方式线程是安全的,首先我们创建了一个静态只读的进程辅助对象,synchronized是确保当一个线程位于代码的临界区时,另一个线程不能进入临界区(同步操作)。如果其他线程试图进入锁定的代码,则它将一直等待,直到该对象被释放。从而确保在多线程下不会创建多个对象实例。这种实现方式要进行同步操作,需要在同步操作之前,添加判断该实例是否为null以降低通过操作的次数,避免影响系统性能的瓶颈和增加了额外的开销。这是经典的DCL(Double-Checked Locking)方法。
代码如下:
class Single { //描述事物 private int num; public void setNum(int num) { this.num=num; } public int getNum() { return num; } /* 对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步懒汉式代码加上即可 */ private static Single s = null; private Single(){} public static Single getInstance() { //此处进行双重判断、加锁即可解决懒汉式容易出现的问题 if(s==null) { synchronized(Single.class) { if(s==null) s = new Single(); } } return s; } } class SingleDemo { public static void main(String[] args) { Single s1 = Single.getInstance(); /* 当加载Single类的时候,对象还不存在,s为null; 当调用getInstance()方法时,对象才建立,s才被赋值。 */ Single s2 = Single.getInstance(); s1.setNum(23); System.out.println(s2.getNum()); } }
输出结果:
但是,很可惜,它也是存在问题的。主要在于 s = new Single(); 这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
1)给 s 分配内存
2)调用Single 的构造函数来初始化成员变量
3)将s 对象指向分配的内存空间(执行完这一步, s 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 s 已经是非 null 了(但却没有初始化),所以线程二会直接返回 s,然后使用,然后顺理成章地报错。
解决办法:只需要将 s 变量声明成 volatile 就可以了。
(注:volatile的用法和作用,日后会作单独写博客加以说明)
代码如下:
class Single { //描述事物 private int num; public void setNum(int num) { this.num=num; } public int getNum() { return num; } /* 对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步懒汉式代码加上即可 */ private static volatile Single s; //声明成volatile private Single(){} public static Single getInstance() { if(s==null) { synchronized(Single.class) { if(s==null) s = new Single(); } } return s; } }
部分人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 s对象的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。即在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
eg:取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从“先行发生原则”的角度理解,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(此处的“后面”是时间上的先后顺序)。
Note:需特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
5.单例模式的另一种实现方式:静态内部类
DCL方法虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,但是,它在某些情况下还会出现失效的问题。这种问题被称为双重检查锁定(DCL)失效。建议使用静态内部类的方式实现单例模式。
以下只提供主要实现代码:
public class Singleton{ /* 对事物描述时,正常该怎么描述就怎么描述;当需要将该事物的对象保证唯一时,就将以下三步饿汉式代码加上即可 */ private Singleton(){} //将构造函数私有化。 /*提供一个公有静态方法可以使其他类获取到该对象。*/ public static Singleton getInstance(){ return SingletonHolder.myInstance; } /* 静态内部类形式:在类中创建一个私有并静态的本类对象。*/ private static class SingletonHolder{ private static final Singleton myInstance=new Singleton(); } }
分析:
当第一次加载Singleton类时并不会初始化myInstance,只有在第一次调用Singleton的getInstance()方法时才会导致myInstance被初始化。因此,第一次调用getInstance()方法会导致虚拟机加载SingletonHolder内部类时,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。推荐使用这种单例模式的实现方式。
6.上述单例的实现存在反序列化问题
在上述的几种单例模式实现中,在一种情况下会出现重新创建对象的情况,那就是反序列化。
通过反序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。
(在上述几个示例中)杜绝单例对象在反序列化时重新生成对象,必须加入以下方法:
private Object readResolve() throws ObjectStreamException { return myInstance; }
7.单例模式的另一种实现方式:枚举实现单例
首先简单了解下枚举,再回到枚举实现单例模式:
1)枚举是一种特殊的类,其中的每一个元素都是该类的一个实例对象。
枚举可以定义构造函数、抽象方法、成员变量和普通方法。
每一个枚举元素,都是一个对象。
如果枚举只有一个成员时,就可以作为一个单例的实现方式。
Note:枚举元素必须位于枚举体中的最开始部分,枚举元素列表后要有分号,与其他成员分隔用逗号。枚举元素名称全部字母必须大写。
枚举类的构造方法必须是私有的。
枚举一般格式:
public enum 类名{ //枚举成员列表eg:RED,GREEN; //私有构造 //其他成员 }
2)使用枚举实现单例:
使用枚举实现单例的最大优点: 如果枚举只有一个成员时,就可以作为一种单例的实现方式。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。以下只提供主要实现代码:
public enum SingletonEnum{ INSTANCE; //下面方法并不是单例的一部分 public void doSomething(){ System.out.print("do sthing"); } }