单例模式
单例模式是一种简单的模式,一般作为学习设计模式的起点。
定义: Ensure a class has only one instance, and provide a global point of access to it. (确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例)
单例模式的通用类图:
下面就说一下单例模式的实现方式
实现方式
主要有两种实现方式:饿汉式和懒汉式
饿汉式
当类加载到内存后,就实例化一个单例,JVM保证线程安全。
步骤:
- new出来一个使用
static
和final
修饰的实例化对象INSTANCE
。 - 将构造方法设为
private
类型,不让别人再去实例化Singleton01类。 - 定义一个静态方法
getInstance
,返回INSTANCE
。外部使用getInstance
方法获取实例化对象。
public class Singleton01 {
private static Singleton01 INSTANCE = new Singleton01();
private Singleton01(){}
public static Singleton01 getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
System.out.println(s1 == s2); // true
}
}
优点: 简单实用,推荐使用
唯一缺点: 不管是否用得到,类装载时都会完成实例化。如果实例化对象没有被用到就会造成时间和空间资源的浪费。
懒汉式
懒汉式(线程不安全)
当外部调用getInstance
方法时先判断实例化对象INSTANCE
是否为null。
如果为null则new出来一个新的对象并赋值给INSTANCE
并返回;否则直接返回INSTANCE
。
public class Singleton02 {
private static Singleton02 INSTANCE;
private Singleton02(){}
public static Singleton02 getInstance(){
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton02();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(Singleton02.getInstance().hashCode())
).start();
}
}
}
输出:
37565919
518461220
518461220
518461220
518461220
518461220
518461220
518461220
37565919
518461220
518461220
433828562
1199730481
287080281
2142643463
619240014
569889152
225543282
...
优点: 懒加载,解决了饿汉式浪费时间和空间资源的问题。
缺点: 线程不安全。多线程访问时,若线程A先判断INSTANCE == null
为TRUE,此时还没实例化对象。线程B再次判断INSTANCE == null
也为TRUE,再次实例化对象,此时实例化了两次。由输出结果也可以知道此方法创建了多个实例。
Synchronized
全局锁
将整个getInstance
方法加锁
public class Singleton03 {
private static Singleton03 INSTANCE;
private Singleton03(){}
public static synchronized Singleton03 getInstance(){
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton03();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(Singleton03.getInstance().hashCode())
).start();
}
}
}
优点: 解决了线程不安全的问题
缺点: 对整个方法加锁,效率低下
局部锁(不可行)
妄图通过减少同步代码块的方式提高效率,但是依旧线程不安全,不可行。
原因和**懒汉式(线程不安全)**的一样。
public static Singleton04 getInstance(){
// 妄图通过减小同步代码块的方式提高效率,不可行
if (INSTANCE == null) {
synchronized (Singleton04.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton04();
}
}
return INSTANCE;
}
DCL单例
DCL全称为Double Check Lock(双重锁定检查)。
在给Singleton加锁后再判断实例是否为空。
public class Singleton05 {
private static volatile Singleton05 INSTANCE;
private Singleton05(){}
public static Singleton05 getInstance(){
// 业务逻辑代码省略
if (INSTANCE == null) {
// 双重检查
synchronized (Singleton05.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton05();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(Singleton05.getInstance().hashCode())
).start();
}
}
}
此时INSTANCE
必须使用volatile
修饰、
volatile
关键字有两个作用:
- 保持线程可见性
- 禁止指令重排序
创建对象的过程有三个步骤:
- 半初始化,申请内存,值为默认值
- 设初始值
- 将INSTANCE对象指向分配的内存空间
如果有一线程A执行到INSTANCE = new Singleton05()
语句,发生了指令重排,步骤二和步骤三颠倒了
- 半初始化,申请内存,值为默认值
- 将INSTANCE对象指向分配的内存空间(此时INSTANCE非null)
- 设初始值
先将INSTANCE
对象和内存空间建立联系,此时为默认值。线程B又继续进行,发现INSTANCE
非空,于是直接返回INSTANCE
,造成各种不可预知的后果。因此在DCL单例模式中,必须要在实例上volatile
关键字。
静态内部类实现(重点)
静态内部类实现是面试官最想看到的答案,因为其中涉及到JVM装载的问题。
public class Singleton06 {
// 私有化构造器
private Singleton06() {}
private static class SingletonInstance {
private static final Singleton06 singleton06 = new Singleton06();
}
// 提供静态公有获取方法
public static synchronized Singleton06 getInstance() {
return SingletonInstance.singleton06;
}
}
程序启动时静态内部类并不会被装载,而且装载内部类是线程安全的,所以这个单例模式真正意义上实现了懒加载与线程安全且节省了内存,必须要掌握。
枚举
因为枚举天生单例,故可以使用枚举类实现单例模式,是一种比较特殊冷门的单例模式的实现方式,了解即可。
enum Singleton07 {
INSTANCE;
public void updateInstance() {}
}