模式 01 单例模式 Singleton

设计模式 单例模式 Singleton


简介

单例:保证在内存当中只有一个实例存在

在代码实现级别保证只能有一个实例存在


使用场景

  • 各种Manager
  • 各种Factory

实现要求

  • 线程安全
  • 最好懒加载(延迟加载)
  • 执行效率高
  • 序列化与反序列化安全

实现

有8种写法,这里融合了一下,将基础的和最推荐的写了出来。其中饿汉式中包含了两种:静态变量和静态代码块


方式一:饿汉式 (推荐使用)

饿汉式,如其名。在类加载的时候就创建了单例,并不等到第一次调用的时候,就仿佛饿汉一样迫不及待地想赶紧先得到一个单例。


示例
/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化,会拖慢程序启动速度
 */
public class Mgr01 {
    // 自己创建一个实例留给其他类使用
    // 方式一:直接创建并初始化对象(静态常量)
    private static final Mgr01 INSTANCE = new Mgr01();
    
    /*
    // 方式二 创建变量,将初始化过程放在status代码块中(静态代码块)
    private static final Mgr01 INSTANCE;
    
    status {
    	INSTANCE = new Mgr01();
    }
    */
    // 保证别人创建不了这个对象的实例
    private Mgr01() {}
	// 其他类获取实例的唯一方法
    public static Mgr01 getInstance() {
        return INSTANCE;
    }
    // 单例中的行为即方法
    public void m(){
        System.out.println("m");
    }
}
// 测试
public static void main(String[] args) {
    Mgr01 m1 = Mgr01.getInstance();
    Mgr01 m2 = Mgr01.getInstance();
    System.out.println(m1 == m2);
}

// 结果
> true
// 通过判断两次获取的对象实例的地址都是同一个,说明两次获取的均为同一个实例,即为单例

解析

如果要保证Mgr01这个对象在内存中只能有一个实例,那么构造方法就肯定不能被其他类调用。所以这里构造方法设置成private私有的

那么禁止别人创建实例,自己就要先创建一个实例,留着给别人用,那么就出现了创建statusfinal修饰的Mgr01的实例

那么就还需要一个外界能访问并得到实例的方法,于是便有了getInstance()方法。


优点

由于在类加载阶段就已经创建了单例,那么在运行时JVM就能保证线程安全。


缺点

不管用到与否,类装载时就完成实例化,会拖慢程序启动速度


方式二:懒汉式 (存在线程问题)

懒汉式,如其名。在类加载时并不获取单例,而是在第一次使用时,才初始化并获取这个单例。仿佛是懒的动,到使用前才去拿一样。


示例
/**
 * 懒汉式 延迟加载(lazy loading)
 * 虽然达到了按需初始化的目的,但却带来了多线程访问不安全的情况
 */
public class Mgr03 {
    // 自己创建一个变量,但是没有初始化
    private static Mgr03 INSTANCE;
	// 保证别人创建不了这个对象的实例
    private Mgr03 (){}
	// 其他类获取实例的唯一方法
    public static Mgr03 getInstance() {
        // 在其他类每次获取时都会去判断,这个变量是否为null,如果是null的话,创建一个实例并返回,否则直接返回
        if (INSTANCE == null) {
            // 此处的try-catch包裹的线程睡眠,是为了测试当多线程同时获取时的问题而写的,否则可不写try-catch里的内容
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }
}
// 测试 
// 多线程测试,测试10个线程去获取这个单例的情况。
// 通过输出hash值来判断是否时同一个实例。同一个类不同的对象的hash值是不同的
public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        // 使用lambda表达式创建线程
        new Thread(() -> 
            System.out.println(Mgr03.getInstance().hashCode())
        ).start();
    }
}
// 结果
1754195085
135417039
478974226
218859975
546405844
224049094
224049094
1037172763
79372097
858497792
// 通过结果可以看出,基本每一个对象的hash值都说是不同的,也就是说,每一个实例都是新的实例,这就造成了获取单例失败的问题    

解析

前面还是将构造方法设置为私有的,并创建一个静态的变量INSTANCE用于存放实例。

但是此时这个变量并没有被初始化,所以不能用final修饰。

