Java设计模式整理之单例模式

单例模式:顾名思义就是在程序运行期间,单例对象的类保证只有一个实例存在。
优点:1.实例控制:阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都
访问唯一实例。
2.灵活性:类自己控制了实例的过程,所以类可以更灵活的更改实例化过程。
3.提高性能:由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
4.安全性:避免对共享资源的多重占用。

缺点: 1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

实现方式:
实现方式主要分为两种:
一:懒汉模式:在全局的实例被第一次被使用时构建(分为线程不安全版本和线程安全版本);
二:饿汉模式:指全局的单例实例在类装载时构建。
日常我们使用的是懒汉模式,按需加载才能做到资源的利用的最大化。本文还有介绍到《Effective Java》一书中优化的实现方式。

一:懒汉模式
1.最简单的实现方式:

//懒汉式实现 v1
public class Single1 {
    private static Single1 instance;
    public static Single1 getInstance() {
        if (instance == null) {
            instance = new Single1();
        }
        return instance;
    }
}

2.为了防止外部类的误调用,将构造函数设置为私有:

   //懒汉式实现 v2
    public class Single1 {
        private static Single1 instance;
        private  Single1(){}
        public static Single1 getInstance() {
            if (instance == null) {
                instance = new Single1();
            }
            return instance;
        }
}

上面的实现方式有线程安全问题,当多线程工作时,有多个线程同时对instance == null进行判断为true时,会创建多个实例,这样一来就不是单例模式了。
对于上面的问题,有线程安全版本的懒汉模式实现方式
3.synchronized版本
对于getInstance()方法使用synchronized关键字修饰。

 //懒汉式实现 v3
        public class Single1 {
            private static Single1 instance;
            private  Single1(){}
            public static synchronized Single1 getInstance() {
                if (instance == null) {
                    instance = new Single1();
                }
                return instance;
            }
    }

但是用synchronized修饰后,同一时间只能有一个线程调用该方法,其他线程强制被等待,对于代码的执行效率有负面的影响。
4.为了解决上面的问题,出现了双重检查(Double-Check)的版本。

// Version 3 
public class Single3 {
    private static Single3 instance;
    private Single3() {}
    public static Single3 getInstance() {
        if (instance == null) {
            synchronized (Single3.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}

但是,对的还是有但是,但是instance = new Single3();这句话并非一个原子操作,他涉及三个步骤:
1.给 instance分配内存
2.调用 Single3 的构造函数来初始化成员变量,形成实例
3.将instance对象指向分配的内存空间(执行完这步 instance才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
所以有了第五个版本
5.volatile版本
只需要给instance的声明加上volatile关键字即可,Version5版本:

// Version 5
public class Single4 {
    private static volatile Single4 instance;
    private Single4() {}
    public static Single4 getInstance() {
        if (instance == null) {
            synchronized (Single4.class) {
                if (instance == null) {
                    instance = new Single4();
                }
            }
        }
        return instance;
    }
}

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不singleton = new
Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if
(instance == null))。

二:饿汉模式
由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。实现方式如下:

//饿汉式实现
public class SingleB {
    private static final SingleB INSTANCE = new SingleB();
    private SingleB() {}
    public static SingleB getInstance() {
        return INSTANCE;
    }
}

饿汉模式是看起来很完美的实现方式,但是还是存在缺点:
由于INSTANCE的初始化是在类加载时进行的,而类加载是由类加载器完成的,所以开发者对于他初始化的时机很难掌控。
1.初始化过早,造成资源的浪费;
2.初始化时有可能依赖其他数据,很难保证其他数据会在他初始化之前准备好。

什么条件会触发类被加载:
1.new一个对象时;
2.使用反射方式创建类实例时;
3.子类被加载时,如果父类没有被加载,就先加载父类;
4.jvm启动时执行的主类会被优先加载。

三. 《Effective Java》第一版推荐了一个中和写法:

 // Effective Java 第一版推荐写法
    public class Singleton {
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
        private Singleton (){}
        public static final Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }

这种写法非常巧妙:

对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。
四.《Effective Java》第二版中推荐了枚举写法:

 // Effective Java 第二版推荐写法
    public enum SingleInstance {
        INSTANCE;
        public void fun1() { 
            // do something
        }
    }
    
    // 使用
    SingleInstance.INSTANCE.fun1();

极简,且线程安全。

单例模式的适用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
1.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。

该文章参考编写:Hi,我们再来聊一聊Java的单例吧

猜你喜欢

转载自blog.csdn.net/varissss/article/details/82773176
今日推荐