ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

JAVA设计模式之单例模式(超详细)

2020-04-20 12:06:54  阅读:228  来源: 互联网

标签:JAVA getInstance INSTANCE static 单例 线程 设计模式 public


单例模式有两种实现方式,一种是饿汉式,一种是懒汉式。

饿汉式:类加载到内存后,就实例化一个单例,JVM保证线程安全,简单实用,推荐使用!唯一缺点,不管用到与否,类装载时就完成实例化,也就是Class.forName("")加载到内存就会实例化。(不过话又说回来,你如果不用它,你要装载它干啥)。

懒汉式:类加载到内容后,不会实例化一个单例,而是在需要时才实例化,但是实现这个方式需要考虑到一些问题,下面我们来分析。

 

1、饿汉式

一、直接初始化

public class MgrTest01 {
    private static final MgrTest01 INSTANCE = new MgrTest01();

    private MgrTest01() {};

    public static MgrTest01 getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        MgrTest01 mgrTest011 = MgrTest01.getInstance();
        MgrTest01 mgrTest012 = MgrTest01.getInstance();

        System.out.println(mgrTest011 == mgrTest012);
    }
}

执行结果:true

二、使用静态语句初始化(本质上和直接初始化没有什么区别)

public class MgrTest02 {
    private static final MgrTest02 INSTANCE;

    static {
        INSTANCE = new MgrTest02();
    }

    private MgrTest02() {};

    public static MgrTest02 getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) {
        MgrTest02 mgrTest011 = MgrTest02.getInstance();
        MgrTest02 mgrTest012 = MgrTest02.getInstance();

        System.out.println(mgrTest011 == mgrTest012);
    }
}

结果:true

2、懒汉式

一、按需初始化

public class MgrTest03 {
    private static MgrTest03 INSTANCE;

    private MgrTest03() {};

    public static MgrTest03 getInstance()  {
        if(null == INSTANCE){
            try {
                Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new MgrTest03();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest03.getInstance())).start();
        }
        try {
            Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest03.getInstance())).start();
        }
    }
}

执行结果:获取的实例对象可能会不同(虽然达到了按需初始化的目的,但是却带来了线程不安全的问题)

问题原因:因为在对象还没有创建之前。多个线程同时调用getInstance方法获取实例的时候,可能存在第一个线程进入了if语句,但是还没有来的及执行实例化对象,后面线程也进入了if语句。等到第一个线程实例化之后,虽然这个时候再有线程调用getInstance,不会再进入if语句直接拿对象,但是已经进入if语句的线程又创建了新的对象。(注意:我们可以看到前五次的对象可能不是同一个,但是后五次肯定是同一个了(中间加入延迟是模拟在对象创建之后再调用getInstance的场景),所以这个问题是在对象还没有创建之前,然后有多个线程同时调用getInstance方法可能出现的问题

二、可以通过synchronized来解决上个问题

public class MgrTest04 {
    private static MgrTest04 INSTANCE;

    private MgrTest04() {};

    public static synchronized MgrTest04 getInstance()  {
        if(null == INSTANCE){
            try {
                Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new MgrTest04();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest04.getInstance())).start();
        }
        try {
            Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest04.getInstance())).start();
        }
    }
}

执行结果:获取的实例对象都是同一个(可以实现懒加载,并且不会有线程安全的问题,但是效率下降)

但是又引发了另一个问题,每次调用getInstance方法的时候都要加锁,因为调用加了synchronized的方法每次都要去判断有没有申请到这把锁,执行效率就降低了。本来我们只是解决在INSTANCE还没有实例化的时候线程安全问题,而INSTANCE初始化之后调用getInstance方法是不会有线程安全问题的,所以我们只需要在INSTANCE为空的时候才需要加锁获取,已经不为空了就没有必要还加锁获取。

 三、通过减小同步代码快的方式提高效率,但是不可行(需要注意)

