懒汉模式
- 什么是懒汉模式:延迟加载,只有在真正使用的时候才进行实例化。
- 实现方式
class LazySingleton {
private static LazySingleton instance;
private LazySingleton() { }
public static LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
在上面的代码我们实现了单例模式,在只有主线程的情况下我们可以用如下代码判断是否真的实现了单例模式
public class Main {
public static void main(String[] args) {
LazySingleton singleton1 = LazySingleton.getInstance();
LazySingleton singleton2 = LazySingleton.getInstance();
if(singleton1 == singleton2) {
System.out.println("yes!");
} else {
System.out.println("no!");
}
}
}
但是如果我们是多个线程呢?使用下面的测试代码我们可以得到两个进程当中的对象是不同的实例,
public class Main {
public static void main(String[] args) {
new Thread(() -> {
LazySingleton singleton = LazySingleton.getInstance();
System.out.println(singleton);
}).start();
new Thread(() -> {
LazySingleton singleton = LazySingleton.getInstance();
System.out.println(singleton);
}).start();
}
}
那么问题出在哪里呢?其实用过懒汉模式我们就可分析得到原因,在getInstance()方法获取单例对象之前是要先进行对象是否为null的判断的,如果是null那么就会调用私有的构造方法来获取一个对象。由此可见一定是多个线程同时运行时都得到单例对象为null的判断,从而调用了多次私有的构造函数,从而出现了这种情况。
实际上解决方法也是很简单,只要我们控制方法在同一时刻只能有一个线程对方法进行访问,因此我们使用synchronized
来修饰方法即可。
class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
但是如果我们使用synchronized
对方法进行加锁的话,会大大降低程序运行的性能。因为如果已经有一个线程获取了单例对象,那么其他的在他之后访问的线程不论并发与否都能获取到单例对象,这样看来同步锁确实会大大降低程序的运行效率。
那么为了提高程序的运行效率,我们将同步锁的作用范围降缩小。
class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
}
这样我们仅仅是将锁作用于初始化对象的部分即可,但是又存在一个并发的问题,如果同时有并发的几个线程判断单例对象为null,最后得到的对象可能也不是单例对象。因为在一个线程创建完成对象之后,当时并发的其他线程将会继续获取新的对象。因此我们需要这样修改
class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if(instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
但是这样写真的就是万无一失了吗?实际上不是的。我们先说明下面的一个问题。
在创建对象的时候应该是走如下的三步:
1.申请空间
2.初始化
3.引用赋值
但是由于JIT(即时编译),会使得指令重排,将会获得如下的顺序:
1.申请空间
2.引用赋值
3.初始化
那么这将会带来什么样的问题呢?其实结果显而易见,就是某个线程先执行引用赋值而没有先执行初始化 ,那么其他的线程就会得到单例对象不为空的判断,但是对象实际上并没有被初始化,因此会出现空指针这样的问题,因此我们使用volatile
修饰单例对象。
class LazySingleton {
private volatile static LazySingleton instance;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if(instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
饿汉模式
- 什么是饿汉模式:类的加载 初始化阶段就完成了实例的初始化。本质上借助于jvm的加载机制,保证实例的唯一性
- 实现
class HungrySingleton {
private static HungrySingleton instance;
static {
instance = new HungrySingleton();
}
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return instance;
}
}
上面的这种写法实际上是基于jvm的类加载机制来实现的,在调用静态方法之前会首先调用静态代码块,并且只会调用一次而且是线程安全的,除了上面这种写法我们也可以写成静态内部类,也就是下面的这种
class InnerClassSingleton {
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
静态内部类实现的单例模式虽然也是依靠jvm的类加载机制来实现的,但是实质上并不是一种饿汉模式,而是一种懒汉模式,因为静态内部类的初始化,只有在访问instance属性的时候才会进行初始化,并不是在调用getInstance方法之前进行的初始化。
但是如果我们使用反射,来获取构造函数,也就是使用如下的方法对单例模式进行测试:
public class Main {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<InnerClassSingleton> constructor = InnerClassSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
InnerClassSingleton innerClassSingleton = constructor.newInstance();
InnerClassSingleton instance = InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton == instance);
}
}
相信大家也知道最后的结果了,是false,因此我们需要进行防护
class InnerClassSingleton {
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() throws RuntimeException {
if(InnerClassHolder.instance != null) {
throw new RuntimeException("");
}
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
反序列化
我们都知道Serializable
接口,那么如果我们通过反序列化获取一个对象和通过getInstance()
方法获取的对象进行比较,这个时候我们就会发现,这是两个不同的对象,也就是不是单例。
public class Main {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
InnerClassSingleton instance = InnerClassSingleton.getInstance();
FileInputStream fileInputStream = new FileInputStream("test");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
InnerClassSingleton obj = (InnerClassSingleton) objectInputStream.readObject();
System.out.println(obj == instance);
}
}
那么到底如何解决这个问题呢?其实在serializable
接口的注释当中已经给出了解决的办法:
* Classes that need to designate a replacement when an instance of it
* is read from the stream should implement this special method with the
* exact signature.
* <PRE>
* ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
* </PRE><p>
实际上就是要我们实现readResolve()
方法即可,同时为了确保能正确性,我们应该添加serialVersionUID
属性
class InnerClassSingleton implements Serializable {
private static final long serialVersionUID = 42L;
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() throws RuntimeException {
if(InnerClassHolder.instance != null) {
throw new RuntimeException("");
}
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
public Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}
}