那么初始化的过程留在了获取实例的方法中。每一个调用者访问getInstance()方法时,都要先判断INSTANCE变量是不是被初始化过,如果为null,那么就初始化一次;如果不为null,那么直接返回。

但是当多线程同时访问getInstance()方法时就会发生意外。比如线程A优先访问了getInstance()方法,当它还在方法体内判断为null,要去但还没来得及去调用构造方法初始化对象时*(代码中的线程睡眠就是为了延长这里的执行时间)*,线程B进入了getInstance()方法,此时它判断也是null,那么它也去调用构造方法初始化对象去了。此时有可能不止只有AB,可能还会有很多线程都进去了。那么每一个线程所获得的实例都不是同一个实例,而是自己线程所创建的实例。INSTANCE最后的值将由最后一个判断为``null`的线程所创建的实例决定。


优点

不在类加载时初始化获取单例,而是在初次使用时初始化,加快了程序启动的速度


缺点

在未初始化时,当多线程同时获取实例时,有几率返回多个不同的实例出来,就不再是单例了,虽然后续的实例是同一个。


方式三:加锁的懒汉式

主要采用:同步方法

懒汉式在未初始化时多线程同时访问是会有线程安全问题的。


示例
/**
 * 加锁的懒汉式
 * 在getInstance()方法前加关键字:synchronized
 */
public class Mgr04 {
    private static Mgr04 INSTANCE;
    private Mgr04 (){}

    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }
}
// 测试 
// 多线程测试,测试100个线程去获取这个单例的情况。
// 通过输出hash值来判断是否时同一个实例。同一个类不同的对象的hash值是不同的
public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
        // 使用lambda表达式创建线程
        new Thread(() -> 
            System.out.println(Mgr04.getInstance().hashCode())
        ).start();
    }
}
// 多次实验结果
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
// 通过结果可以看出,加锁的懒汉式返回的就是同一个对象了

解析

synchronized关键字在这里锁定的是Mgr04.class

那么加把锁让多线程在同时获取实例时一个一个进,前一个出来了后一个才能进.


优点

使得懒汉式变得线程安全


缺点

由于在获取单例的方法上加了锁,所以所有的线程到这里都变成了串行,严重影响效率


方式四:双重检查+加锁的懒汉式

号称:完美的写法之一

主要采用:同步代码块 + 双重检查

像是方式三那样给懒汉式加锁,会导致性能变低。那么有效的缩小加锁范围也能够提高性能


示例
/**
 * 双重检查+加锁的懒汉式
 * 在getInstance()方法前加关键字:synchronized
 */
public class Mgr05 {
    private static Mgr05 INSTANCE;
    private Mgr05 (){}

    public static Mgr05 getInstance() {
        // 第一重检查
        if (INSTANCE == null) {
            // 加锁
            synchronized (Mgr05.class) {
                // 第二重检查
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr05();
                }
            }
        }
        return INSTANCE;
    }
}
// 测试 
// 多线程测试,测试20个线程去获取这个单例的情况。
// 通过输出hash值来判断是否时同一个实例。同一个类不同的对象的hash值是不同的
public static void main(String[] args) {
    for (int i = 0; i < 20; i++) {
        // 使用lambda表达式创建线程
        new Thread(() -> 
            System.out.println(Mgr05.getInstance().hashCode())
        ).start();
    }
}
// 多次执行结果
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
1754195085
// 通过结果可以看出,返回的就是同一个对象

解析

synchronized关键字在这里锁的是Mgr05.class

有效的缩小加锁范围也能够提高性能。

但是如果只有第一重检查和加锁,没有第二重检查。那么可能存在线程A在判断为null后,被线程B继续执行了,然后线程B创建了实例,释放锁。此时线程A拿到了锁,因为在第一重检查已经判断为null,所以直接拿锁执行创建实例的操作。又造成了线程不安全的情况。

所以加了第二道检查,如果线程A拿着锁进入时,再判断发现变量INSTANCE不为null了,说明已经被其他线程抢先初始化了,那么线程A便放弃创建直接返回值就行。


优点

使得懒汉式变得线程安全的同时减少了被锁包裹的代码范围


缺点

缩小了锁的范围,即实现了减小锁带来的低效率,又实现了懒加载,降低系统启动的时间


方式五:静态内部类

号称:完美的写法之一

通过内部类调用私有构造的方式返回单例,由 JVM 保证线程安全


示例
public class Mgr06 {
	// 构造方法依旧是私有的
    private Mgr06(){}
	// 创建内部类:Mgr06Holder(意为:Mgr06的持有者)
    private static class Mgr06Holder {
        // 内部类调用并创建外部类的私有构造
        private static final Mgr06 INSTANCE = new Mgr06();
    }
	// 为调用者返回一个单例
    public static Mgr06 getInstance() {
        // 调用内部类的静态私有成员变量
        return Mgr06Holder.INSTANCE;
    }
}
// 测试 还是使用多线程测试
public static void main(String[] args) {
    for (int i = 0; i < 20; i++) {
        // 使用lambda表达式创建线程
        new Thread(() ->
        	System.out.println(Mgr05.getInstance().hashCode())
        ).start();
    }
}
// 多次执行结果
218859975
218859975
218859975
218859975
218859975
218859975
218859975
218859975
218859975
// 通过结果可以看出,返回的就是同一个对象 

规则

内部类和它的外部类性质:

  • 内部类可以调用它的外部类的私有构造和私有成员变量以及私有方法
  • 一个类可以调用内部类的私有成员变量

JVM规则:

  • 同一个类class只能加载一次,在加载class时就会把status修饰的变量或者代码块或者方法执行了。

  • 类加载时不会加载其内部类


解析

首先,构造方法依旧是私有的,防止被外部其他类创建对象;

然后,创建一个静态内部类,在内部类里调用其外部类Mgr06的私有构造创建实例并赋值给内部类的privatestatusfinal的变量INSTANCE

再然后,创建一个getInstance()方法,提供外部调用,返回的是内部类的私有变量,也就是该类的单例。


执行过程:

  1. 程序加载时,加载了Mgr06类,但是其内部类并没有被加载。

  2. 第一次访问getInstance()方法时,JVM首次加载了Mgr06Holder类,此时同时也执行了创建Mgr06的实例并赋值给变量INSTANCE的操作。

  3. 此时Mgr06Holder类已经被JVM加载过一次了,根据JVM的规定哪怕是多线程此时也无法再一次的加载Mgr06Holder类,且变量INSTANCE是用final修饰的,也就是说变量INSTANCE将永远不会被再次初始化或赋值。而此时该变量里装着的就是Mgr06类的一个实例。

  4. 那么,每一次返回Mgr06Holder类的变量INSTANCE都是将同一个Mgr06类的一个实例返回,从而达到返回单例的效果。


优点
  1. 解决了线程安全问题

  2. 实现了懒加载

  3. 解决了使用锁带来的效率低下的问题


缺点

不能防止反序列化,反序列化仍然会创建一个新的实例。

可以通过额外的工作(Serializable、transient、readResolve())来实现序列化,防止这个问题。


方式六:枚举单例

号称:完美的写法之一

在《Effective Java》中讲到的

不仅可以解决线程同步,还可以解决反序列化


示例
public enum  Mgr07 {
    // 单例
    INSTANCE;

    // 该类的行为方法
    public void whateverMethod() {
        // ...
    }
}
// 测试
public static void main(String[] args) {
    for (int i = 0; i < 20; i++) {
        // 使用lambda表达式创建线程
        new Thread(() ->
            System.out.println(Mgr07.INSTANCE.hashCode())
        ).start();
    }
    Mgr07.INSTANCE.m();
}
// 结果
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
2132637868
// 通过结果可以看出,返回的就是同一个对象      

解析

借助JDK1.5中添加的枚举来实现单例模式。

不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。


优点

最优雅的实现方式. 系统内存 中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。


缺点

当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

写法区别,比如:

  • 其他的方式:Mgr01.getInstance().m();
  • 枚举单例方式:Mgr07.INSTANCE.m();

本模式完。

回到设计模式系列

祝君有所收获,完结撒花!

如果您想关注更多,可以点击以下链接:

回到学习路线

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值