public class MgrTest05 {
    private static MgrTest05 INSTANCE;

    private MgrTest05() {};

    public static MgrTest05 getInstance()  {
        if(null == INSTANCE){
            synchronized (MgrTest05.class){
                try {
                    Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new MgrTest05();
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest05.getInstance())).start();
        }
        try {
            Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest05.getInstance())).start();
        }
    }
}

执行结果:获取的实例对象可能会不同(虽然减少了同步代码块,但是出现了线程安全问题)

问题原因:和上面讲的懒加载第一种方式问题类似,虽然把实例化INSTANCE对象的代码同步了,但是还是有可能存在第一个线程进入了if语句,然后进入了同步代码块上锁了,但是还没有来的及执行实例化对象,后面线程也进入了if语句,只是被锁在实例化对象语句外面,等到第一个进入同步代码的线程出来后,被锁在外面的线程还是可以进入,然后实例化了新的对象,就出现了上面类似的线程安全问题。

四、通过双重检查来解决上一个问题

public class MgrTest06 {
    //做JIT优化的时候会指令重排 加上volatile关键之阻止编译时和运行时的指令重排
    private static volatile MgrTest06 INSTANCE;

    private MgrTest06() {};

    public static MgrTest06 getInstance()  {
        if(null == INSTANCE){
            synchronized (MgrTest06.class){
                //双重检查
                if(null == INSTANCE){
                    try {
                        Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new MgrTest06();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest06.getInstance())).start();
        }
        try {
            Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest06.getInstance())).start();
        }
    }
}

执行结果:获取的实例对象都是同一个(可以实现懒加载,并且不会有线程安全的问题,同时也解决了效率问题)

这样实现了只在INSTANCE为空的时候才需要加锁获取实例,已经不为空了再调用getInstance方法就判断不为空,然后直接获取。

五、静态内部类单例

public class MgrTest07 {
    private MgrTest07() {};

    private static class MgrTest07Holder{
        private static final MgrTest07 INSTANCE = new MgrTest07();
    }

    public static MgrTest07 getInstance(){
        return MgrTest07Holder.INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest07.getInstance())).start();
        }
        try {
            Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest07.getInstance())).start();
        }
    }
}

执行结果:(JVM保证单例,虚拟机加载类的时候只加载一次,所以INSTANCE也只会加载一次,同时实现了懒加载,因为加载外部类时不会加载内部类)

六、枚举单例(不仅可以解决线程同步,还可以防止反序列化)

public enum MgrTest08 {

    INSTANCE;

    public static void main(String[] args){
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest08.INSTANCE.hashCode())).start();
        }
        try {
            Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            new Thread(() -> System.out.println(MgrTest08.INSTANCE.hashCode())).start();
        }
    }
}

执行结果:

总结:一般使用直接初始化单例的和静态内部类单例的方式就可以了,不过使用枚举单例的方式更好,因为只有枚举单例,不仅可以解决线程安全问题,还可以防止反序列化,主要看实际开发中需不需要考虑到这些问题,来选择哪种方式实现单例就可以了。


序列化的问题:

为什么在做单例的时候要防止这一点?

因为java的反射是通过一个class文件,然后把整个class加载到内存,再把它创建一个实例出来,而除了枚举方式,其它的都可以找到class文件通过反序列化的方式(反射)再创建一个实例出来,如果想让它不能被反序列化需要设置一些变量,过程比较复杂。

枚举单例为什么可以防止反序列化?

因为枚举类没有构造方法(java规定没有构造方法),就算拿到class文件也没有办法实例化一个对象出来,它反序列化只是一个INSTANCE值(当前案例),然后根据这个值来找对象的话,找到的是和单例创建的同一个对象。

 

 

 

 

标签:JAVA,getInstance,INSTANCE,static,单例,线程,设计模式,public
来源: https://blog.csdn.net/zyt_java/article/details/105621236

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有