Java设计模式-3、单例模式

单例模式

单例模式属于创建型模式,⼀个单例类在任何情况下都只存在⼀个实例, 构造⽅法必须是私有的、由⾃⼰创建⼀个静态变量存储实例,对外提供⼀ 个静态公有⽅法获取实例。

优点是内存中只有⼀个实例,减少了开销,尤其是频繁创建和销毁实例的 情况下并且可以避免对资源的多重占⽤。

缺点是没有抽象层,难以扩展, 与单⼀职责原则冲突。

单例模式的设计规则

由定义我们可以很清晰的抽象出: 实现Java单例模式类有哪些通用设计规则?

(1)私有化类构造器。

(2)定义静态私有的类对象。

(3)提供公共静态的获取该私有类对象的方法。

了解了单例模式的概念,以及单例模式的通用设计规则,对于如何实现一个Java单例,应该是没什么阻碍了。这里我们还是要思考下单例模式的优点,或者说有啥好处,使用场景是什么?带着这些问题我们就能更好的设计单例模式。

为什么使用单例

1.Java单例模式解决了什么问题?

答:Java的单例模式主要解决了多线程并发访问共享资源的线程安全问题。

2.Java单例模式主要应用场景有哪些?

答:1.共享资源的访问与操作场景,如Windows系统的资源管理器,Windows系统的回收站,显卡的驱动程序,系统的配置文件,工厂本身(类模板),应用程序的日志对象等。

2.控制资源访问与操作的场景,如数据库的连接池,Java的线程池等。

 单例模式的常⻅写法有哪些?

 1. 饿汉式,线程安全

饿汉式单例模式,顾名思义,类⼀加载就创建对象,这种⽅式⽐较常⽤,
但容易产⽣垃圾对象,浪费内存空间。
优点:线程安全,没有加锁,执⾏效率较⾼
缺点:不是懒加载,类加载时就初始化,浪费内存空间

饿汉式单例是如何保证线程安全的呢?
答:java虚拟机加载类的过程时线程安全的,ClassLoader.loadClass方法中使用Synchronized关键字实现。
核心代码:

它是基于类加载机制避免了多线程
的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例。
代码如下:
public class Singleton {
	private static final Singleton instance = new Singleton();

	// 私有的默认构造函数
	public Singleton() {
		
	}

	// 静态工厂方法
	public static Singleton getInstance() {
		return instance;
	}

	public void get() {
        System.out.println("get");
    }
	
	public static void main(String[] args) {
		Singleton.getInstance().get();
	}
}
使⽤反射破坏单例,代码如下:
public class Singleton {

    private static final Singleton instance = new Singleton();

    // 私有的默认构造函数
    private Singleton() {

    }

    // 静态工厂方法
    public static Singleton getInstance() {
        return instance;
    }

    public void get() {
        System.out.println("get");
    }

//	public static void main(String[] args) {
//		Singleton.getInstance().get();
//	}

    public static void main(String[] args) throws Exception {
        // 使⽤反射破坏单例
        // 获取空参构造⽅法
        Constructor<Singleton> declaredConstructor =
                Singleton.class.getDeclaredConstructor(null);
        // 设置强制访问
        declaredConstructor.setAccessible(true);
        // 创建实例
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println("反射创建的实例" + singleton);
        System.out.println("正常创建的实例" +
                Singleton.getInstance());
        System.out.println("正常创建的实例" +
                Singleton.getInstance());
    }
}

结果:

 2. 懒汉式,线程不安全

这种⽅式在单线程下使⽤没有问题,对于多线程是⽆法保证单例的,这⾥
列出来是为了和后⾯使⽤锁保证线程安全的单例做对⽐。

优点:懒加载
缺点:线程不安全
代码实现如下:
/**
* 懒汉式单例,线程不安全
*
*/
public class Singleton {
 // 1、私有化构造⽅法
 private Singleton(){ }
 // 2、定义⼀个静态变量指向⾃⼰类型
 private static Singleton instance;
 // 3、对外提供⼀个公共的⽅法获取实例
 public static Singleton getInstance() {
 // 判断为 null 的时候再创建对象
 if (instance == null) {
 instance = new Singleton();
 }
 return instance;
 }
}
使⽤多线程破坏单例,测试代码如下:
public class Test {
 public static void main(String[] args) {
 for (int i = 0; i < 3; i++) {
 new Thread(() -> {
 System.out.println("多线程创建的单例:" +
Singleton.getInstance());
 }).start();
 }
 }
}

