一、单例模式的概念
1.1 概念
单例模式是指 确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
单例模式的特点是隐藏其所有的构造方法。
属于创建型模式。
1.2 单例模式的适用场景
确保任何情况下都绝对只有一个实例。
例如ServletContext、ServletConfig、ApplicationContext
1.3 单例模式的常见写法
1.饿汉式单例
2.懒汉式单例
二、饿汉式单例
饿汉式单例,指在类刚刚加载还没有实例化的时候就被创建实例。
2.1 实现方式一
构造方法私有,通过static成员的特点,在类刚刚加载还没有实例化的时候就被创建实例:
/**
* @Auther: jesses
* @Description: 饿汉式单例实现方式一
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton=new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
2.2 实现方式二
构造方法私有化,通过static代码块,在类刚刚加载还没有实例化的时候就被创建实例:
/**
* @Auther: jesses
* @Description: 饿汉式单例实现方式二
*/
public class HungrySingleton2 {
//使用final是为了避免有人通过反射机制将它改变
private static final HungrySingleton2 hungrySingleton;
static{
hungrySingleton = new HungrySingleton2();
}
private HungrySingleton2(){}
public static HungrySingleton2 getInstance(){
return hungrySingleton;
}
}
饿汉式单例的特点
构造方法私有化 类加载时创建实例,不会出现线程安全的问题。
浪费内存空间,因为不管是否用到,在类加载时就会创建实例。
因此需要改进,这就出现了懒汉式单例。
三、懒汉式单例
被外部调用时才会创建这个单例的实例。
3.1 实现方式一 simple实现方式:
/**
* @Auther: jesses
* @Description: simple实现方式
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazy=null;
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance(){
if (lazy == null){
//!!存在线程安全的问题
lazy=new LazySimpleSingleton();
}
return lazy;
}
}
/**
* @Auther: jesses
* @Description: 线程中创建单例对象
*/
public class ExectorThread implements Runnable {
public void run() {
LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" : "+instance);
}
}
/**
* @Auther: jesses
* @Description: 测试类
*/
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new ExectorThread());
Thread thread2 = new Thread(new ExectorThread());
thread1.start();
thread2.start();
System.out.println("Exec End .");
}
}
运行结果可以看到,两个线程会创建出不同的实例。
在demo中单例的实现存在线程安全的问题,若两个线程同时通过了if判断,进入if内部,会创建两个不同的对象。
因此,优化方案是在getInstance()方法上加synchronize关键字。但是synchronize性能不好,尤其修饰在static方法上,会造成整个类都被锁定。
因此,更优解是,将synchronized关键字只修饰方法内部的代码块。
/**
* 将synchronized关键字只修饰方法内部的代码块
**/
public class LazyDubboCheckSingleton {
private static LazyDubboCheckSingleton lazy=null;
private LazyDubboCheckSingleton(){}
public static LazyDubboCheckSingleton getInstance(){
if (lazy == null){
//synchronized修饰在静态方法上可能造成整个类都被锁定,将synchronized设置在方法内部,这样线程至少可以进入方法。
//但是两个线程都同时执行到此处时,都会得到lazy都是空,将会顺序执行到synch中的代码,又出现多次创建不同实例的情况。这种情况又需要进行改进
synchronized (LazyDubboCheckSingleton.class){
lazy=new LazyDubboCheckSingleton();
}
}
return lazy;
}
}
synchronized修饰在静态方法上可能造成整个类都被锁定,将synchronized设置在方法内部,这样线程至少可以进入方法。
但是两个线程都同时进入方法,又会出现得到的lazy都是空,顺序执行synchronized片段,又出现了多次创建不同实例的情况。
故而需要双重检查锁,在synchronized代码块中再加一次判断,即下面的双重检查锁的方式。
3.2 实现方式二 双重检查锁实现:
/**
* @Auther: jesses
*/
public class LazyDubboCheckSingleton {
private static LazyDubboCheckSingleton lazy=null;
private LazyDubboCheckSingleton(){}
public static LazyDubboCheckSingleton getInstance(){
if (lazy == null){//此判断如删除则无法进入方法
synchronized (LazyDubboCheckSingleton.class){
//为避免出现多次创建不同实例的线程安全问题,所以需要再多加一层判断,也就是双重检查
if (lazy == null){
lazy=new LazyDubboCheckSingleton();
}
}
}
return lazy;
}
}
3.3 实现方式三 内部类实现方式:
/**
* @Auther: jesses
* @Description: 内部类实现懒汉式单例
*/
//没有使用到synchronized,性能最高
public class InnerClassLazySingleton {
private InnerClassLazySingleton() {
}
//懒汉式单例
//在LazyHolder里的逻辑要等到外部方法getInstance调用时才执行。
//同时利用加载类前先加载静态内部类的特性,加载LazyHolder的空对象,但不执行其中内容。
//因为类加载机制只会加载一次,实现了单例。
//LazyHolder的空对象在类加载时已经生成,在调用getInstance()时,则转变成实质对象。因而,是线程安全的。
public static final InnerClassLazySingleton getInstance() {
return LazyHolder.LAZY;
}
private static class LazyHolder {
private static final InnerClassLazySingleton LAZY = new InnerClassLazySingleton();
}
}
这种方式虽然解决了线程安全问题和性能问题。
但是它还是可能被反射攻击。
四、反射破坏单例
/**
* @Auther: jesses
* @Description:
*/
public class InnerClassLazySingletonTest {
public static void main(String[] args) {
try {
Class<InnerClassLazySingleton> clazz = InnerClassLazySingleton.class;
Constructor<InnerClassLazySingleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);//该类构造方法被私有,通过此设置进行强制访问
InnerClassLazySingleton obj1 = c.newInstance();
InnerClassLazySingleton obj2 = InnerClassLazySingleton.getInstance();
System.out.println(obj1==obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
InnerClassLazySingleton类的构造方法已经被私有,但还是可以通过反射的方式,直接构造出对象实例,破坏单例。
对比两种方式创建的对象,可以发现结果是false。单例确实被破坏了。
要避免反射破坏单例的漏洞,就可以在构造方法内加入校验:
五、反序列化破坏单例
5.1 通过反序列化破坏单例:
/**
* @Auther: jesses
* @Description: 自定义单例类,构造方法私有,提供getInstance方法获取单例实例
*/
public class SeriableSingleton implements Serializable {
private final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return INSTANCE;
}
//private Object readResolve(){
// return INSTANCE;
//}
}
/**
* @Auther: jesses
* @Description: 入口函数,测试通过反序列化得到的多个实例是否单例
*/
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
可以看到,通过反序列化获取运行得到的不是同一个单例对象。
5.2 如何解决反序列化破坏单例?
将SeriableSingleton类中被注释的readResolve()方法解开,
再次运行test类,
在加上readResolve方法后,test类中两次获取到的是相同的单例对象了。
5.3 原因
现在查看源码 看看为何添加了resolve方法后,就不会创建不同的实例.
点击进入流转对象的readObject()方法:
可以看到调用了readObject0()方法:
继续进入readObject0()方法,可以看到调用了readOrdinaryObject():
进入readOrdinaryObject()方法,可以看到判断了对象是否可以实例化,可以就创建新实例obj:
进入isInstantiable()方法,得出结论,是根据这个对象是否有构造方法来判断是否可以实例化的。
有构造方法则true,创建新的实例:
接着readOrdinaryObject()方法继续向下深入,可以看到源码中判断该对象中是否存在ReadResolve方法,
如果存在该方法,就代理该ReadResolve()方法,将ReadResolve方法的返回 重新赋值给obj对象。
而我们的ReadResolve()方法中直接返回了单例的实例,因此两次创建都是同一个实例:
通过追踪源码,可以了解到实际上还是实例化了两次,只不过有ReadResolve方法时,新创建的对象obj没有返回,而是把单例赋值给了新对象的引用obj。
六、注册式单例
注册式单例又称为登记式单例,就是将每个实例都登记到某处,用唯一的标识获取实例。
注册式单例有两种:枚举登记、容器缓存。
6.1 枚举式单例
自定义枚举类:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
测试类:
/**
* @Auther: jesses
* @Description: 注册式单例-枚举式
*/
public class EnumSingletonTest {
public static void main(String[] args) {
try {
/** 构建该枚举类的实例instance2,并设置属性data为new Object() */
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
//将该属性输出到 EnumSingleton.obj 文件
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
//读取 EnumSingleton.obj 文件,使用一个新的枚举实例instance1接收
EnumSingleton instance1 = null;
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();
//对比两个实例中的data属性
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData()==instance2.getData());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果,两个实例的data属性相同:
为什么枚举式单例可以避免反序列化破坏单例呢?
接下来,分析源码了解其原理。
使用XJad、Jad等反编译工具对EnumSingleton.class文件进行反编译:
可以看到,枚举式单例实际上在静态代码块中就对INSTANCE进行了初始化,
很显然,这是饿汉式单例,在加载时就初始化实例,因此不会出现线程问题。
接着跟踪反序列化的过程源码,
进入readObject()方法:
其中调用了readObject0()方法,继续深入readObject0方法:
在枚举类型的处理逻辑中,调用了readEnum()方法,进入readEnum()方法查看其实现:
可以看到,通过Enum.valueOf(Class,name)获取了实例,再进入valueOf()方法看看:
终于发现,这里从枚举类的一个enumConstantDirectory通过枚举中的name(值项)获取,
而enumConstantDirectory是一个Map<String,T>的数据结构,
我们知道,Map中的key是不能重复的,因此通过"INSTANCE"这个name只能从改map中获取到同一个对象实例,
这也就是枚举式单例不能被反序列化破坏的原因了:
那么,枚举式单例能否被反射破坏呢?
测试反射创建枚举式单例的实例:
public class EnumSingletonTest2 {
public static void main(String[] args) {
try{
Class<EnumSingleton> clazz = EnumSingleton.class;
Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor();
constructor.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
}
运行发现报错,找不到无参构造方法:
查看Enum源码,发现Enum只有唯一的一个带参构造方法,并且访问修饰符是protect的。
那么修改代码,再次测试:
public class EnumSingletonTest2 {
public static void main(String[] args) {
try{
Class<EnumSingleton> clazz = EnumSingleton.class;
Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingleton enumSingleton = constructor.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
}
运行,发现报错,不能用反射创建枚举式单例的实例:
为什么会限制枚举式代理用反射创建实例呢?
再到源码中寻找原因,进入newInstance()方法:
可以看到,在Constructor.newInstance()方法中做了判断,如果类的修饰符是Enum,就直接抛出异常。
原来是在JDK层面就已经替我们避免了反射破坏单例。
枚举式单例是《Effective Java》书中推荐的一种单例写法。
因JDK枚举的特性,避免了反射破坏单例,枚举式单例成为较好的一种实现。
6.2 容器式单例
/**
* @Auther: jesses
* @Description: 注册式单例-容器缓存
*/
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> singletonMap = new ConcurrentHashMap<>();
public static Object getBean(String className) {
// ConcurrentHashMap是线程安全的,但只能保证map内部线程安全,无法保证此getBean方法线程安全。
//加synchronized以保证创建单例实例线程安全。
synchronized (singletonMap) {
//通过className从Map容器中取实例,若不存在,则使用创建后加入Map容器。
if (singletonMap.containsKey(className)) {
return singletonMap.get(className);
} else {
Object object = null;
try {
object = Class.forName(className).newInstance();
singletonMap.put(className, object);
} catch (Exception e) {
e.printStackTrace();
}
return object;
}
}
}
}

七、单例模式小结
单例模式可以保证内存中只有一个实例,减少了内存开销,避免对资源过多占用。