名词解释
保证一个类在任何情况下都绝对只有一个事例,要隐藏所有的构造方法
常见用例
- servletContext
- servletConfig
- ApplicationContext
- DBPool
写法分类
饿汉式
以空间换时间,不存在线程安全问题
缺点:在类初始化的时候就已经创建了对象,浪费资源
写法1:
public class HungrySinglton {
private HungrySinglton(){};
private static HungrySinglton instance = new HungrySinglton();
public static HungrySinglton getInstance(){
return instance;
}
}
写法2:
public class HungryStaticSinglton {
private HungryStaticSinglton(){};
private static final HungryStaticSinglton instance;
static {
instance = new HungryStaticSinglton();
}
public static HungryStaticSinglton getInstance(){
return instance;
}
}
懒汉式
以时间换空间
缺点:存在线程安全问题
简单写法
public class SimpleLazySinglton {
private SimpleLazySinglton(){}
private static SimpleLazySinglton instance = null;
public static SimpleLazySinglton getInstance(){
if(null == instance){
instance = new SimpleLazySinglton();
}
return instance;
}
}
改进(双重检查锁)
对实例方法添加 synchronized,不要添加到方法上,因为性能低下,可以加到方法体内
用双重检查锁的方式进行控制,这样就可以兼顾性能和线程安全
代码如下:
public class DoubleCheckLockSingleton {
private volatile static DoubleCheckLockSingleton instance = null;
private DoubleCheckLockSingleton(){}
public DoubleCheckLockSingleton getInstance(){
if(instance == null){
synchronized (DoubleCheckLockSingleton.class){
if(instance == null){
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
}
注意点
volatile
引入volatile 是为了解决 DCL 失效问题
JVM实例化一个对象的时候会经理一下三个步骤
1. 在堆内存开辟空间
2. 在堆内存中实例化对象的各个参数
3. 把对象指向内存空间
由于JVM存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,instance 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的 DCL失效问题
双重检查
第一次检查是为了判断不需要做无谓的创建判断,第二次检查是为了解决多线程下的线程安全问题
静态内部类
内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题,这种形式兼顾饿汉式的内存浪费,也兼顾synchronized 性能问题
public class InnerClassLazySingleton {
//默认使用 InnerClassLazySingleton 的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private InnerClassLazySingleton(){}
//每一个关键字都不是多余的
//static 是为了使单例的空间共享
//final 保证这个方法不会被重写,重载
public static final InnerClassLazySingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return InnerCreateClass.lazyInstance;
}
//默认不加载
private static class InnerCreateClass{
private static final InnerClassLazySingleton lazyInstance
= new InnerClassLazySingleton();
}
}
反射破坏单例
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try{
Class<?> clazz = InnerClassLazySingleton.class;
//通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问,强吻,不愿意也要吻
c.setAccessible(true);
//暴力初始化
Object o1 = c.newInstance();
//调用了两次构造方法,相当于new 了两次
//犯了原则性问题,
Object o2 = c.newInstance();
System.out.println(o1 == o2);
// Object o2 = c.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
}
解决办法
在构造方法中做一些限制,比如:
private InnerClassLazySingleton(){
if(InnerCreateClass.lazyInstance != null){
throw new RuntimeException("不允许创建多个实例");
}
}
序列化破坏单例
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建,就会破坏单例对象的唯一性
可以通过如下代码来测试反序列化对单例的破坏
public class SerializableSingletonTest {
public static void main(String[] args) {
InnerClassLazySingleton s1 = null;
InnerClassLazySingleton s2 =InnerClassLazySingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("InnerClassLazySingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("InnerClassLazySingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (InnerClassLazySingleton)ois.readObject();
ois.close();
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决办法
添加 readResolve 方法,这个方法不属于重写方法,只是 JVM反序列化创建对象时会通过反射去调用这个指定的方法,并将这个方法的返回值作为最终创建对象的值。所以可以在这个方法中再返回一次我们的单例,就能达到反序列化任然得到的是同一个实例对象的目的。
Object readResolve(){
return instance;
}
缺点
这种实际上还是每次调用反射都会实例化一次,只不过新创建的对象没有被返回而已。所以如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,
思考?
静态内部类会存在反序列化攻击吗?怎么处理:
解决办法:
同样可以通过添加 readResolve方法来处理,即处理反序列化破坏单例和处理反射破坏单例是不互相冲突的。
注册式单例(登记式单例)
枚举登记
代码如下
public enum EnumRegisterSingleton {
INSTANCE_ONE,INSTANCE_TWO;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
测试代码如如下:
public class EnumRegisterSingletonTest {
public static void main(String[] args) {
EnumRegisterSingleton s1 = null;
EnumRegisterSingleton s2 = EnumRegisterSingleton.INSTANCE_ONE;
FileOutputStream fos = null;
try {
fos = new FileOutputStream("EnumRegisterSingleton.INSTANCE_ONE.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumRegisterSingleton.INSTANCE_ONE.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (EnumRegisterSingleton)ois.readObject();
ois.close();
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果:true
原理
通过反编译后发现,生成class源码中实际上是已经实现了 饿汉式 的单例模式写法
static{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] { INSTANCE });
}
优点
应对序列化破坏
枚举类型其实通过类名和Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次
查看 ObjectInputStream 》 readObject0()
private Object readObject0(boolean unshared) throws IOException {
...
case TC_ENUM:
return checkResolve(readEnum(unshared));
...
}
看到在readObject0()中调用了readEnum()方法,来看readEnum()中代码实现
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name); //重点看这里
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) { //这个unshared 是 readEnum()方法的参数
handles.setObject(enumHandle, result);
}
}
应对反射破坏
JVM 层面就规定了枚举不能通过反射来创建
查看 Constructor 》 newInstance() 方法
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects"
);
容器式单例(Spring)
法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getBean(String className){
synchronized (ioc) { //解决线程安全问题
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
Spring 中写法举例
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
/** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */
private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
...
}
threadLocal形式的单例
ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全
代码如下:
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
原理
ThreadLocal 将所有的对象全部放在ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。
划重点
枚举式单例也是《EffectiveJava》书中推荐的一种单例实现写法。在JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。