目标
- 掌握单例模式的应用场景
- 掌握IDEA环境下的多线程调试方式
- 掌握保证线程安全的单例模式策略
- 掌握反射暴力共计单例解决方案及原理分析
- 序列化破坏单例的原理及解决方案
- 掌握常见的单例模式写法
定义
- 单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
- 隐藏其所有的构造方法。
- 属于创建型模式。
单例模式的适用场景
- 确保任何情况下都绝对只有一个实例
- ServletContext、ServletConfig、ApplicationContext
- DBPool
单例模式的常见写法
- 饿汉式单例
- 懒汉式单例
- 注册式单例
- ThreadLocal单例
饿汉式单例
- 在单例类首次加载时就创建实例
// 饿汉式单例 写法1
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
public HungrySingleton() {
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
//饿汉式单例 写法2
public class HungryStaticSingleton {
//初始化顺序 先静态后动态 先上后下 先属性后方法
private static final HungryStaticSingleton hungrySingleton;
static{
hungrySingleton = new HungryStaticSingleton();
}
public HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}
优点:执行效率高,性能高,没有任何的锁
缺点:某些情况下,可能会造成内存浪费 大量的实例需要创建的时候需要排队等待
懒汉式单例
- 被外部类调用时才创建实例
// 懒汉式单例 写法
public class LazySimpleSingleton {
private static LazySimpleSingleton lazy = null;
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance(){
if(lazy == null){
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
//测试
//创建线程来实例化
public class ExcutorThread implements Runnable{
@Override
public void run() {
LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread() + ":" +singleton);
}
}
//测试类
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExcutorThread());
Thread t2 = new Thread(new ExcutorThread());
t1.start();
t2.start();
System.out.println("Excutor End");
}
}
打印:
结果1
Excutor End
Thread[Thread-1,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@4add1757
Thread[Thread-0,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@404d8bb2
结果2
Excutor End
Thread[Thread-1,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@283323cb
Thread[Thread-0,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@283323cb
我们会发现有时打印的类是一样的有时是不一样的
打断点调试下看看
当两个线程都进入到这个位置时
将其中一个线程继续跑一步
发现创建的实例是 @496
然后再去跑另一个线程
创建的是@502 这时第一个线程创建的实例被第二个线程创建的实例覆盖掉了
最终打印出的结果是相同的,线程是不安全的,在getInstance()中加synchronized可以保证线程安全
优点:节省了内存,线程安全
缺点:性能低
如何去优化呢? 一步步来
// 优化一
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazy;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
synchronized(LazyDoubleCheckSingleton.class){
if(lazy == null){
lazy = new LazyDoubleCheckSingleton();
}
}
return lazy;
}
}
我们将锁移入到方法内部,发现并不可以,只是将之前在方法层面的阻塞转移到了方法内部。
// 优化二
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazy;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
lazy = new LazyDoubleCheckSingleton();
}
}
return lazy;
}
}
我们将判断移到锁外面,反复测试会发现,当两个线程同时进入到
synchronized (LazyDoubleCheckSingleton.class) {
这里时,仍然会出现后者创建将前者创建的实例覆盖的情况
// 优化三
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance() {
//检查是否要阻塞
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
//检查是否要创建实例
if (lazy == null) {
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
volatile 解决指令重排序问题
优点:性能高了,线程安全了
缺点:if不够优雅,可读性难度增加
//懒汉式内部类写法
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton (){}
private static LazyStaticInnerClassSingleton getInstance(){
return null ;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
优点:写法优雅,利用了java本身语法特点,性能高,避免了内存浪费
缺点:能够反射破坏
//反射破坏单例测试类
public class ReflectTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyStaticInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object instance1 = c.newInstance();
Object instance2 = c.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印:
com.zq.singleton.lazy.LazyStaticInnerClassSingleton@14ae5a5
com.zq.singleton.lazy.LazyStaticInnerClassSingleton@7f31245a
false
//优化
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton (){
if(LazyHolder.INSTANCE != null ){
throw new RuntimeException("不允许非法访问");
}
}
private static LazyStaticInnerClassSingleton getInstance(){
return null ;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
枚举式单例
- 将每一个实例都缓存到统一的容器中,使用唯一标识获取实例
// 容器式单例 1.枚举式
public enum EnumSingletion {
INSTANCE;
private Object data;
public static EnumSingletion getInstance(){
return INSTANCE;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
//测试类
public class EnumSingletionTest {
public static void main(String[] args) {
EnumSingletion instance = EnumSingletion.getInstance();
instance.setData(new Object());
try{
Class clazz = EnumSingletion.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
Object a = c.newInstance();
System.out.println(a);
}catch (Exception e ){
e.printStackTrace();
}
}
}
报错:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.zq.singleton.lazy.EnumSingletionTest.main(EnumSingletionTest.java:24)
枚举中防止了反射获取实例
优点:写法优雅、线程安全
缺点:饿汉式大量创建对象容易造成内存浪费
// 容器式单例2
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
ioc.put(className,instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}
缺点:线程安全问题
//测试类
public class ContainerSingletonTest {
public static void main(String[] args) {
Object instance1 = ContainerSingleton.getInstance("com.zq.singleton.lazy.Pojo");
Object instance2 = ContainerSingleton.getInstance("com.zq.singleton.lazy.Pojo");
System.out.println(instance1==instance2);
}
}
打印:true
序列化破坏单例
//
public class SerializableSingleton implements Serializable {
//序列化
//把内存中对象的状态转换成字节码的形式
//把字节码通过IO输出流,写到磁盘上
//永久保存下来,持久化
//反序列化
//将持久化的字节码内容 通过IO输入流读到内存中来
//转化成一个java对象
private SerializableSingleton(){}
public final static SerializableSingleton INSTANCE = new SerializableSingleton();
public static SerializableSingleton getInstance(){
return INSTANCE;
}
}
//测试类
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton s1 = null;
SerializableSingleton s2 = SerializableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SerializableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerializableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SerializableSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2);
}catch (Exception e){
e.printStackTrace();
}
}
}
打印:
false
如何防止序列化破坏单例呢?
在单例类中加入
private Object readResolve(){
return INSTANCE;
}
是如何解决的呢?
在 ois.readObject()的方法中有如下判断,判断是否重写了readResolve方法,然后反射获取该对象返回
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
如果没有重写,会调用desc.newInstance()重新创建对象返回
obj = desc.isInstantiable() ? desc.newInstance() : null;
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();
}
}
//线程
public class ThreadLocalExcutorThread implements Runnable{
@Override
public void run() {
ThreadLocalSingleton singleton1 = ThreadLocalSingleton.getInstance();
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread() + ":" +singleton1);
}
}
//测试类
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ThreadLocalExcutorThread());
Thread t2 = new Thread(new ThreadLocalExcutorThread());
t1.start();
t2.start();
System.out.println("Excutor End");
}
}
根据打印结果可以看出,基于线程维度是单例的
应用场景
- AbstractFactoryBean.getObject()
- ErrorContext
单例模式的优点
- 在内存中只有一个实例,减少了内存开销
- 可以避免对资源的多重占用
- 设置全局访问点,严格控制访问
单例模式的缺点
- 没有接口,扩展困难
- 可以避免对资源的多重占用
- 如果要扩展单例对象,只有修改代码,没有其他途径
总结
- 私有化构造器
- 保证线程安全
- 延迟加载
- 防止序列化和反序列化破坏单例
- 防御反射攻击单例
本文仅作个人记录学习使用