JavaEE:单例模式(饿汉模式和懒汉模式)精讲

前言

什么是单例模式?

其实用通俗的话就是程序猿约定俗成的一些东西,就比如如果你继承了一个抽象类,你就要重写里面的抽象方法,如果你实现了一个接口,你就要重写里面的方法。如果不进行重写,那么编译器就会报错。这其实就是一个规范。

而单例模式能保证某个类在程序中只存在唯一的一个实例,而不会创建出多个实例

那么,单例模式又分成“饿汉”和“懒汉”两种、

一.饿汉模式 

顾名思义,饿汉模式就是在类加载的时候,创建实例。

package thread;
//期待这个类能有唯一实例
public class hungryDemo {
    private static hungryDemo instance = new hungryDemo();

    public static hungryDemo getInstance() {
        return instance;
    }

    //把构造方法设置为私有,这样在类外就无法 new 出这个对象的实例了
    private hungryDemo() {

    }
}

代码解读:

1. 首先创建了一个 hungryDemo 类,里面有一个类方法和一个类变量

2. 我们将构造方法设置为了private,那么在类外就无法再针对 hungryDemo 再实例化类了

JavaEE:单例模式(饿汉模式和懒汉模式)精讲_第1张图片


我们现在在类外,通过 hungryDemo提供的  public static hungryDemo getInstance 方法来进行调用,可以发现如下结果:

class Demo1 {
    public static void main(String[] args) {
        hungryDemo h1 = hungryDemo.getInstance();
        hungryDemo h2 = hungryDemo.getInstance();
        System.out.println(h1 == h2);

    }
}

运行结果:

JavaEE:单例模式(饿汉模式和懒汉模式)精讲_第2张图片

可以发现,两者获取到的类对象引用是一致的,那么单例模式的饿汉版本就创建好了。

二.懒汉模式 

单线程版本

我们的预期结果是不变的,那就是要实现单例模式,也就是这个类 只能被实例化一次!!!

那么懒汉模式顾名思义,也就是类加载的时候不创建实例,第一次使用的时候才创建实例。

那么我们可以写出以下代码:

package thread;

public class lazyDemo {
    private static lazyDemo instance = null;  
    public static lazyDemo getInstance() {
        /**
         * 只有调用该方法的时候,才创建对象
         */
                if (instance == null) {
                    instance = new lazyDemo();
                }               
        return instance;
    }
    private lazyDemo() {

    }
}

代码解读:

首先,设置类成员变量 instance 为 null,当第一次使用getInstance()的时候才进行创建对      象。

其次,跟饿汉模式一样,将类的构造方法设置为 private ,类外无法再次创建对象。

最后,在getInstance方法中判断 instance 是否为空,为空那就创建对象。为空说明已经        被调用一次了,那么就直接返回 instance 引用。

多线程版本 1

在以上的单线程版本中,我们不难发现以下问题:

假设现在有两个线程,他们是按照如下的顺序来执行的:

JavaEE:单例模式(饿汉模式和懒汉模式)精讲_第3张图片

那么此时的代码就会出现问题: t1 线程首先判断了 instance 是否为空,此时 t2 线程来运行了,也判断 instance 是否为空,紧接着 instance不为空,然后就创建了对象! 然后再回到 t1 线程中,又要进行创建对象。  此时问题已经很明显了,那就是 由于if代码块在多线程中的执行顺序问题导致的

更精简一下:

就是 instance = new lazyDemo() 是写操作, instance == null 是读操作,在多线程中,如果一段代码即涉及读操作,又设计写操作,那么就很容易出现问题!!!


解决办法:

  当一段代码是因为读写操作出BUG,我们首先想到的就是加锁。也就是在我写的时候,你不要       读。我读的时候,你不要写。

synchronized 是一种内置的 Java 关键字,它用于实现线程的同步。当一个线程进入synchronized块或方法时,它获得了锁,这会阻止其他线程同时进入相同的synchronized块或方法,从而确保了共享资源的互斥访问。

