单例模式简介
单例模式(Singleton Pattern)是较为简单的设计模式,而它又是最常用、最重要的一种设计模式。如:在前端开发中可能用到一个所有页面都会共享的一个状态,此时就可以使用单例模式。单例模式是创建型模式中的一种,它是创建对象的最佳方式,该模式涉及一个单一的类,这个类自己负责创建对象,并保证在程序的执行过程中只有一个实例存在,而外界只需要通过这个类中提供的某个方法来访问该实例。
单例模式的特点
- 只有一个实例。
- 实例必须是自己创建的
- 必须提供一个入口让其他对象访问该实例
单例模式的实现核心
- 私有构造器,即不让外界new对象,将创建对象的权力交由给自己
- 自己创建对象,并提供一个出口让外界可以访问
单例模式的分类
懒汉单例模式(延迟加载)
直接上代码:
/**
* @Description 懒汉式
* @Autor Peng hk
* @Date 2020/9/20
**/
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton(){
// 方便查看该对象被创建了几次
System.out.println(Thread.currentThread().getName() + ":LazySingleton执行了。。");
}
// 其他对象访问单例的入口
public static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
这是最基础的单例模式,也是最简单的单例,下面我们写一个测试类来测试下是否该对象只创建了一次:
public class Test{
public static void main(String[] args) {
LazySingleton lazySingleton = LazySingleton.getInstance();
LazySingleton lazySingleton2 = LazySingleton.getInstance();
LazySingleton lazySingleton3 = LazySingleton.getInstance();
}
}
输出结果:
然而,在单线程的环境下,似乎没有什么问题,而实际情况下,往往是多线程环境,那么会出现什么问题呢?我们可以来测试下多线程访问该实例:
class Test{
public static void main(String[] args) {
// 开启十个线程,并发访问该单例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazySingleton instance = LazySingleton.getInstance();
}).start();
}
}
}
输出结果:
从结果可以看出,构造函数执行了很多次,显然对象也被创建了很多次,那么在多线程的环境下,显然它是以上实现的懒汉单例是线程不安全的。
我们可以用synchronized关键字来修饰getInstance方法,保证器只能有一个线程去访问,可以很容易的解决以上问题,但是这种方式显然效率很低,因为每一个线程都要等待,相当于依次去执行了。因此我们可以通过双重校验锁来解决线程安全问题,同时在性能上也有保证,见下。
双重校验锁(基于懒汉单例的优化)
双重校验锁(DCL)在保证线程安全的前提下,同时保证其性能。
/**
* @Description 双检锁/双重校验锁(DCL,double-checked locking) 实现懒汉式单例的线程安全
* @Autor Peng hk
* @Date 2020/9/20
**/
public class LazyDCLSingleton {
private static LazyDCLSingleton lazyDCLSingleton;
private LazyDCLSingleton(){
System.out.println(Thread.currentThread().getName() + ":LazyDCLSingleton执行了。。");
}
/*
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
*/
public static LazyDCLSingleton getInstance(){
if (lazyDCLSingleton == null){
synchronized (LazyDCLSingleton.class){
if (lazyDCLSingleton == null){
lazyDCLSingleton = new LazyDCLSingleton();
}
}
}
return lazyDCLSingleton;
}
}
测试类:
class Test{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyDCLSingleton lazyDCLSingleton = LazyDCLSingleton.getInstance();
}).start();
}
}
}
输出结果:
你看到结果可能以为万事大吉了,其实并不是,我们来分析下首先来分析下JVM创建对象的步骤(以代码中的lazyDCLSingleton = new LazyDCLSingleton();
为例):
从字节码的角度分析,正常的创建对象大概有如下几个步骤:
1. 在堆中开辟空间
2. 初始化实例
3. 将对象地址赋值给变量lazyDCLSingleton
正常情况下创建对象是这样的步骤,但是JIT(编译器)或者是CPU在创建对象的时候可能会让指令码
重排序,即在创建对象的时候可能不是按照123的顺序执行,其中3和2有可能会颠倒执行,如132的顺序执行,在多线程的情况下,先执行3,而后来的线程访问时,拿到对象,对象却没有初始化,有可能会出现问题,因此要保证对象创建的原子性Java中的关键字volatile
可以防止指令重排序,保证对象的创建是按正常的指令码顺序执行。正确的DCL懒汉模式应该在此处加上volatile关键字,保证创建对象的原子性:
private volatile static LazyDCLSingleton lazyDCLSingleton;
静态内部类单例模式
这种利用ClassLoader的特性,保证类只会被加载依次,因此它是线程安全的,代码如下:
package com.phk.singleton;
/**
* @Description 静态内部类单例模式
* @Autor Peng hk
* @Date 2020/9/20
*
* 是否 Lazy 初始化:是
*
* 是否多线程安全:是
*
* 实现难度:一般
*
* 描述:这种方式能达到双检锁方式一样的功效,但实现更简单。
* 对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
**/
public class InnerClassSingleton {
private static InnerClassSingleton innerClassSingleton;
private InnerClassSingleton() {
System.out.println(Thread.currentThread().getName() + ":InnerClassSingleton执行了。。");
}
public static InnerClassSingleton getInstance() {
return InnerClass.innerClassSingleton;
}
public static class InnerClass {
private static final InnerClassSingleton innerClassSingleton = new InnerClassSingleton();
}
}
class Test4 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
InnerClassSingleton.getInstance();
}).start();
}
}
}
测试结果:
饿汉单例模式
/**
* @Description 饿汉式单例模式
* @Autor Peng hk
* @Date 2020/9/20
**/
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
// 构造方法私有化,让外界无法去直接创建对象
private HungrySingleton(){
System.out.println(Thread.currentThread().getName() + ":HungrySingleton构造方法。。");
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
饿汉单例模式是线程安全的,但是这种方式是在类加载的时候初始化对象,可能导致内存的浪费,不过它没有加锁执行效率很高。
枚举(不常用)
/**
* JDK 版本:JDK1.5 起
* 是否 Lazy 初始化:否
* 是否多线程安全:是
* 实现难度:易
*
*
*/
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
class Test5{
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.INSTANCE;
EnumSingleton instance2 = EnumSingleton.INSTANCE;
System.out.println(instance == instance2);
}
}
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化(以上的很多方式都能够被反射机制破坏,这种方式不会被反射破坏)。
- 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,
- 防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
- 不能通过 reflection attack 来调用私有构造方法。
补充:破解单例模式
1.反编译懒汉单例
@Test
public void test1(){
Class<LazySingleton> lazySingletonClass = LazySingleton.class;
try {
// 获取无参构造
Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor();
// 强制打破private
declaredConstructor.setAccessible(true);
// 对象
LazySingleton lazySingleton = declaredConstructor.newInstance();
LazySingleton lazySingleton1 = declaredConstructor.newInstance();
LazySingleton lazySingleton2 = declaredConstructor.newInstance();
System.out.println(lazySingleton);
System.out.println(lazySingleton1);
System.out.println(lazySingleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
2.反编译饿汉单例
@Test
public void test3(){
Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
try {
Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
HungrySingleton hungrySingleton = declaredConstructor.newInstance();
HungrySingleton hungrySingleton1 = declaredConstructor.newInstance();
HungrySingleton hungrySingleton2 = declaredConstructor.newInstance();
System.out.println(hungrySingleton);
System.out.println(hungrySingleton1);
System.out.println(hungrySingleton2);
} catch (Exception e) {
e.printStackTrace();
}
}