设计模式中的单例设计模式

一个类仅有一个实例,由自己创建并对外提供一个实例获取的入口,外部类可以通过这个入口直接获取该实例对象。

单例模式

简述:一个类仅有一个实例,由自己创建并对外提供一个实例获取的入口,外部类可以通过这个入口直接获取该实例对象。

场景:很多时候整个应用只能够提供一个全局的对象,为了保证唯一性,这个全局的对象的引用不能再次被更改。比如在某个应用程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例类统一读取并实例化到全局仅有的唯一对象中,然后应用中的其他对象再通过这个单例对象获取这些配置信息。

如:Spring容器中的对象、Windows任务管理器、垃圾回收站、打印机打印

特点描述特点值
常用程度非常常用
适用层次代码级、应用级
引入时机程序编码,代码级
结构复杂度简单
实现封装对象产生的个数
体现原则

实现思路

  1. 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例

    class Person{
    	private Person() {
    		//此类外部无法使用new关键字进行实例化
    	}
    }
    
  2. 在该类内提供一个静态方法,当我们调用这个方法时,返回该类实例对象的引用。

    public static Person getInstance() {
        return person;
    }
    

饿汉式写法{#gof201}

/**
 * 饿汉式单例写法
 * 
 * 带你轻松学Java
 * www.it235.com
 *
 */
public class Singleton1 {
    // 私有构造
    private Singleton1() {}

    private static Singleton1 single = new Singleton1();

    // 静态工厂方法
    public static Singleton1 getInstance() {
        return single;
    }
}

饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。

缺点:Java反射机制支持访问private属性,所以可通过反射来破解构该造方法,产生多个实例

懒汉式写法{#gof202}

/**
 * 懒汉式写法
 * 
 * 带你轻松学Java
 * www.it235.com
 *
 */
public class Singleton2 {

    // 私有构造
    private Singleton2() {}

    private static Singleton2 single = null;

    public static Singleton2 getInstance() {
        if(single == null){
            single = new Singleton2();
        }
        return single;
    }
}

懒汉式属于延迟加载范畴,好处是当第一次使用到时才会进行实例化,但缺点是在多线程环境下面会产生多个single对象,现在我们采用多线程来进行懒汉式单例的破解

多线程破解

/**
 * 懒汉式多线程测试
 * 
 * 带你轻松学Java
 * www.it235.com
 *
 */
public class Test {

	public static void main(String[] args) {
		//开启两个线程,此时构造方法被访问了多次
		new SingletonThread().start();
		new SingletonThread().start();
	}
}

//编写一个线程类
class SingletonThread extends Thread{
	@Override
	public void run() {
		Singleton2 instance = Singleton2.getInstance();
		System.out.println("对象地址:" + instance);
	}
}

此时懒汉式在多线程模式下将不堪一击,产生了多个实例,该如何解决呢?接下来我们需要学习双检锁写法

双检锁写法{#gof203}

懒汉式多线程改进

package com.hliedu.singleton.eh;

/**
 * 懒汉式加锁
 * 
 * 带你轻松学Java
 * www.it235.com
 *
 */
public class Singleton2 {

	// 私有构造
	private Singleton2() {
		System.out.println("构造方法");
	}

	private static Singleton2 single = null;
	
	public static Singleton2 getInstance() {
        
		// 等同于 synchronized public static Singleton3 getInstance()
		synchronized(Singleton2.class){
			if (single == null) {
				single = new Singleton2();
			}
		}
		return single;
	}
}

改进加锁后的懒汉式能够扛住多线程下的运行,但通过上述代码我们可以看到,每次访问都加上了锁。而其实除了第一次访问会发生多实例情况,后续都不会再次创建,所以极大的降低了效率,我们继续改进,双检锁写法产生

package com.hliedu.singleton.eh;
/**
 * 单例双检锁举写法
 * 
 * 带你轻松学Java
 * www.it235.com
 *
 */
public class Singleton2 {

	// 私有构造
	private Singleton2() {
		System.out.println("构造方法");
	}

	private static Singleton2 single = null;
	
	public static Singleton2 getInstance() {
		// 等同于 synchronized public static Singleton3 getInstance()
		if (single == null) {
			synchronized(Singleton2.class){
				if (single == null) {
					single = new Singleton2();
				}
			}
		}
		return single;
	}
}

此时解决了每次访问加锁问题,多线程也不在话下,但是我们仍然可以通过反射来访问private构造方法,破坏实例化规则,产生多个实例,所以枚举写法应运而生。

枚举写法{#gof204}

/**
 * 单例枚举写法
 * 
 * 带你轻松学Java
 * www.it235.com
 *
 */
public enum Singleton2 {
	
	INSTANCE;
	 
	public void something(){
		//做你想做的
	}
}

上面是推荐写法,但我们看完就懵圈了,完全不知道怎么回事,怎么用?

其实枚举写法,并不是要求你获取某个单例对象,而是通过枚举类直接去做你想做的事情,简单例子如下

// 单例
public enum SingletonEnum {

    Instance;
    
    private SingletonEnum() {
        System.out.println("枚举类初始化");
    }
    
    public String[] getProperties() {
    	final String[] properties = new String[3];
        properties[0] = "属性1";
        properties[2] = "属性2";
        properties[3] = "属性3";
        return properties;
    }
}

//测试
public static void main(String[] args) {
		//第一次获取
        BeanContext b1 = BeanContext.Instance;
        b1.getProperties();
    	//第二次获取
        BeanContext b2 = BeanContext.Instance;
        b2.getProperties();
}

枚举单例写法来源于Effective Java这本书

书中描述:单元素的枚举类型已经成为实现Singleton的最佳方法

思考:为什么枚举单例模式是最好的单例模式?

  1. 写法简单,简洁明了

  2. JDK定义枚举类的构造方法为private的。每个枚举实例都是static final类型的,也就表明只能被实例化一次。

  3. 创建枚举默认就是线程安全的,所以不必担心线程问题

  4. enum是来自于Enum类的,通过JDK我们可以看到Enum类的构造,可以看到枚举类提供了序列化机制

    public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable
    

    其能够阻止默认的反序列化,方法声明如下:

        /**
         * 阻止默认的反序列化操作
         * prevent default deserialization
         */
        private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
            throw new InvalidObjectException("can't deserialize enum");
        }
    
        private void readObjectNoData() throws ObjectStreamException {
            throw new InvalidObjectException("can't deserialize enum");
        }
    
  5. 反射类中的newInstance方法中,禁止对枚举进行实例化,详见Constructor类416行

@CallerSensitive
public T newInstance(Object ... initargs){
    ...省略
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ...省略
}

最后,不管采取何种方案,请时刻牢记单例的三大要点:

  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
1) 优秀的程序应该是这样的:阅读时,感觉很优雅;新增功能时,感觉很轻松;运行时,感觉很快速,这就需要设计模式支撑。 2) 设计模式包含了大量的编程思想,讲授和真正掌握并不容易,网上的设计模式课程不少,大多讲解的比较晦涩,没有真实的应用场景和框架源码支撑,学习后,只知其形,不知其神。就会造成这样结果: 知道各种设计模式,但是不知道怎么使用到真实项目。本课程针对上述问题,有针对性的进行了升级 (1) 授课方式采用 图解+框架源码分析的方式,让课程生动有趣好理解 (2) 系统全面的讲解了设计模式,包括 设计模式七大原则、UML类图-类的六大关系、23种设计模式及其分类,比如 模式的8种实现方式、工厂模式的3种实现方式、适配器模式的3种实现、代理模式的3种方式、深拷贝等 3) 如果你想写出规范、漂亮的程序,就花时间来学习下设计模式吧 课程内容和目标 本课程是使用Java来讲解设计模式,考虑到设计模式比较抽象,授课采用 图解+框架源码分析的方式 1) 内容包括: 设计模式七大原则(一职责、接口隔离、依赖倒转、里氏替换、开闭原则、迪米特法则、合成复用)、UML类图(类的依赖、泛化和实现、类的关联、聚合和组合) 23种设计模式包括:创建型模式:模式(8种实现)、抽象工厂模式、原型模式、建造者模式、工厂模式。结构型模式:适配器模式(3种实现)、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式(3种实现)。行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式) 2) 学习目标:通过学习,学员能掌握主流设计模式,规范编程风格,提高优化程序结构和效率的能力。
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页