Java设计模式-单例模式笔记

目录

        • 1 单例模式的本质&优缺点&应用场景
            • 1.1 本质
            • 1.2 优点
            • 1.3 常见应用场景
            • 1.4 缺点
        • 2 三要素
        • 3 单线程环境实现
            • 3.1 饿汉式(线程安全)(立即加载)
            • 3.2 懒汉式(非线程安全)(延迟加载)
        • 4 在“多线程环境+线程安全”的要求下实现懒汉式单例
            • 4.1 synchronized方法
            • 4.2 synchronized块
            • 4.3 静态内部类
            • 4.4 双重检查模式
            • 4.5 ThreadLocal
        • 5 注意事项
            • 5.1 关于反射
            • 5.2 new or null
        • 6 参考资料

1 单例模式的本质&优缺点&应用场景
1.1 本质

单例对象的类必须保证只有一个实例存在。许多时候,整个系统只需要拥有一个的全局对象

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。事实上,这些应用都或多或少具有资源管理器的功能

例如,每台计算机可以有若干个打印机,但只能有一个 Printer Spooler(单例) ,以避免两个打印作业同时输出到打印机中。【类似临界区?】
比如,每台计算机可以有若干通信端口,系统应当集中 (单例)管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。

单例模式就是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种方法

1.2 优点
  1. 在内存中只有一个对象,节省内存空间;
  2. 避免频繁的创建销毁对象,可以提高性能;
  3. 避免对共享资源的多重占用,简化访问;
  4. 为整个系统提供一个全局访问点。
1.3 常见应用场景
  1. 有状态的工具类对象;
  2. 频繁访问数据库或文件的对象;
  3. 需要生成唯一序列的环境
  4. 需要频繁实例化然后销毁的对象。
  5. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  6. 方便资源相互通信的环境
1.4 缺点
  1. 不适用于变化频繁的对象;
  2. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
  3. 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
2 三要素
  1. 私有的构造方法;
  2. 指向自己实例的私有静态引用;
  3. 以自己实例为返回值的静态的公有方法。
3 单线程环境实现

立即加载 : 在类加载初始化的时候就主动创建实例;
延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。

3.1 饿汉式(线程安全)(立即加载)

单例在单例类被加载时候,就实例化一个对象并交给自己的引用

    // 饿汉式单例
	public class Singleton1 {
	 
	    // 指向自己实例的私有静态引用,主动创建
		
	    private static Singleton1 singleton1 = new Singleton1();
	 
	    // 私有的构造方法:因为一个类只能构造一个对象,因此不能随便new,得设成私有保证安全
	    private Singleton1(){}
	 
	    // 以自己实例为返回值的静态的公有方法,静态工厂方法
	    public static Singleton1 getSingleton1(){
	        return singleton1;
	    }
	}

由于类加载的方式是按需加载,且只加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

3.2 懒汉式(非线程安全)(延迟加载)

单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。