修改代码如下:

package thread;

public class lazyDemo {
    private static lazyDemo instance = null;  
    public static lazyDemo getInstance() {
        /**
         * 只有调用该方法的时候,才创建对象
         */
            synchronized (lazyDemo.class) {   //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次)
                if (instance == null) {
                    instance = new lazyDemo();
                }
            }
        return instance;
    }

    private lazyDemo() {

    }
} 

对于对象lazyDemo.class,实际上就是lazyDemo这个类,也就是对类进行加锁。

此时加锁之后,当t1线程进行读写操作的时候,t2线程再次进行访问就只能进行阻塞。

此时t1就可以放心创建出一个对象出来,此时t2再进行调用方法的时候,instance 不为空,就直接返回 t1 创建好的对象引用。 这时候就确保了只创建出一个实例。

多线程版本2

其实,多线程版本1 还是有问题的,我们发现:如果t1 线程加锁后创建好了对象,其他线程(t2,t3,t4.........)在进行访问的时候,首先就要进行加锁操作。 也就是每次访问都要进行加锁,这是一个资源开销非常大的操作。

深入探究一下,我们发现其他线程(t2,t3,t4.........)在进行访问的时候,只需要判断当前的对象是否被创建好了即可。如果被创建好了,那么就直接返回对象引用。如果没有被创建好,再进行加锁创建对象。

修改代码如下:

public class lazyDemo {
    private static lazyDemo instance = null; 
    public static lazyDemo getInstance() {
        /**
         * 只有调用该方法的时候,才创建对象
         */
        if(instance == null) {   //2. if判断解决的是多次加锁,加锁频率太高的问题
            synchronized (lazyDemo.class) {   //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次)
                if (instance == null) {
                    instance = new lazyDemo();
                }
            }
        }
        return instance;
    }

    private lazyDemo() {

    }
}

在多线程中,这两个 if 的作用大不相同!!!

修改后,我们发现如果 t1 线程创建好了对象, 此时其他线程(t2,t3,t4.........)在进行调用的时候,首先判断了instance 是否为空,不为空就说明已经创建好了对象~

多线程版本3

其实到现在,这个懒汉模式的单例代码还是有问题!!!

 在多线程下,要考虑到编译器的优化问题,当编译器没有按照我们的逻辑进行操作的时候,那么就会出现问题。

在此代码中,new 操作可以分为以下三步:

1.申请内存空间(一定先执行),获取到内存地址  

2.在内存空间上构造对象(构造方法)

3.把内存的地址,赋值给 instance 引用

在单线程环境下,执行那种顺序都无所谓,但是如果在多线程环境下,就可能出现问题:

假设是按照 1 3 2 的顺序来执行,当 1 和 3 操作执行完的时候,instance 已经非空了,只是内存空间上还没有构造对象 / 方法,此时instance 指向的是一个还没初始化的非法对象。 此时此刻 t2 进行访问,判断 instanc 是不为空的,然后就返回了一个还没初始化的非法对象,进一步 t2 线程就有可能访问 instance 里面的属性和方法。此时就出现了问题了。

这个问题就是指令重排序问题,解决办法就是让 instance 加入上volatile 关键字,此时就避免了指令重排序问题。

    //3.加 volatile是为了解决new 操作的指令重排序问题    
private volatile static lazyDemo instance = null; 

此时的代码就会严格按照 1  2  3 的顺序执行。


总结:单例模式是一个约定俗成的规范,保证一个类只能实例化一个对象。饿汉模式在多线程和单线程都没有问题,因为一开始它就创建好了对象。 而懒汉模式的多线程版本会出现以下三个问题:1. 线程安全问题( 确保只new 一次)2. 多次重复加锁的问题  3. 指令重排序问题。

希望以上的解决办法对你有所帮助!!!

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