单例模式,居然还可以问出连环炮?

饿汉单例:


public class Singleton {

     private static Singleton singleton = new Singleton();
 
     private Singleton(){}
 
     public static Singleton getSingleton(){
         return singleton;
    }
}

懒汉单例:

public class Singleton {
 
   private static Singleton singleton;
 
   private Singleton(){}

   public static Singleton getSingleton(){
     
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

线程安全的懒汉单例:



public class Singleton {
 
    private static volatile Singleton singleton;
 
    private Singleton() {
    }
 
    public static Singleton getSingleton() {
     
        if (singleton == null) {
            synchronized (Singleton.class) {      
               
                if (singleton == null) {       
                    singleton = new Singleton();
                }
            }
        }
        return singleton3;
    }
}

问题一:为什么static修饰的对象可以作为单例?

这个问题,首先要看看java的运行时数据区内存分布

根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。

其中,方法区在JVM中是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

static成员变量只会初始化一次。

一个类的static成员变量只有“一份”(存储在方法区),无论该类创建了多少对象

因此,Singleton 中的singleton 变量是存放在方法区作为全局唯一实例的。

问题二:第一种写法是线程安全的吗?

是的,static修饰的变量是线程安全的

首先,我们先了解下类的加载过程。(通常来说,调用一个类的静态方法或者访问一个类的静态成员变量,就会开始加载类。)

 其中,重点来看一下初始化吧,

这个阶段主要是对类变量初始化,是执行类构造器的过程。

换句话说,只对static修饰的变量或语句进行初始化。

或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,可以简单理解为初始化静态变量和静态代码块吧

在《深入理解JAVA虚拟机》中,有以下解释:

 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类只会初始化一次

所以,对于第一种写法,不管有多少个线程同时执行Singleton.getSingleton(),都不会存在线程安全问题

问题三:能不使用双重检查写出线程安全的懒汉单例模式吗

可以使用静态内部类,既可以线程安全,又可以是唯一实例

public class SingleTon{
  private SingleTon(){}

  pubilc static int a = 100;
 
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
}

问题四:上面的写法为什么可以实现懒汉,如果我先调用了SingleTon.a,会导致INSTANCE也被初始化出来吗?

首先,我们先了解下类什么情况下会走加载流程的初始化步骤

1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
3.当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列,所以就算先调用了SingleTon.a导致了SingleTon类开始走加载流程,但是内部类SingleTonHoler也不会走自己的加载流程,因此INSTANCE是只有等到调用getInstance方法才会初始化示实例的,是因为触发了第一种情况的 getstatic 方法,即访问SingleTonHoler类的静态成员变量,这个时候才触发了 SingleTonHoler类的初始化,因而可以实现懒汉模式

这种方法有一个明显的缺点,就是不能传参,所以还是根据实际情况选择双重检查还是静态内部类吧

至于为什么是线程安全的,第二个问题已经分析过了,INSTANCE作为SingleTonHoler的静态变量,不管有多少个线程去调用getInstance,SingleTonHoler的初始化也只会走一遍,因而是线程安全的

问题五:为什么要用双重检查?为什么要是用volatile关键字?

再来看看线程安全的懒汉单例:

public class Singleton {
 
    private static volatile Singleton singleton;
 
    private Singleton() {
    }
 
    public static Singleton getSingleton() {
     
        if (singleton == null) {
            synchronized (Singleton.class) {      
               
                if (singleton == null) {       
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

先判断对象是否已经被初始化,再决定要不要加锁,其实可以把synchronized放在方法那里,但是这样会导致每个想拿对象的线程都要获取锁和释放锁,这样性能不够好。如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。不要最外层判断可以吗?其实不可以,去掉了就和在方法那里加锁是一样的了,那里面的判断可以去掉吗,也不可以,这个是用于防止多次创建对象的

至于为什么要是用volatile关键字,singleton = new Singleton();

这个步骤,其实在jvm里面的执行分为三步:

1.在堆内存开辟内存空间。
2.在堆内存中实例化SingleTon里面的各个参数。
3.把对象指向堆内存空间。

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,singleton 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL(Double Check Lock)失效问题。在JDK1.6及以后,可以使用volatile避免这种问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值