前言
单例模式可以说是设计模式中最简单,也最常见的设计模式。在很多面试开发面试中也会提到单例。单例模式是创建者模式中的一种。单例保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。本节就单例模式做详细介绍。
特点:
- 私有的构造函数
- 私有的静态的全局变量
- 公有的静态的方法访问全局变量
1. 懒汉
1.1 简介
懒汉,即只有真正使用的时候,才初始化对应的实例对象。这样可以避免在项目启动阶段创建大量消耗资源的对象。
1.2 代码示例
package com.wanlong.design_pattern.create.singleton;
/**
* @author wanlong
* @version 1.0
* @description: 懒汉式单例模式
* 对象延迟加载
* 优点:资源利用率高了。
* 缺点: 每次调用getInstance()方法都要同步,并发效率较低。
* @date 2022/9/6 11:04
*/
public class LazyMan {
//私有的静态全局变量
private static LazyMan lazyMan;
//私有化构造
private LazyMan() {
}
//提供公有访问方法
public static synchronized LazyMan getInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
1.3 测试代码
@Test
public void testLazyMan(){
LazyMan instance1=LazyMan.getInstance();
LazyMan instance2 = LazyMan.getInstance();
System.out.println(instance1==instance2);
}
1.4 优缺点
1.4.1 优点
提高资源利用率
1.4.2 缺点
每次调用获取实例方法都要加同步锁,防止并发线程重复创建对象,并发效率会降低
2. 饿汉
2.1 简介
和懒汉模式相反,饿汉是项目初始化阶段加载。不管后续会不会使用该对象实例。
2.2 代码示例
package com.wanlong.design_pattern.create.singleton;
/**
* @author wanlong
* @version 1.0
* @description: 饿汉式单例模式
* @date 2022/9/6 10:41
*/
public class StarveMan {
private static /*final*/ StarveMan starveMan = new StarveMan();
//私有化构造方法
private StarveMan() {
}
//公共的访问
public static /*synchronized*/ StarveMan getInstance() {
return starveMan;
}
}
2.3 测试代码
@Test
public void testStarve(){
StarveMan instance1 = StarveMan.getInstance();
StarveMan instance2 = StarveMan.getInstance();
System.out.println(instance1==instance2);
}
2.4 优缺点
2.4.1 优点
线程安全,调用效率高 。static变量会在类装载时初始化,此时不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。
2.4.2 缺点
不能延迟加载,如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!
3. 懒汉之双重检查锁
3.1 简介
在上面懒汉模式中,由于对 getInstance() 做了同步处理,synchronized 将导致性能开销。如果 getInstance() 被多个线程频繁的调用,将会导致程序执行性能的下降。反之,如果 getInstance() 不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。
为此,我们可以把加锁的步骤往后移动。先判断对象存不存在,不存在的话,再加锁创建对象。 在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。在对象创建好之后,执行 getInstance() 将不需要获取锁,直接返回已创建好的对象。
这里要留意,考虑到java代码在jvm底层可能重排序,为此需要将对象声明为volatile ,禁止对象创建重排序,关于重排序更多细节,具体见文末参考文章。
3.2 代码示例
package com.wanlong.design_pattern.create.singleton;
/**
* @author wanlong
* @version 1.0
* @description: 单例模式之双重检测锁实现
* @date 2022/9/6 11:10
*/
public class DoubleLockCheck {
//注意关键字 volatile 禁止重排序
private static volatile DoubleLockCheck instance = null;
private DoubleLockCheck() {
}
public static DoubleLockCheck getInstance() {
if (instance == null) {
synchronized (DoubleLockCheck.class) {
if (instance == null) {
instance = new DoubleLockCheck();
}
}
}
return instance;
}
}
3.3 测试代码
@Test
public void testDoubleLock(){
DoubleLockCheck instance1 = DoubleLockCheck.getInstance();
DoubleLockCheck instance2 = DoubleLockCheck.getInstance();
System.out.println(instance1==instance2);
}
3.4 优缺点
3.4.1 优点
这个模式将同步内容下方到if内部,提高了执行的效率,不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。
3.4.2 缺点
由于编译器优化原因和JVM底层内部模型原因,需要对变量加关键字,禁止重排序。如果遗漏可能会有问题.
4. 懒汉之静态内部类
4.1 简介
通过jvm初始化类信息的特性,可以通过静态内部类的方式,实现懒汉单例模式。
4.2 代码示例
package com.wanlong.design_pattern.create.singleton;
/**
* @author wanlong
* @version 1.0
* @description: 静态内部类实现单例
* @date 2022/9/6 11:18
*/
public class StaticInnerClass {
private StaticInnerClass() {
}
public static StaticInnerClass getInstance() {
return SinelgtonClassInstance.instance;
}
private static class SinelgtonClassInstance {
private static final StaticInnerClass instance = new StaticInnerClass();
}
}
4.3 测试代码
@Test
public void testStaticInnerClass(){
StaticInnerClass instance1 = StaticInnerClass.getInstance();
StaticInnerClass instance2 = StaticInnerClass.getInstance();
System.out.println(instance1==instance2);
}
4.4 优缺点
4.4.1 优点
- 外部类没有static属性,则不会像饿汉式那样立即加载对象。
- 只有真正调用getInstance(),才会加载静态内部类。加载类时是线程 安全的。 instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.
- 兼备了并发高效调用和延迟加载的优势!
5. 枚举类实现单例
5.1 简介
通过枚举类的特性,实现单例模式
5.2 代码示例
package com.wanlong.design_pattern.create.singleton;
/**
* @author wanlong
* @version 1.0
* @description: 枚举实现单例模式
* @date 2022/9/6 11:26
*/
public enum EnumSingleton {
//实例
INSTANCE;
public void singleletonOperation() {
//单例操作处理
}
}
5.3 测试代码
@Test
public void testEnumSingle(){
EnumSingleton instance1 = EnumSingleton.INSTANCE;
EnumSingleton instance2 = EnumSingleton.INSTANCE;
System.out.println(instance1==instance2);
//单例对象对应的操作
instance1.singleletonOperation();
}
5.4 优缺点
5.4.1 优点
实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
5.4.2 缺点
无延迟加载
6. java单例破解与防破解
6.1 破解
6.1.1 序列化与反序列化破解
@Test
public void testPojie() throws Exception {
LazyMan instance = LazyMan.getInstance();
FileOutputStream fileOutputStream = new FileOutputStream("singletonTest.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
FileInputStream fileInputStream = new FileInputStream("singletonTest.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
//通过反序列化创建对象
LazyMan lazyMan = (LazyMan) objectInputStream.readObject();
System.out.println(lazyMan == instance);
//false
}
6.1.2 反射破解
@Test
public void testPojie2() throws Exception {
LazyMan instance = LazyMan.getInstance();
Class<LazyMan> lazyManClass = LazyMan.class;
Constructor<LazyMan> declaredConstructor = lazyManClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
//通过反射创建对象
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance==instance2);
//false
}
6.2 防止破解
package com.wanlong.design_pattern.create.singleton;
import java.io.ObjectStreamException;
import java.io.Serializable;
/**
* @author wanlong
* @version 1.0
* @description: 单例(除枚举外)可以被反序列化和反射破解,可按照如下方式防止破解
* @date 2022/9/6 11:38
*/
public class SingletonAvoidRepeatCreate implements Serializable {
private static SingletonAvoidRepeatCreate instance;
//反射避免破解
private SingletonAvoidRepeatCreate() throws Exception {
if (instance != null) {
throw new Exception("只能创建一个对象");
//通过手动抛出异常,避免通过反射创建多个单例对象
}
}
public static synchronized SingletonAvoidRepeatCreate getInstance() throws Exception {
if (instance == null) {
instance = new SingletonAvoidRepeatCreate();
}
return instance;
}
//反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象。
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
参考文章:
java为什么要双重检查锁