1. 单例模式
单例模式是一种设计模式,能够确保一个类中只有一个实例,不会创建多个实例,并提供一个全局访问点以访问该实例.
什么是设计模式?
设计模式就好比如建造房屋的过程.在建造房屋的时候,有许多常见的步骤和方法,如规划土地,设计平面图,选择材料.这些步骤的方法和目的就是为了实现一个稳定的,舒适的房屋.
设计模式提供了一种结构化的方法,可以更好地解决问题,使系统更好维护.
1.1 饿汉模式
饿汉模式是指在类加载时就创建出实例,并且在整个应用程序都保持着这个实例.特点是线程安全,因为实例在类加载的时候,就被创建出来了,不存在读线程并发访问的问题,但是缺点也很明显:浪费空间,有时候可能没有使用到该实例.就会导致浪费.
//单例模式(饿汉模式)
//在类加载的同时,创建实例
class Singleton {
//带有static表示类属性,每个类的类对象是单例的,所以类对象的属性(static),也就是单例的,instance属性只会有唯一一份
private static Singleton instance = new Singleton();
//提供公开方法,获取属性.
public static Singleton getInstance() {
return instance;
}
//禁止别人new实例
private Singleton() {
}
}
public class demo1 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = new Singleton();//新建出来的实例
System.out.println(s1==s2);//同一个对象
System.out.println(s1 == s3);
}
}
1.2 懒汉模式 - 单线程
懒汉模式是指在需要的时候才会创建出实例,延迟初始化.特点是节省了空间,实例只有在使用时才会被创建,但是需要注意的是:在多线程情况下,需要考虑线程安全问题,因为很有可能会被创建出多个实例.
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//修改操作不是原子的,会出现线程安全问题
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
//单例模式(懒汉模式)
public class demo2 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
在多线程环境下,假设有两个线程同时执行 getInstance() 方法:
线程t1执行到 if (instance == null) 时,判断 instance 为空,进入条件内部。
此时线程2也执行到 if (instance == null) 时,也判断 instance 为空,同样进入条件内部。
线程1在执行 instance = new SingletonLazy(); 时,创建了一个新的实例并赋值给 instance。
然后线程2也执行 instance = new SingletonLazy();,会再次创建一个新的实例并赋值给 instance。
这样就导致了两个线程同时创建了两个不同的实例,破坏了单例模式的原本意义。
1.3 懒汉模式 - 多线程
通过上面的代码我们了解到懒汉模式实现是线程不安全的,我们可以使用synchronized(加锁)关键字,来保证原子性.
class SingletonLazy {
private static SingletonLazy instance = null;
public synchronized static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
通过加锁解决了原子性的问题,也就不会创建出多个实例,但是懒汉模式下,我们首次调用加锁,创建出一个实例,但是在后续调用的时候,也加上锁了,实际上,后续调用就不必再加锁了,这里把不该加锁的地方加锁,就会很影响程序的效率.
加锁存在的缺点:
- 加锁会引起额外的开销:加锁和解锁过程会涉及线程的互斥锁的申请和释放等操作,会影响程序的性能.
2.只有再第一次创建实例的时候需要加锁:当实例已经被创建后,后续的县城再次调用getInstance() 方法时,实例已经不存在并且不为空,不需要再进入同步块进行加锁操作.所以,加锁操作只需要在第一次创建实例时加锁.
class SingletonLazy {
//避免"内存可见性"导致读取的instance出现偏差,加上volatile保证能读到最新修改到的结果.
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//判断当前是否已经把instance实例创建好.
if (instance == null) {
synchronized (SingletonLazy.class) {
//判断是否要创建对象
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
其实加上volatile还有另外一个用途,避免此处赋值操作的指令重排序(指令重排序是编译器优化的一种手段,在保证原有执行逻辑不变的前提下,对代码执行顺序进行调整,使调整之后的执行效率提高).
在
instance = new SingletonLazy();
这行代码中,就有可能会出现指令重排序.这行代码大概分为三个步骤:
- 给对象创建出内存空间,得到内存地址
- 在空间上调用构造方法,对对象进行初始化
- 把内存地址,赋值给instance 引用
此处可能会涉及到指令重排序,重排序后执行顺序就可能从 1-2-3,变为1-3-2,
如果是单个线程的话,先执行2还是先执行3是没有问题的.
如果是多个线程,假设t1线程是按照1-3-2的顺序执行,并且在执行完3后出现了线程切换,此时对象还没初始化,就调度给别的线程,t2线程在判定instance!=null,就会直接返回instance,并且后续可能会使用instance中的一些属性或者方法(拿到的是一个没有初始化的对象)
public class demo {
private volatile int x = 0;
private volatile int y = 0;
private volatile boolean flag = false;
public void writer() {
x = 1; // 写操作
y = 2;
flag = true; // 写操作
}
public void reader() {
if (flag) { // 读操作
System.out.println(x + y); // 打印结果
}
}
}
此时有两个线程,一个写线程和一个读线程,写线程负责对变量x,y进行写操作,读操作负责读取flag并执行一些操作.
假设在某一时刻,写线程执行到了x = 1,读线程也判断到了flag = true,执行了打印的操作,然而,由于指令重排序,可能会导致打印操作时,x的值为0,因为读线程可能在写线程设置x = 1之前就读取了这个值.
如果x , y 和 flag都声明为volatile类型,那么对于写线程writer来说写操作就会立即刷新到主内存中,线程每次读取就会从主内存中获取到最新的值,就可以避免到指令重排序带来的问题,确保结果是正确的.