超凡回归带大家全面认识单例模式

Hello,大家好!我是老丑,祝大家新年快乐呀!别人在玩,那么我们就学习呗。今天给大家带来的是——单例模式

单例模式是什么?

单例,顾名思义:便是唯一实例。这个实例大家可以理解为创建出来的对象,也就是new出来的对象。

模式,大家可以理解为一种方式、方法、思想。

因此单例模式,就是让该类的对象单一化的一种方式。

单例模式的实现方案

单例模式的实现方案有5种,且听我慢慢道来。

饿汉式模式

饿汉式:假设你饿了很多天,突然有一个天使给你送来食物,想必你脑海中已经有画面了。

OK,其实饿汉式代表的是迫切的意思。

因此,饿汉式模式来实现单例模式,会很迫切的创建一个对象,并保证对象唯一。

实现代码

package cn.laochou.singleton.hungry;

public class Singleton {

    // 我们在本类中创建一个对象,除此之外没有提供方法进行创建对象。
    private static final Singleton singleton = new Singleton();

 // 私有化构造函数,只有本类内可以访问
    private Singleton() {}


    // 返回我们所创建的唯一函数
    public static Singleton getInstance() {
        return singleton;
    }

}

OK,好像确实可以满足我们的单例需求

但是,大家仔细想想,真的能满足吗?

大家在这里,一定得记得一个知识点就是反射。如果不了解反射的,可以通过Java基础之反射篇 这篇文章来做一个初步认识,但是我在反射这篇文章中,并没有说明反射可以通过私有构造函数来创建对象。在这里做一个补充,小伙伴们看好了。

