一,概念
1.1)什么是单例模式:
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责自己创建自己的对象,同时确保永远只有这一个对象被创建。
这个类提供了一种外界访问其唯一对象的方式,外界可以直接访问自己创建好的自身对象 ,不能实例化该类的对象。
1.2)单例模式的意图:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
二,单例模式的特性:
1、单例就是该类只能返回⼀个实例,即,单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给外界提供访问这一实例的唯一方法。
三,单例代码的特点:
单例代码所具备的特点:
1、私有化的构造函数
2、私有的静态全局变量,最终指向自己创建的自身对象
3、供外界访问自己创建的自身对象的、公有的静态方法
四,关键代码:
判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
构造函数是私有的。
五,代码实现——5种方式
首先,我们将创建一个 SingleObject类。
SingleObject 类有它的私有构造函数、以及自身的一个静态全局实例。
SingleObject 类提供了一个静态方法,供外界获取它的这个静态全局实例。
5.1)懒汉式,线程不安全
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
特点:这种方式 lazy loading懒加载 很明显。
缺陷:在多线程环境下不能正常工作!懒汉式存在致命的问题:当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。
public class Singleton {
//私有的静态全局变量
private static Singleton instance;
//私有的构造方法
private Singleton (){}
//供外界访问获取类对象的函数:
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
自己编写:
public class SingleObject01 {
//私有的构造函数:
private SingleObject01() {
}
//提供外界访问的全局变量:
private static SingleObject01 singleObject01;
//供外界访问的方法:
public static SingleObject01 getInstance() {
if (singleObject01 == null) {
singleObject01= new SingleObject01();
}
return singleObject01;
}
/**
* 分析:
* 这种懒汉式的严格来讲,在多线程访问的情况下,都不是单例模式了,
* 因为如果有多个线程同时访问,那么得到的将是多个SingleObject01对象。
*/
}
5.1)懒汉式—加锁优化,线程安全
描述:这种方式具备很好的 lazy loading懒加载效果,能够在多线程中很好的工作,但是效率很低,99% 情况下不需要同步。
缺点:虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法,会严重影响效率。
public class Singleton {
//私有的静态全局变量
private static Singleton instance;
//私有的构造函数
private Singleton (){}
//加锁之后的供外界访问获取类对象的方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
自己编写:
public class SingleObject02 {
//私有构造函数:
private SingleObject02() { }
//私有的静态全局变量:
private static SingleObject02 singleObject02;
//供外界访问上边全局变量的方法:
public static synchronized SingleObject02 getInstance() {
if (singleObject02 == null) {
singleObject02 = new SingleObject02();
}
return singleObject02;
}
}
注意:
但是同步操作的代码其实只在第一次调用时才被需要用到,因为只有在第一次的时候才创建了单例实例对象时。
对于第二次、第三次等以后的调用,直接返回第一次的时候实例化好的对象即可。这就引出了双重检验锁。
5.2)双重检验锁
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双重检验锁机制,安全且在多线程情况下能保持高性能。
public class Singleton {
//私有的静态全局变量
private static Singleton singleton;
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的方法:
public static Singleton getSingleton() {
//双重检验锁:
if (singleton == null) {
//锁住的范围是当前这个单例类:
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
//如果是第二次、第三次之后的访问,单例对象singleton已经在第一次被创建好了,所以直接返回该实例对象:
return singleton;
}
}
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()
这句,这并非是一个原子操作,
事实上在 JVM 中这句话大概做了下面 3 件事情:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。
也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。
如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),
所以线程二会直接返回 instance,然后使用,就会报错。
解决办法就是:我们只需要将 instance 变量声明成 volatile 就可以了。
使用 volatile 的主要原因是其一个特性:禁止指令重排序优化。
public class Singleton {
//使用volatile关键字修饰的私有的静态全局变量
private volatile static Singleton singleton;
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的静态方法
public static Singleton getSingleton() {
//双重检验锁:
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
自己编写:
public class SingleObject04 {
//私有的构造函数:
private SingleObject04() {}
//私有的静态全局变量:
private static volatile SingleObject04 singleObject04;
//供外界获取该类对象的方法:
public static SingleObject04 getInstance() {
if (singleObject04 == null) {
synchronized (SingleObject04.class) {
if (singleObject04 == null) {
singleObject04 = new SingleObject04();
}else{
return singleObject04;
}
}
}
return singleObject04;
}
}
5.3)饿汉式
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader类加载机制 避免了多线程的同步问题,不过,类对象在类加载时就实例化,虽然导致类加载的原因有很多种,
在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类加载,
如果有其他的方式导致了类加载、从而初始化了 类对象instance ,显然没有达到 lazy loading懒加载的效果。
public class Singleton {
//私有的静态全局变量,指向类加载时就实例化好的类对象;
private static Singleton instance = new Singleton();
//私有的构造函数
private Singleton (){}
//供外界访问获取类对象的静态方法:
public static Singleton getInstance() {
return instance;
}
}
自己编写:
public class SingleObject03 {
//私有构造函数
private SingleObject03() {}
//私有的静态全局变量,饿汉式--直接提前实例化好
private static SingleObject03 singleObject03 = new SingleObject03();
//供外界访问的方法:
public static SingleObject03 getInstance() {
return singleObject03;
}
}
·
5.4)静态内部类
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。这种方法也是《Effective Java》上所推荐的。
静态内部类写法的优势:
- 这种写法由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,又因为外部类加载时、并不需要立即加载内部类,因此该写法是懒汉式的、懒加载的;
- 同时访问实例对象的时候,不需要进行同步措施,还能保证线程安全问题,没有性能缺陷;
PS:什么是懒加载:一开始不会创建实例对象、只有当访问该实例对象的时候才会去创建;
示例代码:
public class Singleton {
//私有的构造函数:
private Singleton (){}
//静态内部类:
private static class SingletonHolder {
//供外界访问的静态常量-实例对象:
private static final Singleton INSTANCE = new Singleton();
}
//供外界访问获取类对象的方法:
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
自己编写:
public class SingleObject05 {
//私有的构造函数:
private SingleObject05() {
}
//将静态全局变量放到静态内部类里边:
private static class SingleObject05Inner{
private static final SingleObject05 SINGLE_OBJECT_05=new SingleObject05();
}
//供外界访问的方法:
public static SingleObject05 getInstance() {
return SingleObject05Inner.SINGLE_OBJECT_05;
}
}
·
为什么静态内部类能够实现单例模式,且保证了线程安全?
我们刚才讲到,静态内部类的优点:
静态内部类写法的优势:
- 这种写法由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,又因为外部类加载时、并不需要立即加载内部类,因此该写法是懒汉式的、懒加载的;
- 同时访问实例对象的时候,不需要进行同步措施,还能保证线程安全问题,没有性能缺陷;
PS:什么是懒加载:一开始不会创建实例对象、只有当访问该实例对象的时候才会去创建;
关于懒加载的优势:
外部类加载时并不需要立即加载内部类,内部类不被加载则不会去创建实例对象,故而可以节省内存。
具体来说当单例类第一次被加载时,并不需要去加载静态内部类,
只有当getInstance()方法第一次被调用时,才会导致JVM去加载该单例类的静态内部类。
这种方式,不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例对象的实例化。
为什么可以保证线程安全?
详情参见:
·
5.5)枚举
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
public enum Singleton {
INSTANCE;
public void whateverMethod() {}
}
·
六,建议
一般情况下,不建议使用第 1 种非线程安全的懒汉式单例和第 2 种线程安全的懒汉方式,建议使用第 3 种饿汉方式。
只有在明确必须要实现 lazy loading 懒加载效果时,才使用第四种双重检验锁方式第 5 种静态内部类方式。
一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。
PS:什么是 lazy loading 懒加载?
懒加载其实就是延时加载,即当对象需要用到的时候再去加载。
什么叫做需要用到的时候?比如说一个对象被创建出来就需要一笔内存开支,如果接下来就没有其他的操作,那可以认为这个对象创建得“过早”了。
好处:内存占用小。