在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:
- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
- 省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
- 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
创建单例很简单,总共三点:
- 私有构造器
- 声明一个私有的静态变量
- 提供一个对外的公共的静态方法访问该变量,如果该变量没有对象,则创建该对象
但是单例有很多种,至于哪一种才是天下无敌的,那要看你系统需要哪种,只有适合才是最好的。好了上代码。
第一种:懒汉式(线程不安全)
/**
* 懒汉式
* 线程不安全
* @author Hacfqx
*
*/
public class SingletonDemo1{
//声明一个私有的静态变量
private static SingletonDemo1 instance = null;
//私有构造器
private SingletonDemo1(){}
//提供一个对外的公共的静态方法访问该变量,如果该变量没有对象,则创建该对象
public static SingletonDemo1 getInstance(){
if (null == instance) {
instance = new SingletonDemo1();
}
return instance;
}
}
在单线程模式下可以使用这个模式创建,但是多线程场景下就万万不可了,该单例模式是线程不安全的,当系统创建的时候回出现多个instance。
第二种:懒汉式(线程安全)
/**
* 懒汉式
* 线程安全
* @author Hacfqx
*
*/
public class SingletonDemo2{
//声明一个私有的静态变量
private static SingletonDemo2 instance = null;
//私有构造器
private SingletonDemo2(){}
//此处增加同步锁synchronized
public static synchronized SingletonDemo2 getInstance(){
if (null == instance) {
instance = new SingletonDemo2();
}
return instance;
}
}
该写法在多线程环境能很好的工作,但是效率很低每次获取单例的时候都要先获取锁,这时候性能极其低下。不建议这样做
第三种:饿汉式 (我好饿,我不管,我就要先拥有这个instance,线程安全但是效率比较低)
public class SingletonDemo3 {
private static SingletonDemo3 instance = new SingletonDemo3();
private SingletonDemo3() {}
public static SingletonDemo3 getInstance(){
return instance;
}
}
第四种:双重校验锁
第五种:静态内部类
/**
* synchronized锁
*
* @author Hacfqx
*
*/
public class SingletonDemo4 {
private static SingletonDemo4 instance = null;
private SingletonDemo4() {
}
/**
* 双重校验
* 将synchronized关键字加在了内部,
* 也就是说当调用的时候是不需要加锁的,
* 只有在instance为null,并创建对象的时候才需要加锁,
* 性能有一定的提升。
* 但是,这样的情况,还是有可能有问题的,
* 看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,
* 也就是说instance = new Singleton();语句是分两步执行的。
* 但是JVM并不保证这两个操作的先后顺序,
* 也就是说有可能JVM会为新的Singleton实例分配空间,
* 然后直接赋值给instance成员,然后再去初始化这个Singleton实例。
* 这样就可能出错了
* @return
*/
public static SingletonDemo4 getInstance() {
if (null == instance) {
synchronized (instance) {
if (null == instance) {
instance = new SingletonDemo4();
}
}
}
return instance;
}
}
/**
* 单例模式使用内部类来维护单例的实现,
* JVM内部的机制能够保证当一个类被加载的时候,
* 这个类的加载过程是线程互斥的。
* 这样当我们第一次调用getInstance的时候,
* JVM能够帮我们保证instance只被创建一次,
* 并且会保证把赋值给instance的内存初始化完毕,
* 这样我们就不用担心上面的问题。
* 同时该方法也只会在第一次调用的时候使用互斥机制,
* 这样就解决了低性能问题。
*
* @author Hacfqx
*
*/
public class SingletonDemo4 {
private SingletonDemo4() {
}
/**
* 静态内部类来维护单例
* @return
*/
private static class SingeletonHolder{
private final static SingletonDemo4 instance = new SingletonDemo4();
}
/**
* 获取实例
* @return
*/
public static SingletonDemo4 getInstance(){
return SingletonHolder.instance();
}
}
public enum EnumSingleton {
INSTANCE;
public void method(){
//TODO
}
}
该写法不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象(具体可以查看enum内部实现)。在实际项目中比较少见。
当然还有其他写法, 一般单例都是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。
说到单例我会想到两个问题:
- 单例模式的序列化
- 单例模式的侵犯
下面是解决代码(我随便拿一个单例举例):
解决办法:
- 序列化单例,重写readResolve()方法
- 在私有构造器里判断intance,如存在则抛异常(防止反射侵犯私有构造器)
我把这两个放在同一个类里执行验证,所以下面需要各位亲手运行代码感受了。= =会有收获的。
public class SingletonDemo6 implements Serializable{
// 类初始化时,不初始化这个对象(延迟加载,真正用的时候再创建)
private static SingletonDemo6 instance;
private SingletonDemo6() {
// 防止反射获取多个对象的漏洞
if (null != instance) {
throw new RuntimeException("单例模式被侵犯!");
}
}
public static synchronized SingletonDemo6 getInstance() {
if (null == instance)
instance = new SingletonDemo6();
return instance;
}
// 防止反序列化获取多个对象的漏洞。
// 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
// 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
package com.zz.designpatterns.createpattern.singletonpattern;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
public class ReflectAndSerialSingleton {
public static void main(String[] args) throws Exception {
SingletonDemo6 sc1 = SingletonDemo6.getInstance();
SingletonDemo6 sc2 = SingletonDemo6.getInstance();
System.out.println(sc1);
System.out.println(sc2);
System.out.println(sc1.equals(sc2)); // sc1,sc2是同一个对象
/**
* 通过反序列化的方式构造多个对象(类需要实现Serializable接口)
*/
// 1. 把对象sc1写入硬盘文件
FileOutputStream fos = new FileOutputStream("object.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(sc1);
oos.close();
fos.close();
// 2. 把硬盘文件上的对象读出来
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
// 如果对象定义了readResolve()方法,readObject()会调用readResolve()方法。从而解决反序列化的漏洞
SingletonDemo6 sc5 = (SingletonDemo6) ois.readObject();
// 反序列化出来的对象,和原对象,不是同一个对象。如果对象定义了readResolve()方法,可以解决此问题。
System.out.println(sc5);
ois.close();
/**
* 通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)
*/
Class<SingletonDemo6> clazz = SingletonDemo6.class;
Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳过权限检查
SingletonDemo6 sc3 = c.newInstance();
SingletonDemo6 sc4 = c.newInstance();
System.out.println(sc3); // sc3,sc4不是同一个对象
System.out.println(sc4);
}
}
这次总结让我更加加深了对单例模式,线程安全,反射侵犯以及序列化对单例的影响有了更加深刻的理解。总归一句话,没有强无敌的单例,适合系统才是最好的。