结果: 

 懒汉式,线程安全

懒汉式单例如何保证线程安全呢?通过 synchronized 关键字加锁保证线程 安全, synchronized 可以添加在⽅法上⾯,也可以添加在代码块上⾯,这 ⾥演示添加在⽅法上⾯,存在的问题是 每⼀次调⽤ getInstance 获取实例时 都需要加锁和释放锁,这样是⾮常影响性能的。
优点:懒加载,线程安全
缺点:效率较低
代码实现如下

public class Singleton {
 // 1、私有化构造⽅法
 private Singleton(){ }
 // 2、定义⼀个静态变量指向⾃⼰类型
 private static Singleton instance;
 // 3、对外提供⼀个公共的⽅法获取实例
 public synchronized static Singleton getInstance() {
 if (instance == null) {
 instance = new Singleton();
 }
 return instance;
 }
}
双重检查锁( DCL , 即 double-checked locking )常用
实现代码如下:
public class Singleton {
 // 1、私有化构造⽅法
 private Singleton() {
 }
 // 2、定义⼀个静态变量指向⾃⼰类型
 private volatile static Singleton instance;
 // 3、对外提供⼀个公共的⽅法获取实例
 public static Singleton getInstance() {
 // 第⼀重检查是否为 null
 if (instance == null) {
 // 使⽤ synchronized 加锁
 synchronized (Singleton.class) {
 // 第⼆重检查是否为 null
 if (instance == null) {
 // new 关键字创建对象不是原⼦操作
 instance = new Singleton();
 }
 }
 }
 return instance;
 }
}
优点:懒加载,线程安全,效率较⾼
缺点:实现较复杂
这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,为什么 要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就 不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进 ⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调 ⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同 步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创 建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回了单例。
关于内部的第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁 竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null ,会进⾏单例对 象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返 回已创建的单例对象。
双重检查锁中使⽤ volatile 的两个重要特性: 可⻅性、禁⽌指令重排序

 这⾥为什么要使⽤ volatile

这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯
的步骤:
1. 在堆内存开辟内存空间
2. 调⽤构造⽅法,初始化对象
3. 引⽤变量指向堆内存空间
对应字节码指令如下:

为了提⾼性能,编译器和处理器常常会对既定的代码执⾏顺序进⾏指令重 排序,从源码到最终执⾏指令会经历如下流程:
源码编译器优化重排序指令级并⾏重排序内存系统重排序最终执⾏指令序列, 所以经过指令重排序之后,创建对象的执⾏顺序可能为 1 2 3 或者 1 3  2 ,因此当某个线程在乱序运⾏ 1 3 2 指令的时候,引⽤变量指向堆内存 空间,这个对象不为 null ,但是没有初始化,其他线程有可能这个时候进
⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 nulll ,导致错误使 ⽤了没有初始化的⾮ null 实例,这样的话就会出现异常,这个就是著名的 DCL 失效问题。

