Java实现单例模式

目录

一、简单了解一下设计模式

二、了解单例模式

     ①什么是单例模式

     ②饿汉式单例模式

       (1)什么是饿汉式单例模式?优点?缺点?

       (2)饿汉式单例模式,是如何确保类的对象唯一的?

       (3)代码实现饿汉式单例模式

       (4)饿汉式单例模式,是否线程安全?

        ③懒汉式单例模式

 (1)什么是懒汉式单例模式?

   (2)懒汉式是否线程安全?懒汉式单例模式如何确保线程安全?

   (3)懒汉式单例模式的优点?缺点?


一、简单了解一下设计模式

       设计模式,本质上,就是有点类似于一个"棋谱“。回顾一下棋谱,其实就是一些大佬们针对一些常见的对局场景,比如象棋里面的各种"将君"措施,例如铁门栓,闷龚等等。

       在计算机的圈子当中,也有一些设计"棋谱"的大佬。他们针对一些特殊的业务场景,也设计了一些对应的解决方案。只要按照这个方案来写代码,就可以达到一定的目的。


二、了解单例模式

      ①什么是单例模式

       单例模式,指的是,在一些特定的场景当中,某一些类只能创建出一个实例(对象)。无论是单线程还是多线程环境下面,都只能生成一个对象,这个时候,就需要使用到单例模式。

       通过Java语言特有的语法,达成了某个类只能拥有一个对象。这种确保一个类只能拥有一个对象的设计模式,就是单例模式。

        举一个使用单例模式的例子:数据库链接对象Connection。

        因为数据库连接对象,含义就是通过这些对象来操作数据库,如果创建出多个,一是会造成资源的浪费,二是这样没有意义


     ②饿汉式单例模式

       (1)什么是饿汉式单例模式?优点?缺点?

       饿汉式单例模式,指的是,在类加载的时候,就吧这个类的唯一实例创建出来了的设计模式。这个对象产生的时间位于类的实例创建之前。

        饿汉式单例模式的特点是,类对象的创建特别快速,因为类的加载是一个比较靠前的阶段。

这也是饿汉模式的优点。

       饿汉模式的缺点是:如果类的对象被创建之后,长时间没有使用,那么这个对象就有可能被gc回收,从而造成浪费。


       (2)饿汉式单例模式,是如何确保类的对象唯一的?

       ①在饿汉式单例模式当中,把类的对象使用static关键字修饰,并且作为类的属性。使用static关键字修饰的成员变量,都属于类,高于对象。同时,使用static关键字修饰的属性,属于类对象(class对象)。通过类名.class获取到的对象。因为类对象也正好是单例的,因此对应的SINGLETON属性也是唯一的。

 private static Singleton SINGLETON =new Singleton();

      构造方法被设为private,这样可以确保无法在类的外部创建对象。


       (3)代码实现饿汉式单例模式

class Singleton{
    /**先把当前实例创建出来
     * 这个和属性无关,而是和类相关
     * java代码当中的每个类,都会在编译之后得到.class文件
     * jvm加载这个类的时候
     * 类加载的时候,就把对象创建出来了,无论是否需要使用到当前对象。
     */
    private static Singleton SINGLETON =new Singleton();

    /**
     * 如果想获取这个实例对象,只能通过这个
     * 接口来获取
     * 单例模式对象@return
     */
    public static Singleton getInstance(){
        return SINGLETON;
    }
    /**
     * 无法在类的外部创建实例
     */
    private Singleton() {

    }
}

       (4)饿汉式单例模式,是否线程安全?

        饿汉式是线程安全的,因为当多个线程同时调用getInstance()方法的时候,都是读取的操作,并没有在getInstance()方法内部进行一系列的修改操作。之前文章当中,提到过,如果仅仅是读操作,不涉及修改变量的操作,这就是线程安全的.


        ③懒汉式单例模式

           (1)什么是懒汉式单例模式?

       懒汉模式,指的是在类加载的阶段,并不会立刻创建这个类的唯一对象。当需要使用到这个类的对象时候,才会创建这个对象的设计模式。

          代码实现:     Java实现单例模式_第1张图片


   (2)懒汉式是否线程安全?懒汉式单例模式如何确保线程安全?

       其实上面这样的写法,不是线程安全的。原因:当有两个以上线程同时调用getInstznce()方法,并且这个singletonLazy对象还没有被创建出来的时候,有可能两个线程同时都进入了if(singletonLazy==null)这个语句当中,这样,也就创建了两个对象。违背了单例模式的特点。

    Java实现单例模式_第2张图片