// 懒汉式单例
	public class Singleton2 {
	 
	    // 指向自己实例的私有静态引用
	    private static Singleton2 singleton2;
	 
	    // 私有的构造方法
	    private Singleton2(){}
	 
	    // 以自己实例为返回值的静态的公有方法,静态工厂方法
	    public static Singleton2 getSingleton2(){
	        // 被动创建,在真正需要使用时才去创建
	        if (singleton2 == null) {
	            singleton2 = new Singleton2();
	        }
	        return singleton2;
	    }

总结:
1. 饿汉懒汉的区别就是,饿汉一开始直接new了后面return不需要判空,懒汉一开始没有主动new所以return之前才new
2. 从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些

4 在“多线程环境+线程安全”的要求下实现懒汉式单例

饿汉式单例天生就是线程安全的,可以直接用于多线程而不会出现问题;

而懒汉式,很可能多个线程同时进入 if (singleton2 == null) {…} 语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例。

4.1 synchronized方法
// 线程安全的懒汉式单例
	public class Singleton2 {
	 
	    private static Singleton2 singleton2;
	 
	    private Singleton2(){}
	 
	    // 使用synchronized修饰,临界资源的同步互斥访问,防止new操作多次执行
	    public static synchronized Singleton2 getSingleton2(){
	        if (singleton2 == null) {
	            singleton2 = new Singleton2();
	        }
	        return singleton2;
	    }
	}

与上面传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton2()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例。

缺点:运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗

4.2 synchronized块
// 线程安全的懒汉式单例
	public class Singleton2 {
	 
	    private static Singleton2 singleton2;
	 
	    private Singleton2(){}
	 
	 
	    public static Singleton2 getSingleton2(){
	        synchronized(Singleton2.class){  // 使用 synchronized 块,临界资源的同步互斥访问
	            if (singleton2 == null) { 
	                singleton2 = new Singleton2();
	            }
	        }
	        return singleton2;
	    }
	}

缺点:运行效率仍然比较低,事实上,和使用synchronized方法的版本相比,基本没有任何效率上的提高

4.3 静态内部类
// 线程安全的懒汉式单例
	public class Singleton5 {
	 
	    // 从外部无法访问的私有内部类,按需加载,用时加载,也就是延迟加载
	    private static class Holder {
	        private static Singleton5 singleton5 = new Singleton5();
	    }
	 
	    private Singleton5() {
	 
	    }
	 
	    public static Singleton5 getSingleton5() {
	        return Holder.singleton5;
	    }
	}

是调用getSingleton5()的时候才创建单例,而不是类被加载的时候,

优点:效率较高

4.4 双重检查模式
// 线程安全的懒汉式单例
	public class Singleton3 {
	 
	    //使用volatile关键字防止重排序(JVM编译器-指令重排),因为 new Instance()是一个非原子操作,可能导致指向一个不完整的实例
	    private static volatile Singleton3 singleton3;
	 
	    private Singleton3() {}
	 
	    public static Singleton3 getSingleton3() {
	        // Double-Check idiom
	        if (singleton3 == null) {
	            synchronized (Singleton3.class) {       // 1
	                // 只需在第一次创建实例时才同步
	                if (singleton3 == null) {       // 2
	                    singleton3 = new Singleton3();      // 3
	                }
	            }
	        }
	        return singleton3;
	    }
	}

解读:首先判断现在是否有实例,若没有,则将该class锁起来准备创建,只允许一个线程进入。对于线程,进入了要判断现在实例是否存在,如果不存在(说明这时候是我第一个拿到了锁,有资格创建)就创建,如果存在了说明我不是第一个拿到锁的线程,之前的线程创建了,我就不创建了。

优点:使用双重检测同步延迟加载,不但保证了单例,而且切实提高了程序运行效率

注意:必须使用volatile修饰单例的引用

我理解的原因:在某个线程的内存里还没改,但是主存已经改了
PS:volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。

真正原因:
因为new是非原子操作,包括“1 分配内存空间-2 初始化对象-3 singleton3指向刚刚分配的内存地址”,而这三步可能是无序的(指令重排序),有可能变成“1 分配内存空间-3 singleton3指向刚刚分配的内存地址-2 初始化对象”,所以有可能对象还没有初始化好就被指向了(即当成指向一个非null),此时别的线程认为他是非null的,就会得到一个未初始化的对象,导致错误。

而volatile关键字能阻止指令重排,保证指令的执行顺序

补充:volatile不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。

4.5 ThreadLocal
// 线程安全的懒汉式单例
	public class Singleton4 {
	 
	    // ThreadLocal 线程局部变量
	    private static ThreadLocal<Singleton4> threadLocal = new ThreadLocal<Singleton4>();
	
	    private static Singleton4 singleton4 = null;
	 
	    private Singleton4(){}
	 
	    public static Singleton4 getSingleton4(){
	        if (threadLocal.get() == null) {// 第一次检查:该线程是否第一次访问
	            createSingleton4();
	        }
	        return singleton4;
	    }
	 
	    public static void createSingleton4(){
	        synchronized (Singleton4.class) {
	            if (singleton4 == null) {// 第二次检查:该单例是否被创建
	                singleton4 = new Singleton4();   // 只执行一次
	            }
	        }
	        threadLocal.set(singleton4);// 将单例放入当前线程的局部变量中 
	    }
	}

是上一个方法的变体。将双重检测的第一层检测条件 if (instance == null) 转换为线程局部范围内的操作

ThreadLocal 也只是用作标识而已,用来标识每个线程是否已访问过:如果访问过,则不再需要走同步块,这样就提高了一定的效率。

5 注意事项
5.1 关于反射

所有单例模式的缺点:无法防止利用反射重复构建对象

利用反射构造单例的方法:

//获得构造器
	Constructor con = Singleton.class.getDeclaredConstructor();
	//设置为可访问
	con.setAccessible(true);
	//构造两个不同的对象
	Singleton singleton1 = (Singleton)con.newInstance();
	Singleton singleton2 = (Singleton)con.newInstance();
	//验证是否是不同对象
	System.out.println(singleton1.equals(singleton2));//false,是两个对象
第一步,获得单例类的构造器。
第二步,把构造器设置为可访问。
第三步,使用newInstance方法构造对象。

因此必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,否则将会实例化一个新对象。

那如何阻止反射?——用枚举实现单例,方法如下:

public enum SingletonEnum {
    INSTANCE;
}

因为用了enum语法糖,JVM会阻止反射获取枚举类的私有构造方法
但是枚举方法缺点在于,他不是懒加载,在枚举类被加载的时候就初始化了

5.2 new or null

private static Singleton1 singleton1 = new Singleton1() 还是 = null

6 参考资料

https://blog.csdn.net/czqqqqq/article/details/80451880

https://zhuanlan.zhihu.com/p/33102022?from_voters_page=true

https://www.cnblogs.com/xuwendong/p/9633985.html

你可能感兴趣的:(java,设计模式)