文章目录
一、类型
创建型模式
二、定义
某个类只能存在一个对象实例,可以自行实例化,并存在一个获取这个唯实例的静态方法。
三、UML类图
说明:单例类 Singleton
中有一个私有静态成员变量 instance
,可以通过公共的静态方法 getInstance()
来获取。
四、示例
单例模式有如下八种:
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(线程不安全,同步代码块)
- 双重检测
- 静态内部类
- 枚举
饿汉式是指:单例类在被加载的时候,就实例化一个对象交给引用。
懒汉式是指:只有在 第一次
调用 getInstance() 方法时,才会实例化一个对象。
4.1 饿汉式(静态常量)
步骤
- 创建静态的私有对象
- 创建私有化的构造器,防止外部通过 new 来实例化对象
- 提供一个获取对象的 public 类型的静态方法
单例类
public class Singleton {
// 1.声明私有的静态成员变量,并进行实例化
private static final Singleton instance = new Singleton();
// 2.私有化的构造器,外部无法通过new来创建对象
private Singleton() {
}
// 3.提供一个public的静态方法,用户返回实例对象
public static Singleton getInstance() {
return instance;
}
}
优点 : 写法简单,在类装载阶段就完成了实例化, 没有线程同步的问题
。
缺点 : 由于在类装载时就完成了实例化,所以 无法达到懒加载(Lazy loading)
的效果。如果一直都没有使用这个实例,则会造成 内存浪费
。
结论 : 可以使用,但会造成内存浪费。
4.2 饿汉式(静态代码块)
步骤
- 声明静态的私有成员变量 instance
- 在 static 代码块中实例化成员变量 instance
- 创建私有化的构造器,防止外部通过 new 来实例化对象
- 提供一个获取对象的 public 类型的静态方法
单例类
public class Singleton {
// 1.声明私有的静态成员变量
private static Singleton instance = null;
// 2.通过静态代码块实例化单例对象
static {
instance = new Singleton();
}
// 3.私有化的构造器,外部无法通过new来创建对象
private Singleton() {
}
// 4.提供一个public的静态方法,用户返回实例对象
public static Singleton getInstance() {
return instance;
}
}
饿汉式(静态代码块)和饿汉式(静态常量)是类似的,只不过把单例对象实例化的过程放到了 static 代码块中,在进行类装载的时候执行 static 代码块中的代码,单例对象。
优点 : 写法简单,在类装载阶段就完成了实例化, 没有线程同步的问题
。
缺点 : 由于在类装载时就完成了实例化,所以 无法达到懒加载(Lazy loading)
的效果。如果一直都没有使用这个实例,则会造成 内存浪费
。
结论:可以使用,但会造成内存浪费。
4.3 懒汉式(线程不安全)
步骤
- 声明静态成员变量
- 创建私有的构造器,防止外部通过 new 来实例化对象
- 提供一个 public 的静态方法,用于返回实例对象。当使用到该方法,且 instance 为 null (未被初始化)时,才去初始化 instance 对象
单例类
public class Singleton {
// 1.声明静态成员变量
private static Singleton instance;
// 2.创建私有的构造器,防止外部通过new来实例化对象
private Singleton() {
}
// 3.提供一个public的静态方法,用于返回实例对象
// 当使用到该方法,且instance为null(未被初始化)时,才去初始化instance对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点 : 达到了 懒加载(Lazy loading)
的效果
缺点 : 线程不安全
,只能在单线程下使用,在多线程环境中会产生多个实例。在多线程下,若线程1进入了 if (instance == null) {...}
代码块,但还未开始创建 instance 对象,此时线程2也通过了这个判断进行了代码块中,这时线程1和2都会创建对象,就会 产生多个实例
(若一个个系统中出现了多个id生成器,可能会导致id重复)。
结论 : 不要使用这种方式。
4.4 懒汉式(线程安全,同步方法)
步骤
- 声明静态成员变量 instance
- 创建私有构造器,防止外部通过 new 来实例化
- 提供一个 public 的静态方法,用于返回实例对象。同时使用
synchronized
关键字,使得每次只有一个线程执行这个方法
public class Singleton {
// 1.声明静态成员变量instance
private static Singleton instance;
// 2.创建私有构造器,防止外部通过new来实例化
private Singleton() {
}
// 3.提供一个public的静态方法,用于返回实例对象
// 使用synchronized关键字,使得每次只有一个线程执行这个方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点 : 达到了 懒加载(Lazy loading)
的效果; 线程安全
,不会返回初始化到一半的对象,也不会创建多个实例对象。
缺点 : 效率太低
。这个方法只要执行一次实例化就够了,但如果多个线程同时获取这个单例对象,并执行 getInstance() 方法时都要进行同步,导致效率低下。
结论 : 实际开发中,不推荐使用。
4.5 懒汉式(线程安全,同步代码块)
步骤
- 声明私有的静态成员变量 instance
- 创建私有的构造器,防止外部使用 new 创建对象
- 提供一个 public 的静态方法,用于返回实例对象。使用
synchronized
同步代码块,使得多个线程可以同时进入方法,但是只有一个线程可以创建对象
public class Singleton {
// 1.声明私有的静态成员变量instance
private static Singleton instance;
// 2.创建私有的构造器,防止外部使用new创建对象
private Singleton() {
}
// 3.提供一个public的静态方法,用于返回实例对象
// 使用synchronized同步代码块,使得多个线程可以同时进入方法
// 但是只有一个线程可以创建对象
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
懒汉式(同步代码块)和上面的懒汉式(同步方法)类似,只是把同步方法改为了同步代码块。
优点 : 达到了 懒加载(Lazy loading)
的效果
缺点 : 效率低
。instance 对象只要执行一次实例化就够了,但如果多个线程同时获取这个单例对象,进入 getInstance() 方法后并判断 instance==null
后,只有一个线程可以进入同步代码块并初始化对象,第一个线程在初始化对象后,后面的线程仍然在等待进入同步代码块,它们进入同步代码块后,会重复创建对象,这导致效率低下。
结论 : 实际开发中, 不推荐
使用。
4.6 双重检测
步骤
- 声明私有的静态成员变量 instance,并使用
volatile
关键字进行修饰,防止指令重排序 - 创建私有的构造器,防止外部通过 new 关键字创建对象
- 提供 public 类型的静态方法,用于返回实例对象,使用双重检测,使得线程进入同步代码块后再判断一下对象是否已经创建,防止重复创建对象
public class Singleton {
// 1.声明私有的静态成员变量instance
// 并使用volatile关键字进行修饰,防止指令重排序
private static volatile Singleton instance;
// 2.创建私有的构造器,防止外部通过new关键字创建对象
private Singleton() {
}
// 3.提供public的静态方法,用于返回实例对象
// 使用双重检测,进入同步代码块后再判断一下对象是否为null,防止重复创建对象
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
为什么使用volatile关键字?
一般创建对象的步骤如下所示:
1、申请一块内存空间
2、对其进行初始化
3、分配一个指针指向这块内存
因为指令重排序的问题(编译器优化的问题),没有规定谁先谁后,可能会出现线程 A 初始化对象时,先申请了一块内存,然后分配了一个指针指向内存,最后对其进行初始化,步骤顺序从1、2、3就变为了1、3、2
而线程 A 在执行完1、3、2中的3后,另一个线程 B 调用了 getInstance() 方法,此时 instance 不为空,就将这个对象的引用返回,然而这个实例并没有构造完成,从而造成线程安全问题。
优点:
线程安全
:使用了两次检测,防止多次实例化;使用 volatile 关键字,防止返回未构造完成的对象。效率高
:实例化代码只用执行一次,后面的代码再次访问时,经过if (instance == null)
的判断,就可以直接返回实例化的对象,无需频繁进行同步。
结论 : 推荐使用。
4.7 静态内部类
步骤
- 声明一个私有的静态变量 instance
- 创建私有构造器,防止外部通过 new 创建对象
- 创建私有的静态内部类,内部创建一个静态私有的对象 INSTANCE
- 创建要给 public 的静态方法,用于获取实例
public class Singleton {
// 1.声明一个私有的静态变量 instance
private static Singleton instance;
// 2.创建私有构造器,防止外部通过new创建对象
private Singleton() {
}
// 3.创建私有的静态内部类,内部创建一个静态私有的对象INSTANCE
private static class SingletonClass {
private static Singleton INSTANCE = new Singleton();
}
// 4.创建要给public的静态方法,用于获取实例
public static Singleton getInstance() {
return SingletonClass.INSTANCE;
}
}
优点:
懒加载
: 静态内部类的方式在 Singleton 类被装载时并不会立即实例化,而是在调用 getInstance() 方法时,才会装载 SingletonClass 类,从而完成 Singleton 的实例化。线程安全
: 类的静态属性只有在第一次加载类的时候初始化,所以这里时 JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
结论 : 推荐使用。
4.8 枚举
单例类
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("ok~");
}
}
客户端调用并测试
public class Client {
public static void main(String[] args) {
Singleton instance01 = Singleton.INSTANCE;
Singleton instance02 = Singleton.INSTANCE;
// 查看两对象的地址是否相同,来判断它们是否是同一个实例
System.out.println("hashCode of instance01 : " + instance01.hashCode());
System.out.println("hashCode of instance02 : " + instance02.hashCode());
System.out.println("两个对象的地址是否相同 : " + (instance01 == instance02));
}
}
测试结果
优点:
线程安全
防止反序列化创建新的对象
结论 : 推荐使用
五、注意事项和细节说明
优点
- 保证了系统内存中只存在一个单例类的对象,节省了系统资源;
- 对于需要频繁创建和销毁的对象,使用单例模式可以提高系统性能。
相关场景
- 需要频繁创建和销毁的对象;
- 创建对象时耗时过多或消耗资源过多(重量级对象),但又经常使用的对象;
- 工具类对象;
- 频繁访问数据库或文件的对象(比如:数据源、session工厂等)。
注意事项
- 只能使用单例类提供的方法得到单例对象,
不要使用反射
,否则会实例化得到一个新对象; - 不要断开单例类对象与类中静态引用,由于单例对象未被引用,可能会被JVM当作垃圾进行回收;
- 多线程使用单例进行资源共享时,要注意线程的安全问题。