单例模式
饿汉单例模式
单例模式的基本含义:一个对象在JVM中始终只存在一个。要做到看起来比较简单:只需要给类创建一个实例即可。但实际可不是看起来这么简单。
单例模式设计:
- 创建一个静态变量,用于存放当前的对象地址。
- 提供该类实例的get方法。得是静态的
- 将构造方法私有化。
设计如下:
java
class SingleTon{
private static SingleTon instance = new SingleTon();
public static SingleTon getInstance(){
return instance;
}
private SingleTon(){}
}
使用:
java
public class HungrySingleton {
public static void main(String[] args) {
SingleTon s1 = SingleTon.getInstance();
SingleTon s2 = SingleTon.getInstance();
System.out.println(s1==s2); // true
}
}
这里的s1和s2指的是同一个对象。
但是
这个对象创建的太早了,在类加载的时候就实例化了,故这种模式也称之为饿汉模式(一个人太饿了就饥不择食就吃的很早)。
懒汉单例模式
可以晚点创建对象,在用户调用getInstance方法时再创建对象。
设计如下:
java
class LazySingle{
private static LazySingle instance = null;
public static LazySingle getInstance(){
if(instance == null) {
if (instance == null) {
instance = new LazySingle();
}
}
return instance;
}
private LazySingle(){};
}
如此,在调用getInstance方法时再创建对象,称之为懒汉单例模式。
但是这个设计会有线程安全的问题。
当多个线程都调用getInstance方法时,若都进入了if语句,那么创建的对象就不止一个了,极大浪费系统资源。
线程安全的懒汉单例模式
那么就可以使用synchronized对代码进行同步,保证线程安全。
如下:
java
class LazySingle{
private static LazySingle instance = null;
public static LazySingle getInstance(){
// 通过synchronized加锁。
synchronized (LazySingle.class) {
if (instance == null) {
instance = new LazySingle();
}
}
return instance;
}
private LazySingle(){};
}
加锁之后就可以保证线程安全了。但是又出现了一个问题:每次调用getIntance方法之后,都会加锁。(加锁是一个资料消耗极大的过程,故会降低效率)所以,需要在外层添加一个判断,当instance变量不为空的之后,才进行加锁并进行new操作。
故需要改进:
java
class LazySingle2{
private static LazySingle2 instance = null;
public static LazySingle2 getInstance(){
// 外层if,主要是为了提高效率,判定是否需要加锁。
if(instance == null) {
synchronized (LazySingle2.class) {
// 这里的 if判断是否需要new对象。
if (instance == null) {
instance = new LazySingle2();
}
}
}
return instance;
}
private LazySingle2(){};
}
现在基本实现了一个线程安全的单例模式了。但是还有优化的空间。
指令重排序:编译器为了提高效率,在保证逻辑不变的前提下,会优化指令的执行顺序。但是在多线程的代码中,这个前提很难保证。
当线程t1准备通过new创建该对象时。可能会将new的底层操作进行指令重排序。
例如:new的过程主要是三步:
- 申请堆空间
- 在堆空间上构造对象
- 将堆空间的地址返回给istance对象
但是编译器为了提高效率,会变成132的执行顺序,所以instance会指向一个不合法的空间(这个空间上的还没来得及构造对象),那么此时instance引用就不为空了。故当其他的线程t2执行getInstance方法时,外层if判断不成立,会直接返回instance引用。故t2线程得到了一个指向不合法空间的instance,导致误操作。
解决办法:给instance变量加上volatile关键字。volatile关键字会阻止编译器进行指令重排序,保证逻辑正确。
改进如下:
java
class LazySingle3{
private volatile static LazySingle3 instance = null;
public static LazySingle3 getInstance(){
// 外层if,主要是为了提高效率,判定是否需要加锁。
if(instance == null) {
synchronized (LazySingle3.class) {
// 这里的 if判断是否需要new对象。
if (instance == null) {
instance = new LazySingle3();
}
}
}
return instance;
}
private LazySingle3(){};
}
至此:这就是终极版的java多线程的单例模式的实现。当然后续如果通过反射创建对象也可能会存在多线程的安全问题,但是这种情况太极端了,实际项目开发中几乎不会出现。故忽略该场景。