单例模式,毋庸置疑,(这里以java语言来描述)字面意思就是在一个java进程中,只会存在一个对象实例。
单例模式主要分为4种,饿汉式,懒汉式,委托,枚举;
1.饿汉式:
/**
*
* 测试饿汉式
* 能够保证线程的安全性,但是没有延迟加载,实例化完成后,
* 如果长时间不进行使用,会造成内存空间的浪费,因为单例模式的实例中如果包含很多的静态成员变量
* 占用内存比较大,则内存空间的浪费就会非常明显。所以如果提高其性能,需要进行懒加载
*
* @date 2020-01-01
* @since
*/
public class HungryDemo {
private static byte[] bytes = new byte[1024];
public static String str = "just test";
static {
System.out.println("hungry singleton");
}
/**
* 定义私有的构造器
*/
private HungryDemo() {}
/**
* HungryDemo中的静态变量在类加载期间就会初始化,
* 加载 => classLoader将类字节码加载到jvm内存中
* 验证 => 验证字节码文件的正确性
* 准备 => 为静态变量赋予初始值
* 解析 => 将符号引用转变为直接引用
* 初始化 => 初始化静态代码块(静态变量和静态代码块最后编译后会统计到静态代码块中)和全局变量值,在<clinit>方法中执行
* 此时的HungryDemo会被初始化,因为涉及到创建对象,便会去JVM中查找因此进行对象的创建(此时的class对象已经创建完毕),
* 然后调用<init>(构造器函数)完成对象的创建。
*
*/
private static HungryDemo instance = new HungryDemo();
/**
* 调用此方法时才会触发类加载
* 首先会调用jvm的方法区中查找是否存在HungryDemo的类信息(class对象信息)
* 不存在,则调用classloader来加载HungryDemo进行类加载,然后就是后面的类加载的过程了。
*/
public static HungryDemo getInstance() {
return instance;
}
}
2. 懒汉式:
/**
* 测试懒汉式, 记住,
* 懒汉式的实例中一般都包含很多的成员变量
*
* @date 2020-01-01
* @since
*/
public class LazyDemo {
public static String name = "lazy singleton";
private volatile static LazyDemo instance;
static {
System.out.println("lazy singleton");
}
private LazyDemo() {}
public static LazyDemo getInstance() {
return instance;
}
/**
* 这样处理在多线程的情况下会造成多实例的错误
* 会出现线程安全的问题
*
* @return
*/
public static LazyDemo getInstance1() {
if (Objects.isNull(instance)) {
instance = new LazyDemo();
}
return instance;
}
public static LazyDemo getInstance2() {
// 多线程调用会变为重量级锁,性能低下
synchronized (LazyDemo.class) {
if (Objects.isNull(instance)) {
instance = new LazyDemo();
}
}
return instance;
}
/**
* 保证了懒加载,和线程安全,但是效率不高
*
* @date 2020-01-01
* @updateDate 2020-01-01
* @param
* @return
*/
public static LazyDemo getInstance3() {
// DCL,和上一个方法对比,不为空则直接获取实例,不会出现串行的情况,提升了性能。
if (Objects.isNull(instance)) {
synchronized (LazyDemo.class) {
// 这里在做一次判断,是为了保证2个线程同时进入了第一个判断代码块内
if (Objects.isNull(instance)) {
// 创建一个对象,编译成字节码后,创建对象分为三部分执行
// 23: new #4 // class com/feifei/demo/singleton/LazyDemo 申请内存空间
// 26: dup // 复制栈顶元素并压入栈顶
// 27: invokespecial #5 // Method "<init>":()V // 执行构造器相关的方法
// 30: putstatic #2 // Field instance:Lcom/feifei/demo/singleton/LazyDemo; // 将变量压入常量池中
// 这26, 27, 30可能会发生指令重排的问题,也就是30 在 27执行执行,然后此时线程2进入方法直接返回对象,但是此时对象还未执行构造器初始化,造成对象没有达到安全发布
// 所以需要加入volatile关键字,让对象创建期间不会发生指令重排的问题
instance = new LazyDemo();
}
}
}
return instance;
}
}
3. 委托给静态内部类:
/**
* 构造模式
*
* 声明类的时候,成员变量中不声明实例变量,而是放到内部的静态类中
* 将实例变量委托给静态内部类,当主动去调用getInstance的时候,
* 才会创建对象,实现了懒加载
*
* 这是目前比较优雅的方法,即没有用到如何的锁机制,也不需要担心线程安全性的问题
*
* @date 2020-01-01
* @since
*/
public class HolderDemo {
public static String name = "holder singleton name";
static {
System.out.println("holder singleton");
}
private HolderDemo() {
}
/**
* 定义一个静态内部类, 外部类的类加载并不会影响到内部类的加载,
* 只要外部类的变量没有涉及到内部类的创建即可
*/
private static class InnerHolder {
static {
System.out.println("inner holder singleton");
}
private static HolderDemo instance = new HolderDemo();
}
public static HolderDemo getInstance() {
// 此时才会触发静态内部类的类加载已经对象的创建
return InnerHolder.instance;
}
}
4. 枚举
/**
* 枚举测试,枚举天生就是一个单例
*
* @date 2020-01-01
* @since
*/
public enum EnumDemo {
/**
* 单例对象引用
*/
ENUM_DEMO;
private static String name = "enum singleton";
EnumDemo() {}
public static EnumDemo getInstance() {
return EnumDemo.ENUM_DEMO;
}
}
最后,我们编写一个测试类Test:
/**
* @date 2020-01-01
* @since
*/
public class Test {
public static void main(String[] args) {
// 此时的类初始化了,类加载后会导致instance实例的创建
System.out.println(HungryDemo.str);
System.out.println(HungryDemo.getInstance());
// 此时类初始化了,但是并没有对instance的实例进行创建
System.out.println(LazyDemo.name);
System.out.println(LazyDemo.getInstance());
// 调用此方法才会造成实例的创建,这才是真正的懒加载
System.out.println(LazyDemo.getInstance3());
// 采用委托给静态内部类来完成懒加载,其内部类并不会触发类加载
System.out.println(HolderDemo.name);
// 此时才会触发内部类的类加载和实例的创建
System.out.println(HolderDemo.getInstance());
}
}
如果针对类型的区分推荐采用枚举类,如果涉及到单例的使用,建议使用委托给内部类来实现的构造模式来实现。
如有疑问,可留言。