当我们在引⽤变量上⾯添加 volatile 关键字以后,会通过在创建对象指令 的前后添加内存屏障来 禁⽌指令重排序 ,就可以避免这个问题,⽽且对 volatile 修饰的变量的修改对其他任何线程都是可⻅的。
静态内部类
代码实现如下:
public class Singleton {
 // 1、私有化构造⽅法
 private Singleton() {
 }
 // 2、对外提供获取实例的公共⽅法
 public static Singleton getInstance() {
 return InnerClass.INSTANCE;
 }
 // 定义静态内部类
 private static class InnerClass{
 private final static Singleton INSTANCE = new
Singleton();
 }
}
优点:懒加载,线程安全,效率较⾼,实现简单
静态内部类单例是如何实现懒加载的呢?⾸先,我们先了解下类的加载时 机。
虚拟机规范要求有且只有 5 种情况必须⽴即对类进⾏初始化(加载、验 证、准备需要在此之前开始):
1. 遇到 new getstatic putstatic invokestatic 4 条字节码指令
时。⽣成这 4 条指令最常⻅的 Java 代码场景是:使⽤ new 关键字实
例化对象的时候、读取或设置⼀个类的静态字段( final 修饰除外,被
final 修饰的静态字段是常量,已在编译期把结果放⼊常量池)的时
候,以及调⽤⼀个类的静态⽅法的时候。
这里的逻辑,就是引用static的提前初始化,确定好了单例对象
2. 使⽤ java.lang.reflect 包⽅法对类进⾏反射调⽤的时候。
3. 当初始化⼀个类的时候,如果发现其⽗类还没有进⾏过初始化,则需要
先触发其⽗类的初始化。
4. 当虚拟机启动时,⽤户需要指定⼀个要执⾏的主类(包含 main() 的那
个类),虚拟机会先初始化这个主类。
5. 当使⽤ JDK 1.7 的动态语⾔⽀持时,如果⼀个 java.lang.invoke.MethodHandle 实例最后的解析结果是
REF_getStatic REF_putStatic REF_invokeStatic 的⽅法句柄,则需
要先触发这个⽅法句柄所对应的类的初始化。
getInstance() ⽅法被调⽤时, InnerClass 才在 Singleton 的运⾏时常量 池⾥,把符号引⽤替换为直接引⽤,这时静态对象 INSTANCE 也真正被创 建,然后再被 getInstance() ⽅法返回出去,这点同饿汉模式。

 枚举单例

代码实现如下:
public enum Singleton {
 INSTANCE;
 public void doSomething(String str) {
 System.out.println(str);
 }
}
优点:简单,⾼效,线程安全,可以避免通过反射破坏枚举单例枚举在 java 中与普通类⼀样,都能拥有字段与⽅法,⽽且枚举实例创建是 线程安全的,在任何情况下,它都是⼀个单例,可以直接通过如下⽅式调
⽤获取实例:
Singleton singleton = Singleton.INSTANCE;
使⽤下⾯的javap命令反编译枚举类
javap Singleton.class
得到如下内容
Compiled from "Singleton.java"
public final class com.whm.demo.singleton.Singleton
extends
java.lang.Enum<com.whm.demo.singleton.Singleton> {
 public static final
com.spring.demo.singleton.Singleton INSTANCE;
 public static com.whm.demo.singleton.Singleton[]
values();
 public static com.whm.demo.singleton.Singleton
valueOf(java.lang.String);
 public void doSomething(java.lang.String);
 static {};
从枚举的反编译结果可以看到, INSTANCE static final 修饰,所以可以 通过类名直接调⽤,并且创建对象的实例是在静态代码块中创建的 ,因为 static 类型的属性会在类被加载之后被初始化,当⼀个 Java 类第⼀次被真 正使⽤到的时候静态资源被初始化、Java 类的加载和初始化过程都是线程 安全的,所以创建⼀个 enum 类型是线程安全的。

通过反射验证破坏枚举,实现代码如下:

public class Test {
 public static void main(String[] args) throws
Exception {
 Singleton singleton = Singleton.INSTANCE;
 singleton.doSomething("hello enum");
 // 尝试使⽤反射破坏单例
 // 枚举类没有空参构造⽅法,反编译后可以看到枚举有⼀个两个
参数的构造⽅法
 Constructor<Singleton> declaredConstructor =
Singleton.class.getDeclaredConstructor(String.class,
int.class);
 // 设置强制访问
 declaredConstructor.setAccessible(true);
 // 创建实例,这⾥会报错,因为⽆法通过反射创建枚举的实例
 Singleton enumSingleton =
declaredConstructor.newInstance();
 System.out.println(enumSingleton);
 }
}
运⾏结果报如下错误:

 查看反射创建实例的 newInstance() ⽅法,有如下判断:

 所以⽆法通过反射创建枚举的实例。

单例模式总结:

Singleton 模式中的实例构造器可以设置为 protected 以允许子类派生。

Singleton 模式一般不要实现 Clone 接口,因为这有可能导致多个对象实例,与 Singleton 模式的初衷违背。

如何实现多线程环境下安全的 Singleton?     需注意对双检查锁的正确实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值