一文带你了解单例模式及其逐步优化~
单例模式
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
使用场景:
-
需要频繁创建和销毁的对象
-
创建对象时耗时过多或资源消耗过大
-
工具类对象(无状态的工具类)
-
访问数据库或文件的对象(如数据源、session工厂)
-
系统级资源(如任务管理器、回收站)
常用的两种实现模式分为饿汉模式和懒汉模式,他们两者的区别在于创建时机。
饿汉模式能够在编译阶段创建实例,懒汉模式会在使用时才会创建实例。
饿汉模式
饿在这里面时急迫的意思,在这里面就是尽早的去创建实例。
在实现的时候我们通过static
来修饰实例,来确保他可以在编译阶段创建出实例。
为了避免外界可以随意创建改实例,我们还需要对他的初始化方法使用private
进行修饰,此处是我们实现单例的关键所在。
最后我们使用getInstace的方法返回这一个实例。
以下是参考代码
class singletonHungry {//因为是static的所以他在编译开始的时候就会创建private static singletonHungry instance = new singletonHungry();//只能通过get方法访问唯一的这个实例public static singletonHungry getInstance() {return instance;//只有读操作}//通过使用private方法使外界无法创建。private singletonHungry() {}
}
因为此操作只涉及读操作,因此并不涉及线程安全问题。
懒汉模式
懒和饿是相对的,他会尽可能晚的创建实例,懒在计算机中并不是一个贬义词,尽晚的使用反而会减少实例对计算机的负荷。
懒汉模式的实现和饿汉是相似的,初始化方法也使用private
进行修饰。
不同的是他的创建是在getinstance的时候进行的。
以下是参考代码
class singletonLazy {private static singletonLazy instance = null;public singletonLazy getInstance() {//当用到他的时候在创建if (instance == null) {instance = new singletonLazy();}return instance;}private singletonLazy() {}
}
他在getInstace的时候涉及读和写两种操作,在多线程下可能会产生bug,因此他是线程不安全的。
线程安全的考虑及其优化
线程安全问题
在懒汉模式中,虽然赋值是原子性的操作,但是加上if整体上就不是了,因此我们需要对其进行加锁操作。
class singletonLazy {private static singletonLazy instance = null;public synchronized singletonLazy getInstance() {//加锁//当用到他的时候在创建if (instance == null) {instance = new singletonLazy();//赋值是原子性的,但是加上if就不是了}return instance;}private singletonLazy() {}
}
执行效率优化
但是此时我们就会出现新的问题,加锁也是有代价的。
它仅仅是在线程未创建的时候会涉及到读和写操作,其他情况只涉及读操作,并不涉及线程安全问题。
虽然在这个时候我们保证了线程安全,但是因为锁的存在,他会相互阻塞,影响了执行效率。
因此我们可以这样优化:
class singletonLazy {private static singletonLazy instance = null;public singletonLazy getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new singletonLazy();}}}return instance;}private singletonLazy() {}
}
通过这么一个巧妙的写法,我们就可以解决上述的问题~~
内存可见性问题
但是我们的优化还没有结束,他是否会涉及内存可见性问题呢?编译器优化的问题我们无法预测,因此为了稳妥起见,我们可以给Instace直接加一个volatile,从根本上杜绝内存可见性问题。
另外,volatile不仅保证了内存可见性问题,还保证了指令重排序的问题。
什么是指令重排序问题呢?
指令重排序:也是编译器优化的一种形式,调整代码运行的先后顺序,以得到提高性能的效果。指令重排序的大前提是逻辑不变,在多线程的环境下,这里的判定可能出现失误。
在上述的优化代码中可能会出现这样的情况:
正常顺序:申请空间->开辟空间->赋值引用多线程下可能出现如下:
线程1 线程2
申请空间|
赋值引用|此时线程2执行发现instance不为空了return instance;
开辟空间此时线程2拿到的引用是一个还未开辟空间的地址
你或许会产生疑惑,我们不是加锁了吗?为什么还会有多线程的问题。
这个问题源于我们的优化导致的,我们在上述使用了双重if,而我们的锁是在第二个if里面的,因此第一个if是不受锁的影响,导致了其他线程的可乘之机。
但是这个问题终究是源于指令重排序,因此我们只需要加上volatile就可以完美解决了~
class singletonLazy {private static volatile singletonLazy instance = null;public singletonLazy getInstance() {if (instance == null) {synchronized (locker) {if (instance == null) {instance = new singletonLazy();}}}return instance;}private singletonLazy() {}
}