一文带你了解单例模式及其逐步优化~

单例模式

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。

使用场景:

  1. 需要频繁创建和销毁的对象

  2. 创建对象时耗时过多或资源消耗过大

  3. 工具类对象(无状态的工具类)

  4. 访问数据库或文件的对象(如数据源、session工厂)

  5. 系统级资源(如任务管理器、回收站)

常用的两种实现模式分为饿汉模式和懒汉模式,他们两者的区别在于创建时机。

饿汉模式能够在编译阶段创建实例,懒汉模式会在使用时才会创建实例。

饿汉模式

饿在这里面时急迫的意思,在这里面就是尽早的去创建实例。

在实现的时候我们通过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() {
    }
}

你可能感兴趣的:(javaee,单例模式)