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

Posted Java技术栈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了8 种单例模式写法,助你搞定面试!相关的知识,希望对你有一定的参考价值。

Java技术栈

www.javastack.cn

关注阅读更多优质文章


来源:小小木的博客

www.cnblogs.com/wyc1994666/p/11394755.html


1. 单例模式常见问题

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);
    }

}

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

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;
}

执行结果如下

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

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

造成线程不安全的原因:

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

8 种单例模式写法,助你搞定面试!
为了解决这个问题,我们可以采取加锁措施,所以有了下面这种写法

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();
        }
        
    }

}

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释放锁,这时问题就出来了,有两个线程都实例化了新的对象。

造成这个问题的原因就是线程进入了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线程也会创造一个实例返回,此时就造成创造了多个实例的情况。

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

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

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构造方法。

最近热文:
1、
2、
3、
4、
5、
6、
7、
8、
9、
10、
扫码关注 Java技术栈 公众号阅读更多干货。

点击「」获取面试题大全~

以上是关于8 种单例模式写法,助你搞定面试!的主要内容,如果未能解决你的问题,请参考以下文章

详解单例模式8种写法-一站式搞定面试官

8种单例模式的实现

常见的8种单例模式

四种单例写法与测试

两种单例模式的写法

常见的几种单例模式写法