8 种单例模式写法,助你搞定面试!

简介: 单例模式是一种设计模式,它限制了实例化一个对象的行为,始终至多只有一个实例。当只需要一个对象来协调整个系统的操作时,这种模式就非常有用.它描述了如何解决重复出现的设计问题,

1. 单例模式常见问题

为什么要有单例模式


单例模式是一种设计模式,它限制了实例化一个对象的行为,始终至多只有一个实例。当只需要一个对象来协调整个系统的操作时,这种模式就非常有用.它描述了如何解决重复出现的设计问题,


比如我们项目中的配置工具类,日志工具类等等。


如何设计单例模式 ?


1.单例类如何控制其实例化


2.如何确保只有一个实例


通过一下措施解决这些问题:


private构造函数,类的实例话不对外开放,由自己内部来完成这个操作,确保永远不会从类外部实例化类,避免外部随意new出来新的实例。


该实例通常存储为私有静态变量,提供一个静态方法,返回对实例的引用。如果是在多线程环境下则用锁或者内部类来解决线程安全性问题。


2. 单例类有哪些特点 ?

私有构造函数

它将阻止从类外部实例化新对象


它应该只有一个实例

这是通过在类中提供实例来方法完成的,阻止外部类或子类来创建实例。这是通过在java中使构造函数私有来完成的,这样任何类都不能访问构造函数,因此无法实例化它。


单实例应该是全局可访问的

单例类的实例应该是全局可访问的,以便每个类都可以使用它。在Java中,它是通过使实例的访问说明符为public来完成的。


节省内存,减少GC


因为是全局至多只有一个实例,避免了到处new对象,造成浪费内存,以及GC,有了单例模式可以避免这些问题。


3. 单例模式8种写法

下面由我给大家介绍8种单例模式的写法,各有千秋,存在即合理,通过自己的使用场景选一款使用即可。我们选择单例模式时的挑选标准或者说评估一种单例模式写法的优劣时通常会根据一下两种因素来衡量:


1.在多线程环境下行为是否线程安全


2.饿汉以及懒汉


3.编码是否优雅(理解起来是否比较直观)


1. 饿汉式线程安全的

public class SingleTon{  
 private static final SingleTon INSTANCE = new SingleTon();  
 private SingleTon(){ }  
 public static SingleTon getInstance(){  
  return INSTANCE;  
 }  
 public static void main(String[] args) {  
        SingleTon instance1 = SingleTon.getInstance();  
        SingleTon instance2 = SingleTon.getInstance();  
        System.out.println(instance1 == instance2);  
    }  
}   

这种写法是非常简单实用的,值得推荐,唯一缺点就是懒汉式的,也就是说不管是否需要用到这个方法,当类加载的时候都会生成一个对象。


除此之外,这种写法是线程安全的。类加载到内存后,就实例化一个单例,JVM保证线程安全。关注公众号Java技术栈回复设计模式获取我整理的系列Java设计模式教程。


2. 饿汉式线程安全(变种写法)。

public class SingleTon{  
 private static final SingleTon INSTANCE ;  
 static {  
     INSTANCE = new SingleTon();   
 }  
 private SingleTon(){}  
 public static SingleTon getInstance(){  
  return INSTANCE;  
 }  
        public static void main(String[] args) {  
        SingleTon instance1 = SingleTon.getInstance();  
        SingleTon instance2 = SingleTon.getInstance();  
        System.out.println(instance1 == instance2);  
    }  
}   

3. 懒汉式线程不安全

public class SingleTon{  
 private static  SingleTon instance ;  
 private SingleTon(){}  
 public static SingleTon getInstance(){  
            if(instance == null){  
                instance = new SingleTon();  
            }  
            return instance;  
 }  
 public static void main(String[] args) {  
        SingleTon instance1 = SingleTon.getInstance();  
        SingleTon instance2 = SingleTon.getInstance();  
        System.out.println(instance1 == instance2);  
        // 通过开启100个线程 比较是否是相同对象  
        for(int i=0;i<100;i++){  
             new Thread(()->  
                System.out.println(SingleTon.getInstance().hashCode())  
            ).start();  
        }  
    }  
}   

这种写法虽然达到了按需初始化的目的,但却带来线程不安全的问题,至于为什么在并发情况下上述的例子是不安全的呢 ?

// 通过开启100个线程 比较是否是相同对象  
for(int i=0;i<100;i++){  
     new Thread(()->  
        System.out.println(SingleTon.getInstance().hashCode())  
    ).start();  
}   