package cn.laochou.singleton.hungry;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Crack {

    public static void main(String[] args) {
        Class<?> clazz = Singleton.class;
        try {
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            Singleton singleton = (Singleton) constructor.newInstance();
            System.out.println(singleton);
        } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}

大家如果看完反射的那篇文章的时候,就知道上面一段代码的含义了。大家运行的时候,其实会出现一个Exception。我们来看下这个异常

java.lang.IllegalAccessException: Class cn.laochou.singleton.hungry.Crack can not access a member of class cn.laochou.singleton.hungry.Singleton with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
at cn.laochou.singleton.hungry.Crack.main(Crack.java:12)

大家可以看到这一段话:Class cn.laochou.singleton.hungry.Crack can not access a member of class cn.laochou.singleton.hungry.Singleton with modifiers “private”

翻译过来就是无法访问,对于私有构造方法,我们需要让它变得可以访问。需要加入下面的一行代码。

constructor.setAccessible(true);

public class Crack {

    public static void main(String[] args) {
        Class<?> clazz = Singleton.class;
        try {
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            // 设置允许访问
            constructor.setAccessible(true);
            Singleton singleton = (Singleton) constructor.newInstance();
            System.out.println(singleton);
        } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

}
cn.laochou.singleton.hungry.Singleton@1b6d3586

OK,对象创建出来了。因此我们是有方法来破解我们的饿汉式单例模式

总结: 饿汉式单例模式并不能保证绝对的单例,毕竟可以通过反射来破解。但是饿汉式单例模式没有线程安全这个问题,但是饿汉式模式没有延迟实例化带来的节约资源的好处。

懒汉式模式

懒汉式模式就是与我们的饿汉式模式相反。懒汉式模式就是不到使用的时候就不创建。

实现代码

package cn.laochou.singleton.lazy;

public class Singleton {

    public static Singleton singleton = null;

    // 私有化构造函数
    private Singleton() {

    }

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

如果大家仔细阅读了饿汉式模式,其实也能知道我们懒汉式模式也不能保证绝对的单例(可以通过反射来破解)。除此之外,我们的懒汉式模式还有线程安全问题

这里给大家假设一个场景:

有两个线程,分别为线程A,线程B。

线程A 运行到 if(singleton == null) 的时候,条件满足,进入if结构之后。由于程序调度,这个时候并没有执行 singleton = new Singleton(); 这个时候程序调度给了线程B,那么线程B运行到 if(singleton == null)的时候,条件也满足,进入if结构之后。那么现在其实很清楚了,两个线程都会运行 singleton = new Singleton(); 这条语句。因此Singleton是会被new两次的,也就是创建两个对象。这里就不满足我们的唯一单例了。

如何解决可以通过锁进行解决,大家如果不了解锁。可以阅读这两篇 Java中的并发会有什么问题呢?如何解决呢? 和 干货满满的synchronized详解!!! 进行一个初步了解。后续持续更新

其实还有一个问题

singleton = new Singleton(); 这条语句,在Java里面看似是一条语句。这条语句其实是分为三步执行的:

  1. 为singleton 分配内存空间
  2. 初始化 singleton
  3. 将 singleton 指向分配的内存地址

一般来讲执行的顺序是 1->2->3。但是由于JVM具有指令重排的特性,执行顺序有可能变为1->3->2。在这里大家如果不知道指令重排,可以自行百度了解,我在这里简单的介绍下功效。因为你看上去的一条Java语句,但是对于汇编而言,是可能被翻译为多条语句,而且这些语句所执行的组件都是不同的,为了增加运行效率,采用了流水线的执行方式,但是在流水线执行的过程中,可能由于一些语句导致前后无法衔接,我们称之为中断,因此指令重排主要是最大程度上来减少这种中断。指令重排是建立在happens-before原则之上的。(大家可以自行了解,后续的推文可能会详解)。我们接着上面来讲,1->3->2的执行顺序,对于单线程环境是不存在任何问题的,但是呢对于多线程的环境下,也是存在问题的。

这里又给大家模拟一遍。

有两个线程,线程A,线程B。

假设线程A执行了1->3,但是没有进行初始化,但是此时线程B调用了getInstance()方法来获取实例,因为此时singleton对象已经不为空了,但是此时我们的singleton还没有初始化,从而出现问题。

如何解决我们可以通过使用volatile关键字来禁止JVM的指令重排,保证在多线程的环境下也能正常运行。

总结: 懒汉式模式的优势主要在于延迟实例化、节约资源、用时加载。但是简单的懒汉式模式存在线程安全问题。

懒汉式模式-线程安全

想让懒汉式模式变得线程安全的方式有很多种,在这里我们列举一个比较常用的方式——Double Check(双重校验)

实现代码

package cn.laochou.singleton.check;

public class Singleton {

    private static volatile Singleton singleton = null;
    
    // 私有化构造函数
    private Singleton() {}
    
    public static Singleton getInstance() {
        // double check
        if(singleton == null) {
            // 上锁
            synchronized (Singleton.class{
                if(singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    

}

TIP:不了解synchronized的,可以阅读 干货满满的synchronized详解!!!

如果大家仔细阅读了饿汉式模式,其实也能知道我们懒汉式模式-线程安全也不能保证绝对的单例(可以通过反射来破解)

不知道小伙伴会不会有这样的一个疑问——“第一个if不是判断了是不是为null,然后就上锁了,为什么上锁之后还需要进行一个判断呢”

Answer(答):这里还是给大家假设一个场景,两个线程,线程A,线程B。假设线程A执行完了 if(singleton == null) 条件满足,进入了if结构,但是这个时候由于线程调度的原因,线程A没有继续执行后面的语句,而是执行权交给了线程B,这个时候线程B执行 if(singleton == null) 很明显条件满足,因此也进入了if结构。所以如果synchronized结构里面没有进行判断,还是会创建两个实例对象。只是先后问题,因为synchronized可以保证该结构在同一时刻只能被某一个线程执行。因此是需要进行双重检测的

那么小伙伴可能又会提问了——“我直接方法上锁不香吗?”

Answer(答):香啊,超级香。但是你有没有考虑性能的问题呢,方法上锁,锁的粒度很大,但是有的时候反而还需要进行锁粗化。这个是要根据场景来定的,根据不同的场景选择不同的锁粒度,该细化就细化,该粗化就粗化。但是对于我们上面的代码,很明显锁粒度越小越好,因为在方法中只有一个上锁代码块。

如果细心的小伙伴,可能很快发现,我们在 singleton的静态变量用volatile进行修饰了。这样就可以避免JVM 的指令重排了。也就是解决了上面关于懒汉式模式后面的一个问题。

总结: 懒汉式模式-线程安全主要是解决了线程安全问题和指令重排问题

静态内部类

直接上代码

实现代码

package cn.laochou.singleton.inner;

public class Singleton {

    // 私有化构造函数
    private Singleton() {
        
    }
    
    // 静态内部类
    private static class InnerClass{
        private static final Singleton singleton = new Singleton();
    }
    
    public static Singleton getInstance() {
        return InnerClass.singleton;
    }

}

同样,静态内部类的方式也是无法保证唯一单例(可以通过反射来进行破解),但是静态内部类是线程安全的。实例化一次是通过JVM来保证的,因为是在InnerClass类加载的时候才会进行创建singleton实例,而且静态内部类实现的单例模式也是即用即加载,这里其实还是有一个学问的,就是InnerClass是什么时候进行加载的。

我们写个demo简单的看下

package cn.laochou.singleton.inner.demo;

public class InnerClassLoader {
    
    private static class InnerClass {
        static {
            System.out.println("InnerClass is loading");
        }
        public static void hello() {
            System.out.println("hello");
        }
    }

    public static void show() {
        InnerClass.hello();
    }

    static {
        System.out.println("InnerClassLoader is loading");
    }

    public static void main(String[] args) {
        show();
    }

}

运行效果

InnerClassLoader is loading
InnerClass is loading
hello

从结果上来,加载InnerClassLoader类的时候,并没有加载 InnerClass。而是在调用InnerClass.hello()方法的时候,进行加载的。所以静态内部类是即用即加载。因此使用静态内部类实现单例模式的实例是即用即创建。

具体的类加载会在JVM系列细讲,在这里就简单的一笔带过,大家留个印象。

枚举

大Boss,也是 《Effective java》 极力推荐实现单例模式的方法。

实现代码

package cn.laochou.singleton.enums;


public enum  Singleton {

    INSTANCE;
    
    public void hello() {
        System.out.println("hello");
    }

}

上面就已经实现了单例模式的思想。

通过枚举实现单例模式,可以防止反射破解。而其他的实现方式都是可以通过反射来进行获取私有构造函数,并设置运行访问,即可创建多个对象。枚举方式是通过JVM来进行保证唯一实例,而且这种方式还是线程安全的。

单例模式的运用

看到这里,想必小伙伴们已经了解了单例模式的实现方案。我们学习单例模式,那么它具体用在哪里呢?

且听我细细道来

场景:

  • 资源共享的情况下,避免由于资源操作时导致性能问题或者损耗。比如ID构造器。
  • 控制资源的情况下,方便资源之间的管理。比如连接池,线程池。

单例模式实战

我们写一个简单的ID构造器吧,就是用这个组件来生成ID。

如果大家觉得ID构造器,没有作用的话,我就举个列子,假设你的项目业务很多,每个请求都会有一个唯一ID,这个ID不能重复,因为一旦很多重复的,一旦业务请求出现问题,完全无法排查。孰轻孰重,大家现在应该知道了吧。

实现代码

package cn.laochou.singleton.id;

import java.util.concurrent.atomic.AtomicInteger;

public enum IDGenerator {

    INSTANCE;

    private AtomicInteger atomicInteger;

    private IDGenerator() {
        this.atomicInteger = new AtomicInteger();
    }

    public int getId() {
        return atomicInteger.getAndIncrement();
    }

}

以上就是一个简单的ID生成器。大家可以看看。

我也把我的测试代码贴在这里

public static void main(String[] args) {
    // 在这里,我之前使用lambda表达式来写的,但是考虑lambda的推文还没完成,大家敬请期待。
    Runnable r = new Runnable() {
        @Override
        public void run() {
            while (true) {
                System.out.println(IDGenerator.INSTANCE.getId() + " " + Thread.currentThread().getName());
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    Thread thread1 = new Thread(r);
    Thread thread2 = new Thread(r);
    Thread thread3 = new Thread(r);
    Thread thread4 = new Thread(r);
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
}

个人测试是没有问题的哈,打印的数字的顺序肯定是不一致的。(大家看下ID是否唯一)

最后

OK,整个推文到这里就基本结束了。

希望小伙伴们看完推文,能够有所收获。

学习路上有FingerDance,不孤单。欢迎加群——进入公众号,菜单栏【关于我们】->【加入我们】!

觉得推文写得还不错的小伙伴,还请点赞、关注、转发支持下。

超凡回归带大家全面认识单例模式

我是老丑,一位又老又丑的少年!我们下期见。

往期推荐:

我们是FingerDance,欢迎加入我们!


原文始发于微信公众号(FingerDance):超凡回归带大家全面认识单例模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/26708.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!