那如何让这种延迟加载的懒汉式变为线程安全的呢?

       那就是加锁,让其中一个线程(假如是thread1)调用getInstance()方法的时候,另外的线程(thread2)需要阻塞等待,直到获取到锁的线程创建完对象之后,解锁了,另外一个线程才可以继续调用getInstance()方法,来获取这个类的实例。

       此时,thread2获取到的实例,就是第一个线程已经创建好的了。

       这样,才算是线程安全的单例模式。


       代码实现:优化1:

  Java实现单例模式_第3张图片


       这样的写法,虽然线程安全了,但是效率却非常的低下,原因是:当这个对象被创建出来之后,其他线程如果想要获取这个对象,仍然会发生阻塞等待的现象。但是,如果仅仅是读取这个对象,然后返回,由于仅仅涉及"读取”操作,因此没必要针对“读“操作加锁。

      前面的文章提到过,如果多个线程仅仅是针对内存当中的某一变量进行“读”操作,是不会存在线程安全问题的。因此,没必要针对“读”的操作频繁加锁,这样会导致锁的粒度过大。


        代码实现:优化2:双重if减小锁的粒度

       因此,可以考虑:如果对象没有被创建,即:singletonLazy==null的时候,才需要加锁创建对象。如果singletonLazy!=null的时候,直接返回即可。

class SingletonLazy{
    /**
     * 此处不着急创建属性实例
     */
    private static SingletonLazy singletonLazy=null;

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

    }
}

        图解一下上面的饿汉式单例模式:

      需要注意的是,外层的if操作,判断的是是否需要进行加锁操作,内部的if语句,是判断是否需要创建对象。外层的if,为了避免在对象创建对象之后,其他获取此实例的线程都进入阻塞状态

      Java实现单例模式_第4张图片


代码优化3:

但是,可以看到一个警告:

Java实现单例模式_第5张图片

  在代码优化2当中,看似好像没有任何问题了,但是,其实还是会存在指令重排序,导致的问题;

在上述代码的这一行代码当中:

Java实现单例模式_第6张图片

可以看到,是一个对象初始化的语句,但是,这个语句在编译器底层的实现,其实是分为3个步骤的: 

 ①memory=allocate()   //为需要初始化的singletonLazy对象申请一块内存空间;

 ②ctorInstance(memory) //初始化这个对象

 ③singletonLazy=memory//设置singletonLazy引用指向刚刚申请的内存空间->memory


  但是,此时假如发生了编译器优化,

singletonLazy = new SingletonLazy();

上面的操作就会变成这样的:

 ①memory=allocate()   //为需要初始化的singletonLazy对象申请一块内存空间;     ③singletonLazy=memory//设置singletonLazy引用指向刚刚申请的内存空间->memory

 ②ctorInstance(memory) //初始化这个对象

 这样,会发生什么问题呢?我们来图解一下:

时间轴 线程A 线程B

t1

进入到内层if语句,并且执行了①操作:为对象申请内存空间
t2 设置引用指向的地址空间(执行③操作)
t3 刚刚好进入到外层的if语句,进行判断:singletonLazy==null?
t4 因为线程A已经为对象申请了一块内存空间了,因此判断得到singletonLazy!=null,直接返回singletonLazy引用
t5 执行②操作,初始化对象
t6 返回instance引用

       可以看到,在t4时刻,线程B获取到了一个占了内存空间,但是没有被初始化的对象。这一切的原因,就是编译器对①②③指令进行了重排序,变为了①③②。

       因此,为了避免指令重排序,需要对singletonLazy属性使用volatile关键字修饰,避免编译器对指令进行重排序。


代码实现:

Java实现单例模式_第7张图片

  这样优化过之后的代码,避免了指令重排序,就会变成如下的图解:

时间轴 线程A 线程B
t1 进入外层if语句
t2 进入同步代码块(加锁)
t3 执行①,为对象申请内存空间
t4 执行②,初始化对象
t5 由于singletonlazy对象还没有被放入被申请的空间,因此singletonLazy==null,进入外层if语句
t6 遇到了同步代码块,但是线程A还没有解锁。因此线程B阻塞等待
t7 执行③操作:把singletonLazy引用指向对象的内存空间
t8 解锁 进入同步代码块,但是遇到了内层的if语句,由于此时线程A已经执行完③操作了,因此直接返回线程A创建的对象,确保了单例
t9 返回

 (3)懒汉式单例模式的优点?缺点?

          优点:真正需要使用某个类的实例的时候,才会创建,这样不会造成资源的浪费。但是,因为使用了synchronized关键字来修饰代码块,有可能造成线程的阻塞,因此,缺点就是效率低下。

      

     

         

            

      

    

你可能感兴趣的:(java,单例模式,开发语言)