简介
单例模式,即在任何情况下。我们去取某一个对象的实例,取到的均是同一个实例。这个实例的物理地址是确定的唯一的。所以在我们程序的运行期间该单例对象它只可能创建一次。
应用场景:
例如我们的现实世界电脑屏幕,一台电脑有很多程序,但是我们只有一个屏幕,此时获取的屏幕对象就只能是单例供各个程序分配调度使用。或者我们开发中的线程池、缓存、配置表等对象。只要是整个系统只能存在一个对象的情况都可以应用单例模式。
设计单例:
public class SingleTon {
private static SingleTon instance;
private SingleTon(){
}
public static SingleTon getInstance(){
if(instance == null){
instance = new SingleTon(); // 实例化实体
}
return instance;
}
}
上述代码是单例模式经典实现:
1.把对应的构造方法和构造出来的实体类进行私有化,不对外进行开放。
2.对外开放静态方法去获取对象的实体,获取的方式在内部实现,可以看到这里会判断如果有实体了就不再去构造,从而保证对象的唯一性。
总结一下就是:确保一个实体类,并提供一个全局访问点。
按照上述的方法实现在大多数情况下,我们的确可以创造出唯一的单例对象。但是如果我们放到多线程中去看上述代码,是不是就可以保证一定取到的是单例?
定位到上述代码中 instance = new SingleTon(); 这段代码,这段代码我们其实可以拆分为两段执行顺序:
- new SingleTon() 初始化实例对象。
- 将实例对象赋值给 instance。
现在有两个线程,线程 X 和线程 Y 同时去调用到 getInstance() 方法。
线程 X 执行到了 顺序 1 去实例化 SingleTon 对象。此时线程 X 正好被 cpu 释放掉处理的调度,由线程 Y 继续执行 getInstance() 方法,当线程 Y 执行到判断语句判断 instance 是否为空,因为前面线程 X 只是进行 new SingleTon() 实例,但是还没赋值给 instance,所以 instance 此时依然为空。这种情况就会导致再次实例化一次 SingleTon 对象,从而线程 X 和 线程 Y 最终拿到的 instance 对象会是不同的对象。
线程 X : if(instance == null){}
线程 X :new SingleTon() --> 对象地址 A
线程 Y : if(instance == null){}
线程 Y :new SingleTon() --> 对象地址 B
线程 Y :赋值 instance 指向 B地址
线程 Y :返回 instance
线程 X :赋值 instance 指向 A 地址
线程 X : 返回 instance
上面的执行顺序反映了多线程在访问上述单例模式代码中可能会出现创建不同实例的情况,接下来我们对上述代码进行优化来解决多线程的同步问题。
public class SingleTon {
private static SingleTon instance;
private SingleTon(){
}
public static synchronized SingleTon getInstance(){
if(instance == null){
instance = new SingleTon();
}
return instance;
}
}
getInstance() 静态方法中加上 synchronized 锁修饰,这样getInstance() 方法限制每次只能一个线程去访问,所以此时线程 X 和 线程 Y 同时去访问 getInstance() 方法的时候。只有当一个线程调用 getInstance() 方法直至返回对象,另外一个线程才可以去继续访 getInstance() 方法。
线程 X : if(instance == null){}
线程 X :new SingleTon() --> 对象地址 A
线程 X :赋值 instance 指向 A 地址
线程 X : 返回 instance
线程 Y : if(instance == null){}
线程 Y :instance 不为空 ,并且指向 A 地址
线程 Y :返回 instance
如上按照 synchronized 的方法就一定保证了多线程去获取单例对象的唯一性了。
但是其实我们也知道,在多线程中使用 synchronized 修饰方法,会降低程序的执行性能。这里我们只需要在第一次去获取 instance 对象的时候锁住整个方法代码块。当获取到了 instance 对象之后每次还对代码块进行 synchronized 同步锁住,其实也是一种累赘。
当然如果对性能要求不是很关键,上述代码已经足够保证单例对象的唯一性。
假如我们追求比较高的性能,我们同样还有两种选择去优化
第一种方法 静态变量加载实例化:
public class SingleTon {
private static SingleTon instance = new SingleTon();
private SingleTon(){
}
public static synchronized SingleTon getInstance(){
return instance;
}
}
上述代码private static SingleTon instance = new SingleTon(); 同样分段执行语句。但是这里我们之前的分段执行不一样,instance 为静态变量, 此处 jvm 可以保证类的静态变量在类加载的时候已经被创建出来了。所以运行期间我们多线程调用 getInstance() 一定是唯一的对象返回,因为它在类加载的时候已经被创建了。
第二种方法,双重检查加锁:
public class SingleTon {
private volatile static SingleTon instance ;
private SingleTon(){
}
public static synchronized SingleTon getInstance(){
if(instance == null){
synchronized (SingleTon.class) {
if(instance == null){
instance = new SingleTon();
}
}
}
return instance;
}
}
主要做了两个方面的优化:
- volatile 关键字修饰 instance 常量,保证多线程访问 instance 时候,当其中一个线程修改了 instance 的值,其它线程即时同步相关数据。
- getInstance() 方法使用了双重检查加锁,同时当多线程访问时候,只有第一次进入时候会进入 synchronized 加锁代码块。因为当有其他线程再次进入的时候 instance 已经赋值了,不会再次进入同步代码块。从而增加程序的运行性能。
注:双重加锁不适用于 java1.4 以及更早版本,JVM 对于 volatile 关键子的实现会导致双重加锁的失效。
网友评论