Java中单例模式不同写法详解(包含懒汉式、饿汉模式、双重检查模式、静态内部类)

前言:       

        在Java,单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在全局某个类只有一个实例存在。它适用于一个类的实例需要重复被利用,使用单例即可避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间。一些管理器和控制器常被设计成单例模式。常用的单例模式写法有以下几种:饿汉模式、懒汉式、双重检查模式、静态内部类等。

一、 饿汉模式

public class SingletonA{
    private static SingletonA instance = new SingletonA();
    private SingletonA(){}
    public static SingletonA getInstance(){
        return instance;
    }
}

说明:

(1)SingletonA类的构造函数定义为private,即说明SingletonA不能直接被实例化,提供了一个静态实例instance,调用者可以通过newInstance( )获取SingletonA类  的       唯一实例;

(2)饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在;

(3) 优点:饿汉模式在类加载的时候就创建了,保证了线程安全;

(4) 缺点:即使在程序中没有使用这个单例,它依然在类被加载后就创建了,浪费了内存

(5) 适用场景:该单例占用内存小、或者程序刚开始就要使用到该单例。

 

二、 懒汉模式

public class SingletonB{
    private static SingletonB instance = null;
    private SingletonB(){}
    public static SingletonB getInstance(){
        if(null == instance){
            instance = new SingletonB();
        }
        return instance;
    }
}

说明:

(1)SingletonB类的实例是在需要的时候才去创建的,如果单例已经之前已经创建了,再次调用newInstance获取将不会重新创建新的对象,而是直接返回之前创建的对象。

(2)优点:延迟加载,在需要的时候创建,不会在整个程序周期里都占用资源;

(3)缺点:懒汉模式是线程不安全的,因为可能会出现某个线程正在创建该实例,另一个线程调用它的getInstance()方法的情况,即可能出现创建多个实例的情况;

 (4)适用场景:该单例使用的次数少,创建单例时消耗的资源较多、程序没有在刚开始初始化的情况下就使用该单例。

(5)为解决线程安全问题,需要给getInstance()方法加锁解,如下:

public class SingletonB{
    private static SingletonB instance = null;
    private SingletonB(){}
    public static synchronized SingletonB getInstance(){
        if(null == instance){
            instance = new SingletonB();
        }
        return instance;
    }
}

 

三、双重校验锁

    public class SingletonC {
        private static SingletonC instance = null;
        private SingletonC(){}
        public static SingletonC getInstance() {
            if (instance == null) {   //  Checked
                synchronized (SingletonC.class) {
                    if (instance == null) { //  checked
                        instance = new SingletonC();
                    }
                }
            }
            return instance;
        }
    }

说明:

(1)给getInstance( ) 方法加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题。synchronized修饰的同步方法比一般方法要慢很多, 如果多次调用getInstance(),累积的性能损耗就比较大了。

(2)因此就有了双重校验锁,如图这种写法不是直接在给getInstance( ) 方法加锁,而是给里面的代码块加锁,所以对于getInstance( ) 方法,不需要每个访问的线程都等待getInstance( ) 方法的锁的释放,如果有线程创建了该单例,其他线程访问该方法,该方法直接将单例返回。提高了性能。

(3)为什么要双重校验(两次判断instance == null)?

        因为如果synchronized (SingletonC.class){}方法内部没有第二次判断instance == null,则有可能出现这种情况:A线程和B线程同时在判断第一个if (instance == null)为true,所以都进入了if语句内,A线程执行了synchronized (SingletonC.class){}内部的方法块,创建了一个单例对象。B在等待synchronized 锁的释放,等A线程执行完毕了单例对象的创建,释放了synchronized 锁,然后B线程也执行了synchronized (SingletonC.class){}内部的方法块,又创建了一个单例对象。此时就创建了两个单例对象了,为了解决这个问题,还需要在同步代码块中增加if (instance == null)判断语句。

 

注意:尽管双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,但依然还是有可能出现问题的,

        这里要提到java中的指令重排序问题:

先看以下代码:

int a = 1;    //语句(1)
int b = 2;    //语句(2)
a = a + 3;    //语句(3)
b = a*a;      //语句(4)

看到以上代码,你可能会认为代码语句的执行一定是:

语句(1)=> 语句(2)=> 语句(3)=>语句(4)

其实不然,代码语句的执行可能会是以下顺序:

语句(2)=> 语句(1)=> 语句(3)=>语句(4)

        指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

        比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。不过虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同()。

所以:java的指令重排序是不会出现以下顺序的:

语句(2)=> 语句(1)=> 语句(4)=>语句(3),因为处理器在进行重排序时是会考虑指令之间的数据依赖性。语句(4)的操作需要用到语句(3)的结果,故处理器会保证语句(4)在语句(3)后面执行。

虽然重排序不会影响单个线程内程序执行的结果,但是对多线程还是有影响的!

看以下代码:

线程1:

//线程1:
instance = initInstance ();    //语句1,初始化instance对象
inited = true;                 //语句2

线程2:

//线程2:
while(!inited ){
  sleep()
}
toDoSomeThing(instance);

        在线程1中,语句2和语句3的语句重排序是不会影响到线程1接下来的结果的,即在线程1中先执行了语句二,而后执行了语句一,不会影响线程1后面的执行结果,但是如果在线程1中执行了语句2后线程2开始执行,此时线程2拿到的inited值为true, 跳出while循环体开始执行toDoSomeThing(instance)方法,里面带的对象参数instance还没有初始化(即线程1中的语句1还没有开始执行),所以很明显线程2接下来的执行将会是错误的,由此可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性;

 

 

因此即使按照上面的方法使用了双重校验锁后,在多线程环境下由于java中的指令重排序依然可能会有问题

        问题可能会出现在这一句语句上:

instance = new SingletonC();

        对于这条语句实际上包含了三个操作:(1)分配对象的内存空间;(2)将该分配有内存空间的对象初始化;(3)使变量instance指向刚分配的内存地址。

即如下图:

         但是实际执行这一句语句的时候并不一定是按照(1) => (2) => (3) 的顺序,由命令的重排序后可能出现 (1) => (3) => (2) 的顺序,如果线程A执行了这一句:instance = new SingletonC() ,且底层重排序后顺序为(1) => (3) => (2) 的顺序, 在线程A执行到(3)的时候,线程B进行判断if(instance==null)时就会为false,而实际上这个instance并没有初始化成功,即线程B此时拿到的instance不为null,但是并没有被初始化。因此线程B之后的操作就会是错误的。

 

        为了解决这种可能出现的问题,可使用volatile修饰instance这个实例对象,这样可以禁止实例化的时候2和3操作重排序,从而避免这种情况。因为volatile包含禁止指令重排序的语义,所以才有了有序性。

       添加volatile(只能对变量使用)修饰后如下:

    public class SingletonC {
        private static volatile SingletonC instance = null;
        private SingletonC(){}
        public static SingletonC getInstance() {
            if (instance == null) {   //  Checked
                synchronized (SingletonC.class) {
                    if (instance == null) { //  checked
                        instance = new SingletonC();
                    }
                }
            }
            return instance;
        }
    }

       

 

四、静态内部类(推荐)

public class SingletonD{
    private static class SingletonHolder{
        public static SingletonD instance = new SingletonD();
    }
    private SingletonD(){}
    public static SingletonD getInstance(){
        return SingletonHolder.instance;
    }
}

说明:

(1)这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此也是线程安全的。但不一样的一点是,它是在内部类里面去创建对象实例;

(2)这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。通过这种方式可以同时保证延迟加载和线程安全;

(3)该方式推荐使用。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值