单例模式,饿汉模式,懒汉模式

目录

1.1 解决什么问题

1.2 为什么可以解决

1.3 实现方式

1.3.1 饿汉模式(线程安全)

1.3.2 懒汉模式(线程不安全)

1.3.3 懒汉模式(线程安全)


       

    单例模式是设计模式的一种。

        设计模式就是一些大佬总结出的一些针对特定问题的解决方案,在这里简单学习两种设计模式--单例模式和工厂模式。有一本书《设计模式:可复用面向对象软件的基础》具体介绍了常用的23种设计模式(设计模式不止23种),如果想学习一些其他的设计模式的可以看看这本书。


1.1 解决什么问题

         通常用来解决资源共享的问题,比如windows中的资源管理器,这种情况下只要维护一个实例就好了。因为如果实例多了,那么多个进程去同步数据麻烦,而且有的时候共享的数据比较大,那么实例多了,占用的存储空间也多了。

          比如市图书馆(实例),管理了很多书(共有资源),有很多的人要来看书,有人想通过另一个实例(图书馆)来看书,那么就需要再建一个图书馆,并且同步所有的书,当两个图书馆中有一个增加一本书,那么另一个也要增加,有一个删除,另一个也要删除,因为维护的是公共资源。

        例如:jdbc编程中的DataSource实例只需要一个。

1.2 为什么可以解决

        单例模式只允许类创建唯一实例,提供一个public方法供外部获取唯一实例。这个多个进程操作的就是同一个实例了,能够共享数据,互不影响(实现线程安全之后)。

1.3 实现方式

    实现单例模式有很多方法,这里主要介绍两种实现方法。

1.3.1 饿汉模式(线程安全)

    单例模式只允许有一个实例,那么我们可以将唯一实例作为类的静态属性,静态属性只属于类是唯一的。但是此时,外部还是可以创建出实例,所以我们可以利用private来进行限制,让编译器来检查是否出现问题。如果在类外创建新的对象,编译器就会出现“红波浪线”来告诉程序员,此处不可以创建对象。

package thread;

public class Singleton {
    public static Singleton instance = new Singleton();
    private Singleton() {

    }
}

      还可以换一种写法,属性都是private,提供一个方法给外部获取唯一实例

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {

    }
    public Singleton getInstance() {
        return instance;
    }
}

      饿汉模式是线程安全的,在多个线程同时调用getInstance的时候,本质上是同时进行读操作,多个线程对同一个变量同时进行读操作是不会出现线程安全问题的。

1.3.2 懒汉模式(线程不安全)

        饿汉模式之所以是叫饿汉模式,是因为在类加载的时候对象就被加载出来了,创建类的时间太早了,太迫切了,就像一个饿死鬼,饭才煮好,烫的很,就去急吼吼干饭了。

        在饿汉模式中,唯一对象在类初始化的时候就被加载了。但是有的时候,可能没那么快就用到这个对象,那么这个对象就会有比较长的时间占用资源。过早的创建对象,显然是不合适的。

        于是在饿汉模式的基础上进一步优化,就有了懒汉模式,第一次用到实例的时候才创建对象。

package thread;

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

        当第一次调用getInstance方法的时候,实例还未创建,instance == null,进入判断条件开始创建实例返回对象。后面在调用的时候,实例已经存在,就进入不了判断条件了,就直接返回之前创建好的的实例。

         这样实例依旧是唯一的,创建实例的时机就不是程序驱动的时候了,而是第一次调用getInstance方法的时候。

        懒汉模式是线程不安全的,因为对于同一个变量有写操作也有读操作,比如:

         t1,t2调用前,instance == null,那么t1,先进入if条件,准备来创建实例,但是还没有创建实例,就被调度走了,这时候t2这边instance == null, 那么t2 也进入了if条件,也要创建新的实例。最后执行完毕,t1 和 t2所得到得实例是完全不同的,不满足单例模式下只有一个实例的原则。所以懒汉模式是线程不安全的。

        我们可以通过给代码加锁的方法来实现线程安全(不是随便加锁,需要根据代码结构合理的加锁)。懒汉模式(线程安全):

