单例模式大家一定不陌生
- 饿汉式
public class SingletonTest {
private static SingletonTest instanse = new SingletonTest();
private SingletonTest() {
}
public static SingletonTest getInstanse() {
return instanse;
}
}
在类加载的时候就初始化一个对象,这样没有线程安全问题。但是没办法做到懒加载
2. 懒汉式
public class SingletonTest2 {
private static SingletonTest2 instance;
private SingletonTest2() {
}
public static SingletonTest2 getInstance() {
if (null == instance) {
instance = new SingletonTest2();
}
return instance;
}
}
只有在getInstance时才去初始化这个对象,实现了懒加载
但是在线程情况下会出现线程安全问题,如果线程1到了if(null==instance)这一步停止了,线程2这时也会走到这一步,这样就会创建两个实例。最简单的解决办法是在方法上加synchronized。但是这样又会影响性能,因为只有第一次会出现线程安全问题,以后只要判断返回即可。
因此可以采用双重检查的方法
public class SingletonTest3 {
private static SingletonTest3 instance;
private SingletonTest3() {
}
public static SingletonTest3 getInstance() {
if (null == instance) {
synchronized (SingletonTest3.class) {
instance = new SingletonTest3();
}
}
return instance;
}
}
不幸的是,这样依然没有解决问题,会遇到空指针问题。
可能会非常奇怪,这样怎么会有空指针问题呢?这和java的内存模型有关系。在上面的类中,除了一个instance变量,就没有其他的成员变量了。如果这个类中有许多其他的属性和变量,或者在构造方法中做了一些耗时的初始化工作,那么在instance变量构造完成后,其他属性并没有来得及加载完毕,直接调用就会引发空指针问题。
在并发中的三个重要的概念
1. 原子性
2. 可见性
3. 有序性
在jvm的优化下,有序性并不是完全严格的。例如在一段代码块中
{
int a; (1)
boolean b; (2)
a = 1; (3)
b = false; (4)
}
执行顺序并不一定是 1,2,3,4.可能是1,3,2,4.jvm会保证最终一致性,但是会做一些优化顺序,所以就会出现这样的问题
那么怎么解决这个问题呢?
public class SingletonTest4 {
private static volatile SingletonTest4 instance;
private SingletonTest4() {
}
public static SingletonTest4 getInstance() {
if (null == instance) {
synchronized (SingletonTest4.class) {
instance = new SingletonTest4();
}
}
return instance;
}
}
这个例子中,instance被volatile修饰,便可以解决这个问题。
原因是volatile关键字保证了两个性质,可见性和有序性,但是不保证原子性。可见性是指不同线程读取到同一个变量,必须是一致的,不会因为各自线程的缓存而有区别。有序性是指在读取这个变量是,必须先执行写操作,也就是保证最终拿到的instance,一定是在初始化工作全部完成后的,也就是happens-before原则。
还有几个方法也可以保证线程安全
1. 使用内部类
public class SingletonTest5 {
private static volatile SingletonTest5 instance;
private SingletonTest5() {
}
private static class InstanceHolder {
private static final SingletonTest5 instance = new SingletonTest5();
}
public static SingletonTest5 getInstance() {
return InstanceHolder.instance;
}
}
分析一下,在类的加载中有几个阶段,1,装载 2,链接 3,初始化。static在加载的时候只会被运行一次且能严格保证运行顺序。而且是主动加载,也就是懒加载。在调用的时候会被初始化一次。这样便可以解决问题
- 使用枚举
public class SingletonTest6 {
private static volatile SingletonTest6 instance;
private SingletonTest6() {
}
private enum Instance {
INSTANCE;
private final SingletonTest6 instance;
private Instance() {
instance = new SingletonTest6();
}
private SingletonTest6 getInstance() {
return instance;
}
}
public static SingletonTest6 getInstance() {
return Instance.INSTANCE.getInstance();
}
}
枚举定义的时候构造函数中就创建了instance,且只会被创建一次,因此也是线程安全的。