目录
前言
好的单例模式设计可以帮助我们更好的利用机器内存和提高程序运行效率。
1、单例模式的分类
单例模式一般分为3
大类
:
饿汉式单例:
static 全局的单例实例在第一次被使用时构建;但是加载时机早,占用内存大;
懒汉式
单例
:Synchronized +静态内部类 + double-check 指全局的单例实例在类装载时构建,在线程调用以前就初始化完成,线程安全;懒汉式加载是一种按需加载,这样可以做到资源的最大化利用;
注册式单例
单例
:
-
enum 实现,其好处是防止反射和序列化来破坏单例模式;
-
map 容器注册式单例;
Spring
中常见的单例模式
使用场景:
-
无状态的类,全局不可变参数配置类都可以使用单例模式;
-
Spring容器的注册式单例:spring 的doGetBean() - > getSingleton()就是使用的注册式单例ConcurrentHashmap();
-
Spring框架应用中 ApplicationContext;@Controller和@Service等 常用的类基本都是注册式容器单例模式;
-
ServletContext和ServletContextConfig也都是单例模式;
2、单例模式的具体实现
2.1、饿汉式单例
缺点:
-
(1)、在单例类首次加载的时候加载,没有掉用就会初始化,可能浪费内存;
-
(2)、如果初始化依赖一些其他的数据,很难保证数据会在它初始化之前准备好;
-
(3)、初始化的时机很难把握,由jvm来保证;
优点:
-
简单可靠;如果需要的资源占用的内存少,且不依赖其他的资源,这种方法很高效;
触发类加载的时机:
-
(1). new一个对象时;
-
(2). 使用反射创建它的实例时;
-
(3). 子类被加载时,如果父类还没被加载,就先加载父类
-
(4). jvm启动时执行的主类会首先被加载;
代码实现:
/*
*饿汉式单例模式
*/
public class HungrySimpleSingleton {
//todo 类装载的过程是由类加载器完成的,由jvm保证,所以它由一个先天的优势,免疫多线程引起的问题
private static final HungrySimpleSingleton singleton = new HungrySimpleSingleton();
//todo 防止外部调用 但反射还是可以破坏的;
private HungrySimpleSingleton(){}
public static HungrySimpleSingleton getInstance(){
return singleton;
}
}
反射破坏:
//反射破坏单例
public void reflectionAttack(){
Class<?> clazz = HungrySimpleSingleton.class;
Constructor<HungrySimpleSingleton> constructor1 = (Constructor<HungrySimpleSingleton>) clazz.getDeclaredConstructor();
constructor1.setAccessible(true);
HungrySimpleSingleton hungrySimpleSingleton1 = constructor1.newInstance();
hungrySimpleSingleton1.print();
}
可以在构造函数中加以限制,防止反射的破坏。
//private 防止调用默认的构造函数来创建对象
private HungrySimpleSingleton(){
//todo 防止反射攻击
if (SingletonHolder.singleTon != null) {
throw new RuntimeException("InnerSingleton 不允许构建过多实例!");
}
}
2.2、懒汉式-synchronized加锁
优点:按需调用时创建,避免了饿汉式的空间浪费.
缺点:效率低;
代码实现:
public class LazySynchronizedSingleton {
private static LazySynchronizedSingleton simpleSingleton = null;
private LazySynchronizedSingleton() {
}
//锁定了这个类,阻塞较严重
public synchronized static LazySynchronizedSingleton getInstance() {
if (null == simpleSingleton) {
simpleSingleton = new LazySynchronizedSingleton();
}
return simpleSingleton;
}
}
2.3、double-check无锁方案
由于2.2的懒汉式synchronized使的多线程阻塞严重,为了减少线程的阻塞切换,采取一种double-check的无锁方案。
优点:阻塞范围变小,避免了a线程之外所有的线程等待;
注意原子性问题,拿到的实例可能是没有完全初始化完全的引起数据问题;产生中间数据问题;所以需要加上volatile。
代码实现:
public class LazyDoubleCheckSingleton {
//todo singleton 为什么加volatile需要了解原子操作?
//singleton的赋值实际分为三步走:
// 第一步:给 singleton 分配内存;
// 第二步:调用 Singleton 的构造函数来初始化成员变量,形成实例;
// 第三步:将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)。
//但是在JVM的即时编译器中存在指令重排序的优化 2-3的重排序导致非null的数据不完整;
private static volatile LazyDoubleCheckSingleton singleton = null;
private static AtomicInteger inc = new AtomicInteger(0);
//使用计数防止反射来攻击创建多个实例;
private LazyDoubleCheckSingleton() {
inc.getAndIncrement();
if (inc.intValue() > 1) {
throw new RuntimeException("LazyDoubleCheckSingleton 是单例,不允许重复创建");
}
}
//提供唯一的入口
public static LazyDoubleCheckSingleton getInstance() {
if (null == singleton) {
synchronized (LazyDoubleCheckSingleton.class) {
if (null == singleton) {
singleton = new LazyDoubleCheckSingleton();
}
}
}
return singleton;
}
}
2.4、静态内部类机制
加锁对性能始终有影响,所以考虑最好不加锁,为了兼顾懒加载和锁的消耗,这里采用了内部类来解决问题,但是要注意反射和序列化的问题。
代码实现:
public class InnerSingleton implements Serializable {
//当Singleton初始化的时候,静态内部类SingletonHolder并没有被加载到内存;
//TODO 利用内部类 保证一:利用jvm来完成单例类的初始化,保证唯一性,完美的避免了线程安全问题;
private static class SingletonHolder {
private static final InnerSingleton singleTon = new InnerSingleton();
}
//todo 保证二:懒加载 在调用内部类getSingleton方法时 SingletonHolder才会加载初始化singleTon这个对象
public static final InnerSingleton getSingleton() {
//返回结果前,一定会讲实例加载到内存;
return SingletonHolder.singleTon;
}
//private 防止调用默认的构造函数来创建对象
private InnerSingleton() {
//todo 防止反射攻击
if (SingletonHolder.singleTon != null) {
throw new RuntimeException("InnerSingleton 不允许构建过多实例!");
}
//todo 防止序列化攻击 解决序列化生成多例的问题,可以覆盖反序列化生成的对象,其实还是生成了两次;
//发生在jvm中,之前序列化出来的对象被GC回收;
private Object readResolve() {
return SingletonHolder.singleTon;
}
2.5、 枚举实现单例
缺点:不能继承;
优点:
-
防止序列化;
-
防止反射调用;
//todo 枚举是通过 包名路径类名 + 名字,这样在jvm只会被加载一次;
//todo 如果是枚举,直接抛出异常;modeify.enum JDK层面为枚举不被序列化和反射破坏来保驾护航;
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;
}
}
2.6、容器注册式单例
适用于创建非常多的单例模式,使用容器保存。
代码实现:
public class ContainSingleton {
//ioc容器的实现
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
private ContainSingleton() {
}
public static Object getInstance(String name) {
synchronized (ioc) {
if (!ioc.containsKey(name)) {
Object object = null;
try {
object = Class.forName(name);
ioc.put(name, object);
} catch (Exception e) {
e.printStackTrace();
}
return object;
}
return ioc.get(name);
}
}
}
4、破坏单例模式
4.1、反射调用
防止
反射破坏单例:
阻止反射的措施:在构造函数里控制:
-
使用原子计数;
-
判空的方法;
上例中有实现,这里不再赘述。
4.2、序列化破坏
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。
反序列化后对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例模式:
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); //false
} catch (Exception e) { e.printStackTrace();
} }
}
运行结果:
com.king.HungryStaticSingleton@7506e922
com.king.HungryStaticSingleton@4d405ef7
false
从结果可以看出:产生了两个不同的实例对象。
解决办法:增加readResolve()方法
public class SerializeSingleton implements Serializable {
private static final SerializeSingleton singleton = new SerializeSingleton();
private SerializeSingleton() {
}
public static SerializeSingleton getInstance() {
return singleton;
}
//todo 解决序列化生成多例的问题,可以覆盖反序列化生成的对象,返回原来的单例对象,其实还是生成了两次,发生在jvm中,只不过序列化出来的对象会被GC回收;
private Object readResolve() {
return singleton;
}
}
运行结果:
com.king.HungryStaticSingleton@4d405ef7
com.king.HungryStaticSingleton@4d405ef7
true
从结果来看,序列化后也是同样的对象,这样避免了序列化的破坏。
5、小结
单例模式的优点:
-
1、只有一个实例,减少重复创建,减少内存开销;
-
2、设置全局访问点;
-
3、减轻cpu负担,每次使用直接从容器中返回,提高程序运行效率;
单例模式中每一个关键字都不是多余的:
-
static: 为了使单例空间共享;jvm加载单实例;
-
final: 保证这个方法不会被代理重写;
-
volatile: 保证操作的原子性;线程安全;
-
synchronized: 保证线程安全;
-
private: 保证构造函数私有,避免外部调用;
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。