目录
单例模式
单例模式我们最常用的写法就是饿汉式和懒汉式。
那么饿汉式和懒汉式的区别是什么呢?
我们可以看一个简单的例子来了解饿汉式和懒汉式的区别:
首先我们需要知道什么是单例模式:单例单例,顾名思义就是在程序运行的过程中始终只能有一个对象实例,不能再出现第二个对象实例。
饿汉式和懒汉式的区别 举个栗子:
比如说我们需要读取计算机中的一个10个G的文件,饿汉式就是直接把这10个G的文件全部读取到内存中,然后进行显示。这样的操作很明显不是很好,因为我们不知道内存到底够不够,而且10个G的文件打开肯定也要卡半天。相比之下,懒汉式就很聪明了。懒汉式就是不一次性全部打开这个文件,而是只把文件的一小部分显示在当前用户的界面上,当用户翻页的时候,在去读取其他的文件进行显示,而不是一次性全部读取完成。懒汉式就能很快的打开这个文件。
对于单例类,我们需要注意一下几点:
1:单例类只能有一个实例
2:单例类必须自己创建自己的唯一实例
3:单例类必须给其他对象提供这一实例
4:构造方法必须私有化(防止外部直接new)
饿汉式
class Singleton{
private static Singleton singleton = new Singleton(); //static 修饰,属于类对象 只有一份
public static Singleton getSingleton() {
return singleton;
}
//通过构造器私有化 来限制单例
private Singleton() {}
}
public class threadDemo17 {
//单例模式
public static void main(String[] args) {
//s1和s2都是同一份实例
Singleton s1 = Singleton.getSingleton();
Singleton s2 = Singleton.getSingleton();
//报错 构造器私有化
//Singleton s3 = new Singleton();
}
}
我们可以看到饿汉式不管你需不需要这个实例,他都是第一时间先把这个实例创建好。等你需要的时候再去获取。此时我们可以看到,获取到的实例都是同一个实例。
懒汉式
class SingletonLazy {
//懒汉式
public static SingletonLazy singletonLazy = null;
public static SingletonLazy getSingletonLazy() {
if(singletonLazy == null) {
return singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
private SingletonLazy() {} //构造器私有化
}
public class threadDemo18 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getSingletonLazy();
SingletonLazy s2 = SingletonLazy.getSingletonLazy();
System.out.println(s1 == s2);
}
}
上述代码我们可以看到,代码层面饿汉式和懒汉式的区别,饿汉式是在类加载的时候直接把实例创建好,而懒汉式则是在你真正需要这个实例的时候再去给你创建这个实例。
资源的加载和性能
饿汉式在类加载的时候已经创建出来一个静态的实例出来了,这个实例也会占据一定的内存,所以在第一次加载这个类的时候可能会速度较低,但是在第一次调用返回实例的时候,速度会很快,因为对象实例已经创建完成了,只需要返回即可。
懒汉式则是在类进行加载的时候不创建静态实例,也不会占据更多的内存空间,但是在第一次调用返回实例的时候,需要进行对象的实例创建,此时就会导致速度降低。
多线程环境单例模式
上述都是在单线程下的场景。
那么如果在多线程场景下,应该如何使用?
首先我们需要明白线程不安全的因素都有什么?
1:抢占式执行
2:多个线程修改同一个变量
3:修改操作不是原子的(不可分割的最小单位)
4:内存可见性
5:指令重排序
上述就是造成线程不安全的主要因素。
饿汉式
根据上述因素我们可以看出饿汉式在多线程环境下是天然安全的,因为饿汉式不涉及到修改操作,只是读取操作。
懒汉式
懒汉式则是涉及到了修改的操作,根据下述代码我们也可以看出因为在类加载的时候singletonLazy 实例是为空的,如果此时有对个线程需要获取这个单例类的实例时,就会出现new操作进行了多次的情况。假如此时有2个线程t1和t2在并发执行,当t1执行到if判断的时候,条件为真,在进行new操作之前,此时CPU突然调度t2线程开始执行,也进行if判断,条件也为真,也准备进行new操作,此时就会返回两个不同的实例了,这也就违背了单例模式的初衷。此时也就无法保证创建的对象是唯一的了。
public static SingletonLazy getSingletonLazy() {
if(singletonLazy == null) {
return singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
那么如何解决呢?
我们就得从造成线程不安全的原因入手:
我们需要保证整个if判断和new操作是原子的,并且还有保证指令重排序问题。
我们可以采用加锁和禁止指令重排序
解决单例模式线程安全的问题:
保证if和new是原子性的
使用双重if减少不必要的加锁操作
使用volatile关键字禁止指令重排序
class SingletonLazy {
//懒汉式
private static SingletonLazy singletonLazy = null; //禁止指令重排序
public static SingletonLazy getSingletonLazy() {
if(singletonLazy == null) { //这个条件用来判断是否要加锁,如果对象已经有了,则在不加锁,此时本身就是线程安全的
synchronized (SingletonLazy.class) { //保证if() 判定和new是一个原子操作
if(singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
private SingletonLazy() {}
}
public class threadDemo18 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getSingletonLazy();
SingletonLazy s2 = SingletonLazy.getSingletonLazy();
System.out.println(s1 == s2);
}
}
可以看出我们并没有对整个方法进行加锁,因为加锁就意味着效率就要降低,锁的粒度就更粗。
我们可以看看下面的核心代码,首先我们使用双重if进行判断,第一个if就是用来判断是否需要进行加锁操作,如果对象已经存在,则不进行加锁,直接返回,此时线程天然安全的。如果此时对象还没有创建出来,就需要进行加锁操作,因为这个方法是static修饰的静态方法,那么此时的加锁对象就应该是类对象,加锁之后,我们的第二个if则是判断此时对象是否存在,如果不存在就进行new操作,但是在进行new操作的同时,可能就有bug。
假如此时有两个线程t1和t2在并发执行,当t1线程进行第一个if后,加锁成功了,成功之后在进行new操作的过程中,由于编译器优化的指令重排序,此时已经有创建好了这个实例,但是这个实例里面并没有真正的数据,此时CPU切换到t2线程上,t2线程在进行第一个if判断时候,发现条件为假,所以直接返回,但是返回的这个实例时不合适的,因为指令重排序的问题,这就导致t2线程返回的就是一个空壳子。
private static SingletonLazy singletonLazy = null; //禁止指令重排序
public static SingletonLazy getSingletonLazy() {
if(singletonLazy == null) {
//这个条件用来判断是否要加锁,如果对象已经有了,则在不加锁,此时本身就是线程安全的
synchronized (SingletonLazy.class) { //保证if() 判定和new是一个原子操作
if(singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
解决上述问题也非常简单粗暴,我们直接把singletonLazy这个引用用volatile修饰就解决了指令重排序的问题。
下面是最终代码:
class SingletonLazy {
//懒汉式
volatile private static SingletonLazy singletonLazy = null; //禁止指令重排序
public static SingletonLazy getSingletonLazy() {
if(singletonLazy == null) { //这个条件用来判断是否要加锁,如果对象已经有了,则在不加锁,此时本身就是线程安全的
synchronized (SingletonLazy.class) { //保证if() 判定和new是一个原子操作
if(singletonLazy == null) {
singletonLazy = new SingletonLazy();
//有可能会出现指令重排序,所有要把singletonLazy引用用volatile修饰
}
}
}
return singletonLazy;
}
private SingletonLazy() {}
}
public class threadDemo18 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getSingletonLazy();
SingletonLazy s2 = SingletonLazy.getSingletonLazy();
System.out.println(s1 == s2);
}
}
现在就保证了懒汉式在多线程下的安全问题。