为了使效果更直观一点我们对getInstance 方法稍做修改,每个线程进入之后休眠一毫秒,这样做的目的是为了每个线程都尽可能获得cpu时间片去执行。代码如下

public static SingleTon getInstance(){  
   if(instance == null){  
       try {  
           Thread.sleep(1);  
       } catch (InterruptedException e) {  
           e.printStackTrace();  
       }  
       instance = new SingleTon();  
   }  
  return instance;  
}   

执行结果如下


image.png


上述的单例写法,我们是可以创造出多个实例的,至于为什么在这里要稍微解释一下,这里涉及了同步问题


造成线程不安全的原因:


当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完singleton是null的时候,线程A就进入了if块准备创造实例,但是同时另外一个线程B在线程A还未创造出实例之前,就又进行了singleton是否为null的判断,这时singleton依然为null,所以线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。


注:这里通过休眠一毫秒来模拟线程挂起,为初始化完instance

image.png


为了解决这个问题,我们可以采取加锁措施,所以有了下面这种写法


4. 懒汉式线程安全(粗粒度Synchronized)。

public class SingleTon{  
 private static  SingleTon instance ;  
 private SingleTon(){}  
 public static SingleTon synchronized getInstance(){  
     if(instance == null){  
            instance = new SingleTon();  
     }  
     return instance;  
 }  
 public static void main(String[] args) {  
     SingleTon instance1 = SingleTon.getInstance();  
     SingleTon instance2 = SingleTon.getInstance();  
     System.out.println(instance1 == instance2);  
            // 通过开启100个线程 比较是否是相同对象  
            for(int i=0;i<100;i++){  
                new Thread(()->  
                System.out.println(SingleTon.getInstance().hashCode())  
            ).start();  
        }  
    }  
}   

由于第三种方式出现了线程不安全的问题,所以对getInstance方法加了synchronized来保证多线程环境下的线程安全性问题,这种做法虽解决了多线程问题但是效率比较低。


因为锁住了整个方法,其他进入的现成都只能阻塞等待了,这样会造成很多无谓的等待。


于是可能有人会想到可不可以让锁的粒度更细一点,只锁住相关代码块可否?所以有了第五种写法。关注公众号Java技术栈回复多线程获取我整理的系列Java多线程教程。


5. 懒汉式线程不安全(synchronized代码块)

public class SingleTon{  
 private static  SingleTon instance ;  
 private SingleTon(){}  
 public static SingleTon getInstance(){  
     if(insatnce == null){  
         synchronied(SingleTon.class){  
                    instance = new SingleTon();  
         }  
     }  
     return instance;  
 }  
 public static void main(String[] args) {  
        SingleTon instance1 = SingleTon.getInstance();  
        SingleTon instance2 = SingleTon.getInstance();  
        System.out.println(instance1 == instance2);  
        // 通过开启100个线程 比较是否是相同对象  
        for(int i=0;i<100;i++){  
             new Thread(()->  
                System.out.println(SingleTon.getInstance().hashCode())  
            ).start();  
        }  
    }  
}   

当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完instance是null的时候,线程A就进入了if块并且持有了synchronized锁,但是同时另外一个线程t2在线程t1还未创造出实例之前,就又进行了instance是否为null的判断,这时instance依然为null,所以线程t2也会进入if块去创造实例,他会在synchronized代码外面阻塞等待,直到t1释放锁,这时问题就出来了,有两个线程都实例化了新的对象。

image.png



造成这个问题的原因就是线程进入了if块并且在等待synchronized锁的过程中有可能上一个线程已经创建了实例,所以进入synchronized代码块之后还需要在判断一次,于是有了下面这种双重检验锁的写法。


6. 懒汉式线程安全(双重检验加锁)

public class SingleTon{  
 private static  volatile SingleTon instance ;  
 private SingleTon(){}  
 public static SingleTon getInstance(){  
     if(instance == null){  
         synchronied(SingleTon.class){  
                    if(instance == null){  
                        instance = new SingleTon();  
                    }  
         }  
     }  
     return instance;  
 }  
 public static void main(String[] args) {  
        SingleTon instance1 = SingleTon.getInstance();  
        SingleTon instance2 = SingleTon.getInstance();  
        System.out.println(instance1 == instance2);  
        // 通过开启100个线程 比较是否是相同对象  
        for(int i=0;i<100;i++){  
             new Thread(()->  
                System.out.println(SingleTon.getInstance().hashCode())  
            ).start();  
        }  
    }  
}   

