文章目录
1.单例模式介绍
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
特点
- 只有一个实例
- 自我实例化
- 提供全局访问点
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
2.单例模式的优缺点
优点:在内存里只有一个实例,节省资源,减少性能开销,提高系统效率,同时也能够严格控制客户对它的访问。
缺点:因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难。
3.单例模式的应用场景
重量级对象,不需要多个实例,如线程池、数据库连接池
4.常见的实现方式
4.1 饿汉式
是在类加载机制的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就被实例化了,不可能存在访问安全问题。
public class HungryPattern {
private HungryPattern(){
}
private static final HungryPattern hungryPattern = new HungryPattern();
public static HungryPattern getSingleton(){
return hungryPattern;
}
}
- 优点: 没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好
- 缺点: 类加载的时候就初始化,不管用与不用都占着空间,如果项目中有大量单例对象,则可能会浪费大量内存空间
也可以使用静态代码块的方式实现
public class HungryPattern {
//静态代码块方式实现
private static final HungryPattern hungryPattern;
static{
hungryPattern = new HungryPattern();
}
public static HungryPattern getSingleton(){
return hungryPattern;
}
}
这两种写法都非常的简单,也非常好理解,饿汉式适用在单例对象较少的情况
4.2 懒汉模式
只有在真正使用的时候,才开始实例化
4.2.1 普通写法
public class LazyPattern {
private LazyPattern(){
}
private static LazyPattern lazyPattern = null;
public static LazyPattern getSingleton(){
if(lazyPattern == null){
lazyPattern = new LazyPattern();
}
return lazyPattern;
}
}
上面的写法是最简单的一种懒汉式单例写法,但是存在线程安全问题,多线程情况下会有一定几率返回多个单例对象,这明显违背了单例对象原则,可以使用synchronized
来解决线程安全问题。
4.2.2 synchronized写法
public class LazyPattern {
private LazyPattern(){
}
private static LazyPattern lazyPattern = null;
public static synchronized LazyPattern getSingleton(){
if(lazyPattern == null){
lazyPattern = new LazyPattern();
}
return lazyPattern;
}
}
synchronized
写法仅仅是在getSingleton()
方法上面加了synchronized
关键字,其他地方没有任何变化。用 synchronized
加锁,在线程数量比较多情况下,如果CPU分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既 兼顾线程安全又提升程序性能呢?答案是肯定的。接下来就再介绍一种双重检查锁(double-checked locking)单例写法
4.2.3 DCL(double-checked locking) 单例
public class LazyPattern {
private LazyPattern(){
}
private static volatile LazyPattern lazyPattern = null;
private static LazyPattern getSingleton(){
if(lazyPattern == null){ //1
synchronized (LazyPattern.class){ //2
if(lazyPattern == null){ //3
lazyPattern = new LazyPattern(); //4
}
}
}
return lazyPattern;
}
}
这里的写法将同步放在了方法里面的第一个非空判断之后,这样可以确保对象不为空的时候不会被阻塞,但是第二个非空判断的意义是什么呢?我们假设线程A首先获得锁,进入了3,还没有释放锁的时候,线程B又进来了,这时候因为线程还没有执行对象初始化,所以判空成立,会进入2等待获得锁,这时候当线程A释放锁之后,线程B会进入到3,这时候因为第二个判空判断对象不为空了,所以就会直接返回,如果没有第2个判空,这时候就会产生新的对象了,所以需要两次判空!
大家可能注意到这里的变量定义上加了volatile关键字,为什么呢?这是因为DCL有可能会存在失效的情况:
4 :lazyPattern = new LazyPattern();
大致存在以下三步:
- 分配内存给对象
- 初始化对象
- 将初始化好的对象和内存地址建立关联(赋值)
而这3步由于CPU指令重排序,不能保证一定按顺序执行,假如线程A正在执行new的操作,第1步和第3步都执行完了,但是第2步还没执行完,这时候线程B进入到方法中的第1行代码,判空不成立,所以直接返回了对象,而这时候对象并没有初始化完全,所以就会报错了,指令重排发生的概率很低,但是还是有可能发生的,解决这个问题的办法就是使用volatile关键字,禁止指令重排序(jdk1.5之后),保证按顺序执行上面的三个步骤。关于volatile关键字,可以参考这篇文章Java多线程volatile底层原理详解
4.2.4 静态内部类写法
public class LazyPattern {
//静态内部类写法
private LazyPattern(){
}
public static class SingletonHolder{
private static final LazyPattern lazyPattern = new LazyPattern();
}
public static LazyPattern getSingleton(){
return SingletonHolder.lazyPattern;
}
}
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化,故而不占内存,第一次调用getSingleton()
方法时虚拟机才会加载SingletonHoler
类。
问题
单例模式真的能够实现实例的唯一性吗?
答案是否定的,利用反射或者反序列化都可以破坏单例模式
public class Test {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("singleton.LazyPattern");
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
//通过反射获取单例对象
Object o = constructor.newInstance();
Object o1 = LazyPattern.getSingleton();
LazyPattern singleton = LazyPattern.getSingleton();
System.out.println(o == o1); // false
}
上面这个结果输出的结果为false,说明产生了2个对象,我们可以用一个很简单的方法来防止单例被破坏,在构造方法中加一个判断即可
private LazyPattern(){
if(singleton != null){
throw new RuntimeException("不允许构造多个实例");
}
}
这样虽然防止了反射破坏单例,但是依然可以被序列化破坏单例,下面就让我们验证一下序列化是如何破坏单例的!
首先对上面的类实现序列化接口
public class LazyPattern implements Serializable {
接下来开始对单例对象类进行序列化和反序列化测试:
//测试反序列化破坏单例
public class Test2 {
public static void main(String[] args) {
LazyPattern l1 = null;
LazyPattern l2 = LazyPattern.getSingleton();
FileOutputStream fos = null;
try{
//会在当前项目路径下创建LazyPatternSingleton.obj文件
fos = new FileOutputStream("LazyPatternSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(l2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("LazyPatternSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
ois.close();
System.out.println(o == l2); //false
}catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果为false,说明产生了2个对象,那么我们应该如何防止序列化破坏单例呢?我们可以在LazyPattern
类加上readResolve
方法就可以防止序列化破坏单例
//防止序列化和反序列化破坏单例
public Object readResolve() {
return lazyPattern;
}
为什么类中只要有readResolve
方法,就可以防止序列化和反序列化破坏单例呢?
这是因为JDK源码中会检验一个类中是否存在一个readResolve()
方法,如果存在,则会放弃通过序列化产生的对象,而返回原本的对象,也就是说,在校验是否存在readResolve()
方法前产生了一个对象,只不过这个对象会在发现类中存在readResolve()
方法后丢掉,然后返回原本的单例对象,保证了单例的唯一性,这种写法虽然保证了单例唯一,但是过程中类也是会被实例化两次,假如创建对象的频率增大,就意味着内存分配的开销也随之增大,那么有没有办法从根本上解决问题呢?那么下面就让继续介绍一下注册式单例
4.3 注册式单例
注册式单例就是将每一个实例都保存到某一个地方,然后使用唯一的标识获取实例
public class ContainerSingleton {
private ContainerSingleton() {
}
public static Map<String, Object> ioc = new ConcurrentHashMap<>();
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 (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return ioc.get(className);
}
}
}
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,spring中的单例就是属于此种写法。
4.4 枚举式
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;
}
}
枚举式单例是《Effective java》一书中推荐的写法,非常高效。
关于枚举类的实现方式,本人暂时还没搞懂,可以看一下写篇文章为什么使用枚举类实现单例模式
4.5 ThreadLocal式单例
ThreadLocal不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){
}
private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private static ThreadLocalSingleton getSingleton() {
return threadLocal.get();
}
}
参考: