单例模式(饿汉懒汉、内部类、枚举)

单例模式

概念

单例模式(Singleton Pattern)的定义为:确保一个类在任何情况下只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式是创建型模式。

单例模式分为

  • 饿汉式单例

  • 懒汉式单例

知识铺垫:Java枚举

关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。

public enum t { SPRING,SUMMER; }

饿汉式单例

概念

饿汉式单例模式就是在类加载的时候就立即初始化,并且创建单例对象。不管你有没有用到,都先建好了再说。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

优缺点

优点线程安全,没有加任何锁、执行效率比较高。

缺点:类加载的时候就初始化,不管后期用不用都占着空间,浪费了内存

简单的饿汉式写法(代码)

 public class Hungry {
     //类初始化的时候 就进行实例化
     private static final Hungry hungry= new Hungry();
     //构造函数
     privat Hungry() {
     }
     //getInstance方法返回实例
     private static Hungry getInstance(){
         return hungry;
     }
 }

饿汉式单例适合用在单例类比较少的情况下,在实际项目中,有可能会存在很多的单例类,如果我们都使用饿汉式单例的话,对内存的浪费会很大,所以,我们要学习更优的写法。(懒汉式)

懒汉式单例

概念

懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则直接返回,没有则新建。

要点

  1. 定义为静态成员变量

  2. 私有化构造方法

  3. 公共静态方法获取单例对象,内部判空

简单实现

 
public class Lazy {
     private static  Lazy lazy;
 ​
     // 让构造函数为private,这样该类就不会被实例化
     private Lazy() {
         System.out.println(Thread.currentThread()+"");
     }
 ​
     private static Lazy getInstance(){
         //懒汉式 如果实例不存在 则创建
         if (lazy == null){
             lazy = new Lazy();
         }
         return lazy;
     }
 ​
     public static void main(String[] args) {
         for (int i = 0; i < 10; i++) {
             new Thread(()->{
                 Lazy.getInstance();
             }).start();
         }
 ​
     }

上述代码在单线程下能够完美运行,但是在多线程下存在安全隐患。

多线程下的结果

 Thread[Thread-0,5,main]
 Thread[Thread-3,5,main]
 Thread[Thread-1,5,main]
 Thread[Thread-2,5,main]
情况1:

每次线程执行getInstance方法,得到的结果是我们想要的

情况2:

此种情形下,该种写法的单例模式会出现多线程安全问题,得到两个完全不同的对象

情况3:

该种情形下,虽然表面上最终得到的对象是同一个,但是在底层上其实是生成了2个对象,只不过是后者覆盖了前者,不符合单例模式绝对只有一个实例的要求。

多线程下的单例模式的懒汉式写法

synchronized加锁

 public class LazyUpgrade {
     private static  LazyUpgrade instance;
 ​
     // 让构造函数为private,这样该类就不会被实例化
     private LazyUpgrade() {
         System.out.println(Thread.currentThread()+"");
     }
 ​
     private static synchronized LazyUpgrade getInstance(){
         //懒汉式 如果实例不存在 则创建
         if (instance == null){
             instance = new LazyUpgrade();
         }
             return instance;
 }

但是用synchronized加锁时,在线程数量较多的情况下,会导致大批线程阻塞从而导致程序性能大幅下降

双重检查锁
 
public class LazyUpgrade {
     private volatile static  LazyUpgrade instance;//volatile为了避免指令重排
 ​
     // 让构造函数为private,这样该类就不会被实例化
     private LazyUpgrade() {
         System.out.println(Thread.currentThread()+"");
     }
 ​
     private static LazyUpgrade getInstance(){
 //        第一重检查是为了确认instance是否已经被实例化,
 //        如果是,直接返回实例化对象
 //        否则进入同步代码块进行创建,避免每次都排队进入同步代码块影响效率;
         if (instance == null){
             //线程会被阻塞在这里
             synchronized (LazyUpgrade.class){
                 if (instance == null){
 //                    第二重检查是真正与实例的创建相关,如果instance未被实例化,则在此过程中被实例化
                     instance = new LazyUpgrade();//不是一个原子性操作;
                 }
             }
 ​
         }
         return instance;
 }

当第一个线程调用 getInstance()方法时,第二个线程也可以调用。当第一个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整 个 LazySimpleSingleton 类的阻塞,而是在 getInstance()方法内部的阻塞,只要逻辑不太复杂,对于 调用者而言感知不到。

但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道没有更好的方案吗?显示是有的,我们可以从类初始化的角度来考虑,采用静态内部类的方式。

静态内部类实现懒汉式单例

静态内部类模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的方法/属性被调用时才会被加载,并初始化静态属性,静态属性由于被static修饰,保证只能被初始化一次,并且严格保证实例化顺序。

静态内部类模式是一种优秀的单例模式。在没有任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间浪费。

 
public class LazyInner {
 ​
     //如果没有使用,则内部类不会加载;
     private LazyInner() {
         System.out.println(Thread.currentThread()+"");
     }
 ​
     //定义一个静态内部类, 在内部类中来创建实例;
     //默认不会加载
     public static class InnerClass{
         private static final LazyInner INSYANCE = new LazyInner();
     }
 ​
     //对外访问方法 static 为了使单例的空间共享,保证这个方法不会被重写和重载
     public static LazyInner getInstance(){
         return InnerClass.INSYANCE;
     }
 -----
     public static void main(String[] args) {
         for (int i = 0; i < 10; i++) {
             new Thread(()->{
                 LazyInner.getInstance();
             }).start();
         }
     }
 }
反射破坏单例

如果我们通过反射来调用其构造方法,在调用 getInstance() 方法得到 new 出来的实例,应该会存在两个不同的实例。

那么我们继续升级:

一旦检测到对象已经被实例化,但是构造方法仍然被调用时直接抛出异常。(也就是说如果构造方法一旦出现多次创建,则直接抛出异常:)

 public class LazyInner {
 ​
     private LazyInner() {
         if (InnerClass.INSYANCE != null){
             throw new RuntimeException("实例被重复创建");
         }
         System.out.println(Thread.currentThread()+"");
     }
     //对外访问方法
     public static LazyInner getInstance(){
         return InnerClass.INSYANCE;
     }
 ​
     //定义一个静态内部类
     //未被使用的话是不会加载的;
     public static class InnerClass{
         private static final LazyInner INSYANCE = new LazyInner();
     }
 ​

序列化破坏单例

一个单例对象创建好后,有时候我们需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象 并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化 的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。

public class SeriableSingle implements Serializable {
     private static final SeriableSingle INSTANCE= new SeriableSingle;
 ​
     private SeriableSingle() {
     }
 ​
     private static SeriableSingle getInstance(){
         return INSTANCE;
     }
 ​
     private Object readResolve(){
         return INSTANCE;
     }
 }

虽然增加 readResolve()方法返回实例解决了单例模式被破坏的 问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。

注册式单例模式

注册式单例模式又叫登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识 获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式

枚举式单例模式
public enum EnumSingle {
     INSTANCE;//枚举类中放一个参数;
 ​
     private Object data;
 ​
     public Object getData() {
         return data;
     }
 ​
     public void setData(Object data) {
         this.data = data;
     }
 ​
     public static EnumSingle getInstance(){
             return INSTANCE;
         }
 }
容器式单例

容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的。

参考链接:设计模式篇——单例模式详解(从此面试不再怕) (baidu.com)

一文带你搞定单例模式 - 知乎 (zhihu.com)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值