这种写法基本趋于完美了,但是可能需要对一下几点需要进行解释:


第一个判空(外层)的作用 ?


第二个判空(内层)的作用 ?


为什么变量修饰为volatile ?


第一个判空(外层)的作用


首先,思考一下可不可以去掉最外层的判断?答案是:可以


其实仔细观察之后会发现最外层的判断跟能否线程安全正确生成单例无关!!!


它的作用是避免每次进来都要加锁或者等待锁,有了同步代码块之外的判断之后省了很多事,当我们的单例类实例化一个单例之后其他后续的所有请求都没必要在进入同步代码块继续往下执行了,直接返回我们曾生成的实例即可,也就是实例还未创建时才进行同步,否则就直接返回,这样就节省了很多无谓的线程等待时间,所以最外的判断可以认为是对提升性能有帮助。


第二个判空(内层)的作用


假设我们去掉同步块中的是否为null的判断,有这样一种情况,A线程和B线程都在同步块外面判断了instance为null,结果t1线程首先获得了线程锁,进入了同步块,然后t1线程会创造一个实例,此时instance已经被赋予了实例,t1线程退出同步块,直接返回了第一个创造的实例,此时t2线程获得线程锁,也进入同步块,此时t1线程其实已经创造好了实例,t2线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以t2线程也会创造一个实例返回,此时就造成创造了多个实例的情况。


为什么变量修饰为volatile


因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。关注公众号Java技术栈回复JVM获取我整理的系列JVM教程。


首先要明白在JVM创建新的对象时,主要要经过三步。


1.分配内存


2.初始化构造器


3.将对象指向分配的内存的地址


因为仅仅一个new 新实例的操作就涉及三个子操作,所以生成对象的操作不是原子操作。


而实际情况是,JVM会对以上三个指令进行调优,其中有一项就是调整指令的执行顺序(该操作由JIT编译器来完成)。46张PPT弄懂JVM性能调优,这篇推荐看下。


所以,在指令被排序的情况下可能会出现问题,假如 2和3的步骤是相反的,先将分配好的内存地址指给instance,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为instance对象已经实例化了,直接返回一个引用。


如果这时还没进行构造器初始化并且这个线程使用了instance的话,则会出现线程会指向一个未初始化构造器的对象现象,从而发生错误。


7. 静态内部类的方式(基本完美了)

public class SingleTon{  
 public static SingleTon getInstance(){  
     return StaticSingleTon.instance;  
 }  
 private static class StaticSingleTon{  
            private static final SingleTon instance = new SingleTon();  
 }  
 public static void main(String[] args) {  
        SingleTon instance1 = SingleTon.getInstance();  
        SingleTon instance2 = SingleTon.getInstance();  
        System.out.println(instance1 == instance2);  
        // 通过开启100个线程 比较是否是相同对象  
        for(int i=0;i<100;i++){  
             new Thread(()->  
                System.out.println(SingleTon.getInstance().hashCode())  
            ).start();  
        }  
    }  
}  

因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。


另外由于静态变量只初始化一次,所以singleton仍然是单例的。


8. 枚举类型的单例模式(太完美以至于。。。)

public Enum SingleTon{  
    INSTANCE;  
    public static void main(String[] args) {  
         // 通过开启100个线程 比较是否是相同对象  
        for(int i=0;i<100;i++){  
            new Thread(()->  
                System.out.println(SingleTon.getInstance().hashCode())  
            ).start();  
        }  
    }  
}  

这种写法从语法上看来是完美的,他解决了上面7种写法都有的问题,就是我们可以通过反射可以生成新的实例。但是枚举的这种写法是无法通过反射来生成新的实例,因为枚举没有public构造方法


相关文章
|
12月前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
389 6
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
209 1
|
设计模式 安全 Java
Java面试题:什么是单例模式?如何在Java中实现单例模式?
Java面试题:什么是单例模式?如何在Java中实现单例模式?
217 0
|
设计模式 安全 Java
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
127 0
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
198 0
|
设计模式 安全 Java
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
181 0
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
160 0
|
设计模式 安全 NoSQL
Java面试题:结合单例模式与Java内存管理,设计一个线程安全的单例类?分析Java多线程工具类ExecutorService与Java并发工具包中的工具类,设计一个Java并发框架的分布式锁实现
Java面试题:结合单例模式与Java内存管理,设计一个线程安全的单例类?分析Java多线程工具类ExecutorService与Java并发工具包中的工具类,设计一个Java并发框架的分布式锁实现
223 0
|
设计模式 存储 缓存
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
141 0