单例模式
单例模式
在整个运行时域,一个类只有一个实例对象
实现单例模式需要考虑的三点
1.是否线程安全
2.是否懒加载
3.能否反射破坏
单例模式的实现方式
懒汉式
实例对象式第一次被调用才构建,而不是启动时构建好的等你调用
好处:懒加载
缺点:线程不安全。在执行if (instance == null)时可能会有多个线程同时进入,就会实例化多次
public class Singleton {
//构造器私有,其他类无法用new来构造找个对象的实例
private Singleton() {}
//初始化对象为null
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式(线程安全)
好处:懒加载且线程安全,使用synchronized使得同一时刻只有一个线程能够进入getInstance方法
缺点:每次获取对象时都要进行同步操作,影响性能
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
饿汉式
从上面2个懒汉式对比中,可以看出线程安全问题出现在了构建对象的阶段,只要在编译期构建对象,在运行时调用,就不需要考虑线程安全问题了
好处:线程安全
缺点:不是懒加载,当构建对象开销大时,如果这个对象在项目启动时就构建,万一从来没被调用过,就会浪费资源
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
分析引入
线程安全的懒汉式中,形成低效的原因在于sychronized
那么只要在构建对象时同步,而可以直接使用对象时就没必要同步
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static Singleton getInstance() { //1
if (instance == null) { //2
synchronized (Singleton.class) { //3
instance = new Singleton(); //4
}
}
return instance;
}
}
现在所有线程都可以直接进入getInstance,然后进入第1步进行判断,如果实例对象还没构建,那么多个线程开始争抢锁,抢到手的那个线程开始创建实例对象,实例对象创建之后,以后所有线程在执行到第2步时,都可以直接跳过,返回实例对象进行调用,这样就不用去争抢锁,解决了低效的问题
但是,在多个线程执行语句2后,虽然只有一个线程抢到锁去执行语句3,但可能会有其他线程已经进入了if代码块,此时正在等待,一旦一个线程执行完,那么这个等待的线程就会立即获取锁,然后进行对象创建,这样对象会被创建多次,这样线程又不是安全的了
双检锁(DCL,double-checked locking)
两次对对象进行判空
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static Singleton getInstance() {
if (instance == null) { //1
synchronized (Singleton.class) {
if (instance == null) { //2
instance = new Singleton();
}
}
}
return instance;
}
}
假设a、b两个线程同时进入getInstance方法
a首先获取锁,b在进行等待,然后进行instance的构建,a构建完成后交还锁
a交还锁后b也会立即获得锁,此时它会在语句2中执行判断,而instance已经被线程a初始化,所以instance不为null,于是线程b将会直接退出,返回实例
这样一看就解决了上面几点问题
但是仍存在问题,这里多线程环境下没有遵循happens-before原则
instance = new Singleton();在指令层面,不是一个原子操作,分为三步
1.分配内存
2.初始化对象
3.对象指向内存地址
在真正执行时,JVM虚拟机为了效率可能会对指令进行重排,比如先执行第1步,再执行第3步,再执行第2步
按此顺序,线程a执行到第3步时,此时instance还没被初始化,假设此时有一个线程b执行到了语句1if (instance == null),此时在线程b中,instance == null返回false,直接跳到return instance;,而线程a中instance对象还未完成初始化,b线程中调用getInstance就返回还未被初始化的instance,出现线程不安全的情况。
public class Singleton {
private volatile static Singleton instance;
private Singleton () {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
从happens-before原则出发,只要给instance加上volatile修饰,就能阻止作用在instance上的指令重排问题
最常用写法,既满足懒加载,也满足线程安全,且性能比较高,就是写起来比较复杂
静态内部类
public class Singleton {
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
利用了Jdk类加载机制的特性实现了懒加载,同时也是线程安全的,也兼并了性能
以上几种均能反射破坏
反射破坏测试
public class Main {
public static void main(String[] args) throws IllegalAccessException,
InvocationTargetException, InstantiationException {
Constructor c = null;
try {
//构造器
c = Class.forName("Sington.DCL.Singleton").getDeclaredConstructor();
} catch (Exception e) {
e.printStackTrace();
}
if (c != null) {
c.setAccessible(true);
Singleton singleton1 = (Singleton)c.newInstance();
Singleton singleton2 = (Singleton)c.newInstance();
System.out.println(singleton1.equals(singleton2));// false,不是一个对象,非单例
}
}
}
反射从运行时类型信息中获取了构造器,并通过构造器构造了对象,而单例的目的就是阻止外部来构建对象
目前问题是如何拒绝JVM来读取类的私有方法,这时应该就无法通过上层代码来实现了
枚举
但是Jvm本身也提供了这种机制,就是枚举类型,对于枚举类型反射是无法获得它的构造器的,因此反射就不能破坏枚举类型的单例,而且枚举类型本身也能够保证线程安全,但是枚举无法实现懒加载,它在程序启动之初,就已经把这个内部的实例,完全构建好来提供给使用者
public enum Singleton {
INSTANCE;
}

浙公网安备 33010602011771号