概述
场景:很多时候整个应用只能够提供一个全局的对象,为了保证唯一性,这个全局的对象的引用不能再次被更改。比如在某个应用程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例类统一读取并实例化到全局仅有的唯一对象中,然后应用中的其他对象再通过这个单例对象获取这些配置信息。
如:Spring容器中的对象、Windows任务管理器、垃圾回收站、打印机打印
特点描述 | 特点值 |
---|---|
常用程度 | 非常常用 |
适用层次 | 代码级、应用级 |
引入时机 | 程序编码,代码级 |
结构复杂度 | 简单 |
实现 | 封装对象产生的个数 |
体现原则 | 无 |
实现思路
将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例
class Person{ private Person() { //此类外部无法使用new关键字进行实例化 } }
在该类内提供一个静态方法,当我们调用这个方法时,返回该类实例对象的引用。
public static Person getInstance() { return person; }
饿汉式写法
/**
* 饿汉式单例写法
* 设计模式轻松学:君哥聊编程
*
*/
public class Singleton1 {
// 私有构造
private Singleton1() {}
private static Singleton1 single = new Singleton1();
// 静态工厂方法
public static Singleton1 getInstance() {
return single;
}
}
饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。
缺点:Java反射机制支持访问private属性,所以可通过反射来破解构该造方法,产生多个实例
懒汉式写法
/**
* 懒汉式写法
* 设计模式轻松学:君哥聊编程
*
*/
public class Singleton2 {
// 私有构造
private Singleton2() {}
private static Singleton2 single = null;
public static Singleton2 getInstance() {
if(single == null){
single = new Singleton2();
}
return single;
}
}
懒汉式属于延迟加载范畴,好处是当第一次使用到时才会进行实例化,但缺点是在多线程环境下面会产生多个single对象,现在我们采用多线程来进行懒汉式单例的破解
多线程破解
/**
* 懒汉式多线程测试
*
* 设计模式轻松学:君哥聊编程
*
*/
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);
}
}
此时懒汉式在多线程模式下将不堪一击,产生了多个实例,该如何解决呢?接下来我们需要学习双检锁写法
双检锁写法
懒汉式多线程改进
package com.it235.singleton.eh;
/**
* 懒汉式加锁
* 设计模式轻松学:君哥聊编程
*
*/
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.it235.singleton.eh;
/**
* 单例双检锁举写法
*
* 设计模式轻松学:君哥聊编程
*
*/
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构造方法,破坏实例化规则,产生多个实例,所以枚举写法应运而生。
枚举写法
/**
* 单例枚举写法
*
* 设计模式轻松学:君哥聊编程
*
*/
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的最佳方法
思考:为什么枚举单例模式是最好的单例模式?
- 写法简单,简洁明了
- JDK定义枚举类的构造方法为private的。每个枚举实例都是static final类型的,也就表明只能被实例化一次。
- 创建枚举默认就是线程安全的,所以不必担心线程问题
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"); }
反射类中的newInstance方法中,禁止对枚举进行实例化,详见Constructor类416行
@CallerSensitive public T newInstance(Object ... initargs){ ...省略 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ...省略 }
总结
不管采取何种方案,请时刻牢记单例的三大要点:
- 线程安全
- 延迟加载
- 序列化与反序列化安全