单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
使用场景:
需要频繁创建和销毁的对象
创建对象时耗时过多或资源消耗过大
工具类对象(无状态的工具类)
访问数据库或文件的对象(如数据源、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() {
}
}