package thread;

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

    }
}

          此处通过加锁来将if和new打包成原子的操作,t1先获取到锁(调度是随机的,这里只是针对上面的图),进入if判断,后面虽然被调度出CPU了,但是t2拿不到锁,if判断不能进入CPU上调度,等t1 new 完对象之后释放锁,t2才可以竞争到锁,然后进入if判断,这个时候判断就是instance != null,然后释放锁,返回的就是t1创建好的实例。

            但是虽然线程安全了,但是还是有问题。只有第一次调用getInstance时,是线程不安全的,后续的话就是读操作,是线程安全的。但是每次读,进入if判断的时候都要去夺取锁。而加锁操作就有可能会阻塞,一旦阻塞了,什么时候解除阻塞,就得打个问号了。加锁操作虽然好用,但是用了就和“高性能”无关了。所以加锁不能随便加,在需要加锁的时候才加。

            所以我们可以对上面的代码加一个if判断,在该加的时候加锁,不该加的时候不加。

package thread;

public class SingletonLazy {
    private static SingletonLazy instance = null;
    public Object locker = new Object();
    public SingletonLazy getInstance() {
    // 如果instance 为null,就说明是首次调用,首次调用就有可能会产生线程安全问题需要加锁
    // 如果instance非null,那么就说明,后面都是读操作,不会产生线程安全问题,就不用加锁了
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) instance = new SingletonLazy();
            }
        }
            return instance;
    }
    private SingletonLazy() {

    }
}

但是上述代码还是有问题。

          编译器的优化方式,有一种叫指令重排序,就是调整原有代码的执行顺序,保证逻辑不变的前提下,提高程序的效率。

          比如:你要从家去书店买书,去超市买好吃的,去小吃街买饭

         你选择,先去买书,然后再去超市,然后再去小吃街,最后才回家。然后编译器就会给你有优化成,先去小吃街,然后再去书店,然后再去超市,最后再回家。 你该干都干了,只是顺序和你选择的不一样,你走的路更短了,效率更高了。

       有时这样确实能够提高效率,但有时,反而会产生bug。

       instance = new SingletonLazy(); 可以拆分成三个步骤(不是三个指令):

  1. 申请一段内存空间

  2. 在这个内存空间上调用构造方法,创建出这个实例

  3. 把这个内存地址赋值给instance引用变量

      正常情况下,上述代码是按照1,2,3的顺序来执行的,但是编译器可能会优化成1,3,2来执行,在单线程的情况下是没问题的,但是在多线程的情况下,就有可能出问题了。

        如图此时,t1调用getInstance时,instance == null,所以可以进入加锁,执行了1,3之后,instance非空,但是还未被初始化,就被调度走,此时t2判定instance!=null,就不竞争锁,直接返回未被初始化的instance,那么后续t2调用instance里的属性和方法,就很有可能出现bug。

       针对这种情况,我们可以使用关键字volatile,来禁止指令重排序。针对这个变量的读写操作是不可以被指令重排序的。

volatile一共有两个功能:

  1. 保证内存可见性,每次访问变量都必须重新读取内存,不会优化到缓存/寄存器中。

  2. 禁止指令重排序,针对这个变量的读和写操作的相关指令不可以被重排序。

1.3.3 懒汉模式(线程安全)

package thread;

public class SingletonLazy {
    private volatile static SingletonLazy instance = null;
    public Object locker = new Object();
    public SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) instance = new SingletonLazy();
            }
        }
            return instance;
    }
    private SingletonLazy() {

    }
}

        这个就是最后得到的线程安全的懒汉模式的代码,最后再盘一盘,这段代码主要有三个点:

        1. 第一次的 if(instance == null )   是因为只有instance == null时,会进入if条件里new 对象,会涉及到写操作,这时才有可能会产生线程安全问题,instance!=null时,则一定不会产生线程安全,因为只是读的操作。当对象创建之后,后续就不会有instance == null的情况了,instance != null的情况占了大部分情况,在这种可以不用加锁的情况下加锁,是很浪费资源的,而且加锁就代表了会产生锁竞争,就有可能会导致阻塞,大大降低效率。所以需要判断一下条件,只在一定要加锁的情况下加锁。

         2.第二次的if(instance == null ) ,这是进入加锁之后要执行的逻辑。因为可能有多个线程都进入到了竞争锁中,这个if可以保证,第一个竞争到锁的线程能进入这个条件完成第一次new对象,后续拿到锁的线程,能进行判断不去new新的对象。

         3. volatile ,这是告诉jvm对某个内存的操作不要进行指令重排序,因为new对象涉及了三个操作(ps,不是三个指令)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值