Java设计模式-创建者模式-单例模式-懒汉式

岳庆锦

发布于 2022.03.13 19:56 阅读 1602 评论 0

单例模式(Singleton Pattern)-懒汉式

 懒汉式:类加载的时候该类的对象并不会被创建,而是在首次使用该对象的时候(外界调用获取该单实例对象的getInstance()方法时)被创建。

  下面来介绍懒汉式的四种实现方式(线程不安全线程安全双重检查锁方式静态内部类方式

 

 

 1.懒汉式-方式1(线程不安全)

 需要注意的点

 1.对象的真正创建在getInstance()方法里实现(创建一个Singleton类型的对象,然后赋值给成员变量instance)。

 2.给instance赋值时要对instance是否为null进行判断。(若不加判断,则每次调用getInstance()方法都会创建一个新的对象,多次获取到的就不是同一个对象)

 具体代码

1.1单例类(Singleton)

public class Singleton {

    //私有构造方法
    private Singleton(){};

    //声明Singleton类型的变量
    private static Singleton instance;//此时还没有对instance进行赋值,instance为null

    //对外提供访问方式
    public static synchronized Singleton getInstance(){
        //判断instance是否为null,如果为null,说明还没创建Singleton类型的对象
        //如果没有,创建一个并返回;如果有,直接返回
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

1.2测试类(Client)

public class Client {
    public static void main(String[] args) {

        Singleton instance1 = Singleton.getInstance();

        Singleton instance2 = Singleton.getInstance();

        System.out.println(instance1 == instance2);//true
    }
}

1.3运行结果(结果为true,说明两次获取到的是同一对象,实现了单例)

 结果分析:上面的代码看似没有问题,实际还是存在问题的。举个栗子,现在假设是多线程,两个线程同时调用getInstance()方法,线程1拿到CPU的执行权,执行getInstance()方法。线程1进入方法后,先进行判断instance是否为null(此时为null),然后线程一进行等待,因为这个时候CPU的执行权被线程2拿走了,然后线程2进来进行判断(此时instance还为null,因为线程1进行完判断后就等待了,没有对instance进行赋值)。这样,我们获取到的就不是单个对象了。所以,如果有多个线程进来卡到刚判断完的位置,就会获取到多个对象。

 改进方法:给getInstance()方法加上同步的关键字(synchronized)(也就是线程安全的方式

 

 

 2.懒汉式-方式2(线程安全)

 因为线程安全这种方式不创建多线程是演示不出来的,但创建了多线程也不一定能演示出来,所以我们这里就只进行分析。

 分析:线程1拿到CPU的执行权,然后进行判断,判断完后进行等待。假设此时CPU的执行权被线程2获取到了,但线程2进入不了方法,因为这里面有同步锁,线程1在里面等待,还没有释放这个同步锁,线程2就进不来,只能在外面等着,直到线程1执行完。线程1执行完就意味着instance已经被赋值了(不为null了),这样后来的线程进入方法进行判断的结果就为false,就直接返回instance,这就解决了上述多线程获取到的可能不是同一对象的问题。

2.1单例类(Singleton)

public class Singleton {

    //私有构造方法
    private Singleton(){};

    //声明Singleton类型的变量
    private static Singleton instance;

    //对外提供访问方式
    /*添加同步锁后,线程1等待时,就算线程2拿到cpu执行权也进入不到方法体,
    线程1还在等待没有释放同步锁,线程2就进不来,只能等线程1执行完(也就是运行完这个方法)*/
    public static synchronized Singleton getInstance(){
        //判断instance是否为null
        //为null,创建一个并返回,不为null,直接返回
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

2.2测试类(Client)的代码和运行结果同方式1(线程不安全)一样,这里就不重复了。

 线程安全和线程不安全在代码上的唯一区别:线程安全给getInstan()方法加了同步锁(synchronized)。

 线程安全存在的缺点对于getInstance()方法来说,绝大部分操作都是读操作,读操作是线程安全的,每次都需要持锁才能进入方法的话,会导致性能的低下。

 通过分析getInstance()方法理解读操作

public static synchronized Singleton getInstance(){
        
        if(instance == null){
            
            instance = new Singleton();
        }
        return instance;
    }

 第一次调用getInstance()方法会首先判断instance是否为null,此时instance肯定为null,所以会创建Singleton对象赋给instance,这里赋值的操作,称为写操作赋完值之后就把instance返回。以后每次调用getInstance()方法,都是直接返回instance对象,因为instance对象已经创建成功了,判断的结果就是false,而后面访问getInstance()方法,直接把instance返回的操作,叫做读操作

 所以,没有必要让每个线程必须持有锁才能调用该方法,只需调整加锁的时机。(引出双重检查锁方式,“双重检查”也就是“两次检查”)

 

 

 3.懒汉式-方式3(双重检查锁方式)

 两次检查第一次判断:判断instance等不等于null,如果不等于null,不需要抢占锁,直接返回instance就可以了,即使后面调用getInstance()方法,都是直接返回的,并没有持有锁,所以它的效率就可以提升。如果instance等于null,进入判断后要先持一把锁(设置同步代码块,锁对象就是当前类的字节码对象(Singleton.class)),在同步代码块中进行第二次判断,第二次判断也是判断instance是否为null,如果为null,就创建Singleton类型的对象,然后赋值给instance;如果不为null,直接返回对象(instance)。

 代码如下

3.1单例类(Singleton)

​public class Singleton {

    //私有构造方法
    private Singleton(){};

    //声明Singleton类型的变量
    private static Singleton instance;

    //对外提供公共访问方法
    public static Singleton getInstance(){
        //第一次判断,如果instance的值不为null,不需要抢占锁,直接返回对象
        if(instance == null){
            //同步代码块
            synchronized (Singleton.class/*当前类的字节码对象*/){
                //第二次判断,也是:true创建对象,false直接返回
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
​

3.2测试类(Client)和运行结果同上,这里也不重复了。

 单例类代码分析:双重检查锁方式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。上面的双重检查锁模式看上去完美无缺,其实是存在问题的,在多线程的情况下可能会出现空指针问题,出现问题的原因是,JVM在实例化对象的时候会进行优化和指令重排序操作。(不太明白)

 双重检查锁空指针异常问题解决方案:添加volatile关键字,volatile关键字可以保证可见性和有序性。(在此处体现的就是有序性

 改进后的单例类代码:(将volatile关键字放在instance上面)

public class Singleton {

    //私有构造方法
    private Singleton(){};

    //声明Singleton类型的变量
    private static volatile Singleton instance;//添加volatile关键字,保证指令是有序的

    //对外提供公共访问方法
    public static Singleton getInstance(){
        //第一次判断,如果instance的值不为null,不需要抢占锁,直接返回对象
        if(instance == null){
            //同步代码块
            synchronized (Singleton.class/*当前类的字节码对象*/){
                //第二次判断,也是:true创建对象,false直接返回
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

 添加volatile关键字后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程情况下线程安全也不会有性能问题。推荐使用双重检查锁方式)

 

 

 4.懒汉式-方式4(静态内部类方式)

 静态内部类单例模式中,实例由内部类创建

 由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载(外界调用getInstance()方法),并初始化其静态属性(这里的静态属性其实就是我们创建的外部类的对象),静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序(也就是解决了指令重排序的问题)。

 内部类 Java语言允许在类中再定义类,这种在其它类(在这里指的就是单例类Singleton)内部定义的类就叫内部类 。

4.1单例类(Singleton)

public class Singleton {

    //私有构造方法
    private Singleton(){};

    //定义一个静态内部类
    private static class SingletonHolder{
        //在内部类中声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
        //加final是为了防止外界对它进行修改,此时instance就是一个常量,常量命名规范就是字母全部大写
        //字母全部大写快捷键:ctrl+shift+u
    }

    //提供公共的访问方式
    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

4.2测试类(Client)和运行结果同上,不再重复。

 静态内部类方式总结创建外部类的对象,是在内部类里声明变量进行初始化

 因为是静态内部类,我们在getInstance()方法里面通过静态内部类的类名调用其静态属性的话,就会加载静态内部类并且初始化外部类对象,而且只会初始化一次。那么多个线程调用,获取到的都是同一个对象。

 静态内部类方式优点:在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

 

 在考虑内存空间浪费的情况下,推荐使用懒汉式-3(双重检查锁方式)和4(静态内部类方式)。