一个类仅有一个实例,由自己创建并对外提供一个实例获取的入口,外部类可以通过这个入口直接获取该实例对象。
生活中常见的单例模式:
- Windows/mac桌面中的回收站,无法同时打开多个回收站窗口
- 打印机,一个打印机可以有多个打印任务,但是一台打印机,只能一个一个执行打印任务
单例的实现主要是通过以下两个步骤
将该类的构造方法定义为私有方法
,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;在该类内提供一个静态方法
,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
场景:
很多时候整个应用只能够提供一个全局的对象,为了保证唯一性,这个全局的对象的引用不能再次被更改。
比如在某个应用程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例类统一读取并实例化到全局仅有的唯一对象中,然后应用中的其他对象再通过这个单例对象获取这些配置信息。
- 多线程中的线程池
- 数据库连接池
- 系统环境变量
单例模式是一种思想,保证实例为一,实现单例有很多方法;
例如:Spring 中的三级缓存
- 一级缓存:singletonObjects
- 二级缓存:earlySingletonObjects
- 三级缓存:singletonFactories
一、懒汉式
懒汉式分为:无锁、单一锁、双检锁方式
- 无锁:在并发环境中会产生多个实例对象,所以示例中不写了
- 单一锁:单一锁在每次获取实例对象时,都会先加锁,消耗性能,所以示例中不写了
- 双检索:既能保证多线程环境下,单一实例又能兼顾性能。
懒汉式单例模式,通过判断这个对象是否存在,如果存在就直接返回这个对象
,将new对象延迟到了获得对象的方法中,实现了懒加载,解决了饿汉式可能造成空间浪费的弊端
由于volatile关键字屏蔽了虚拟机中一些必要的代码优化,所以运行效率率低。
具体使用无锁,单一锁,双检索具体业务具体分析
效率:无锁 > 单一锁 > 双检索(volatile)
可以利用反射破解~~~
参考示例
/**
* 单例-懒汉模式
* 线程不安全,需要手动处理(双检锁)
* volatile,修饰成员变量,保证可见性与有效性,防止指令重排
* synchronized:保证原子性
*
* 示例:ReactiveAdapterRegistry、TomcatURLStreamHandlerFactory
*
* new的数据都是放在内存中的
* jvm序列化就可以把内存中的数据通过序列化的机制把内存中的数据以某种格式序列化出来
*
* @author liushiwei
*/
public class LazySingletonDemo {
public static void main(String[] args) throws Throwable {
LazySingleton instance = LazySingleton.getInstance();
try {
// 通过jdk 序列化到磁盘上 执行后会在当前项目下生成一个test文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/liushiwei/logs/test"));
oos.writeObject(instance);
} catch (IOException e) {
e.printStackTrace();
}
try {
// 通过jdk反序列化,反序列化会对单例造成破坏
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/liushiwei/logs/test"));
LazySingleton lazySingleton = (LazySingleton) ois.readObject();
System.out.println(lazySingleton == instance);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
// 懒汉模式获取实例
LazySingleton instanceLazy = LazySingleton.getInstance();
// 通过反射获取实例( 此时 LazySingleton 类被实例化 )
Constructor<LazySingleton> declaredConstructor = LazySingleton.class.getDeclaredConstructor();
// 暴力反射
declaredConstructor.setAccessible(true);
// 创建一个实例
LazySingleton singleton = declaredConstructor.newInstance();
System.out.println(singleton == instanceLazy);
}
}
/**
* 实现Serializable具备序列化的能力,如果类不实现serializable,就无法完成序列化和反序列化
*/
class LazySingleton implements Serializable {
/**
* 序列化版本
*/
private static final long serialVersionUID = -5849649956194247784L;
/**
* volatile:可以保证可见性与有效性,防止指令重排
*/
private static volatile LazySingleton instance;
/**
* 私有的构造方法防止new实例
*/
private LazySingleton(){
}
// 双检索+内存可见性
public static LazySingleton getInstance() {
// 当前 实例为null是才创建
if(null == instance){
// 创建实例时加同步锁,保证线程安全
synchronized (LazySingleton.class){
// 防止多线程时,同时多个线程进入等待,一个线程完成创建后,其他线程再次new
if(null == instance){
// 内存模型
// new时会在堆(heap)开辟一个新空间,返回地址到临时内存区域(栈)
// 1.开辟新空间。
// 2.初始化空间
// 赋值
instance = new LazySingleton();
}
}
}
return instance;
}
/**
* 保证序列化一致性
* 需要指定 serialVersionUID 否则会抛serialVersionUID不一致无法序列化异常
* @return
* @throws ObjectStreamException
*/
Object readResolve() throws ObjectStreamException {
return getInstance();
}
}
二、饿汉式
-
在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。
-
注意:Java反射机制支持访问private属性,所以可通过反射来破解构该造方法,产生多个实例,
所以构造方法中要增加非空判断
,如下所示 -
缺点:
对象的实例是静态的,就说明我们即使不使用这个对象,它也存在
,那么如果这个变量占用内存很大,那么很显然会造成浪费,例如:HungrySingleton类中有一个超大的对象private byte[] data4 = new byte[102410245];
/**
* 饿汉模式
* 线程安全的(实例化的过程是线程安全的 ),因为初始化时时,同步进行的
* 典型示例:Runtime 类
* @author liushiwei
*/
public class HungrySingletonDemo {
public static void main(String[] args) throws Throwable {
// 懒汉模式获取实例
HungrySingleton instance = HungrySingleton.getInstance();
// 通过反射获取实例( 此时 HungrySingleton 类被实例化 )
Constructor<HungrySingleton> declaredConstructor = HungrySingleton.class.getDeclaredConstructor();
// 暴力反射
declaredConstructor.setAccessible(true);
// 创建一个实例
HungrySingleton hungrySingleton = declaredConstructor.newInstance();
System.out.println(hungrySingleton == instance);
}
}
class HungrySingleton{
/**
* 初始化时机
* 1、当前类是启动类,即main函数所在类
* 2、直接进行new操作,访问静态属性,用反射访问类,初始化一个类的子类等操作
*
* 类加载的初始化阶段,完成了实例的初始化,借助jvm加载机制,
* 保证实例的唯一性(初始化过程只会执行一次)及线程
* 安全(jvm以同步的形式来完成类加载的整个过程)
*
* 类加载过程
* 1、加载二进制数据到内存中,生成对应的Class数据结构
* 2、链接:验证,准备(给静态成员变量赋默认值),解析
* 3、初始化:给类的静态变量赋初始值
* 4、使用
* 5、卸载
*
* jvm中HungrySingleton只会被实例化一次,因为会存在缓存中
*/
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
// 防止多例
if(null != instance){
throw new RuntimeException("单例不允许多个示例。");
}
}
public static HungrySingleton getInstance() {
return instance;
}
}
三、匿名内部类
静态属性getInstance时才会初始化SingletonHolder,调用其他属性则不会初始化SingletonHolder
可以利用反射破解~~~
参考示例
因为是内部类中实现的,示例中代码如果看着不清晰的话,可以按照下面的方式拆开
- InnerClassSingletonDemo:启动类
- InnerClassSingleton:属性类,SingletonHolder:InnerClassSingleton中的内部单例类
/**
* 匿名内部类
*
* @author liushiwei
*/
public class InnerClassSingletonDemo {
public static void main(String[] args) {
// 不会初始化 SingletonHolder
System.out.println(InnerClassSingleton.name);
// 会初始化 SingletonHolder
System.out.println(InnerClassSingleton.getInstance());
// 反射破解
Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
InnerClassSingleton newInstance1 =declaredConstructor.newInstance();
InnerClassSingleton newInstance2 =declaredConstructor.newInstance();
System.out.println(newInstance1.equals(newInstance2));
}
}
class InnerClassSingleton{
static {
System.out.println("InnerClassSingleton init");
}
public static String name ="innerClass";
/**
* 匿名内部类
* 相对饿汉模式,内部类的优势是只有真正调用
* 静态属性getInstance时才会初始化SingletonHolder,
* 调用其他属性则不会初始化SingletonHolder
* 延迟加载的形式,饿汉和懒汉的结合
*/
private static class SingletonHolder{
static {
System.out.println("SingletonHolder init");
}
private static InnerClassSingleton instance = new InnerClassSingleton();
}
/**
* 构造方法
*/
private InnerClassSingleton(){
}
static InnerClassSingleton getInstance(){
return SingletonHolder.instance;
}
}
四、枚举类
枚举类型由enum声明,它和class一样是关键字,enum是无法被继承的。
- 枚举只能拥有私有的构造器
- 枚举类实际上是一个继承Enum的一个final类
- 枚举类不允许被反序列化,Enum重写了方法
- 静态代码块中对final变量的值进行初始化
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法
/**
* 枚举类
* @author liushiwei
*/
public enum SingletonEnum {
INSTANCE;
public void print(){
System.out.println("111");
}
}
javap -p
反编译 SingletonEnum.class,可以看到
- SingletonEnum类是被final修饰的不能被继承
- 成员变量都被声明为static final,表明其为类变量,类加载的准备阶段这些变量就会被初始化并赋值。
查看反射中创建对象源码,枚举是不可以反射的