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

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

饿汉式:类加载到内存后,就实例化一个单例,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值(当前案例),然后根据这个值来找对象的话,找到的是和单例创建的同一个对象。

发布了9 篇原创文章 · 获赞 10 · 访问量 5917

猜你喜欢

转载自blog.csdn.net/zyt_java/article/details/105621236