前言
单例模式常用在一些全局唯一的管理类,避免对象重复创建,节省内存创建释放开销。
饿汉模式
每一次通过 new 创建的都是一个新的对象,为了保证全局唯一,我们就可以利用static关键字的特性,创建全局且线程共享的对象。这就是单例的第一种实现:饿汉模式
public class Singleton {
private static final Singleton instance = new Singleton();
// 重要:写单例的第一件事就是将默认构造函数私有化,避免外部通过new来创建对象
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
懒汉模式
优秀的程序员懂得优化代码的时间空间,避免资源的浪费。在首次调用单例前,并没有初始化的必要,引出单例的第二种实现:懒汉模式
- 先私有化默认构造函数
- 在getInstance()方法中判断对象是否存在,没有就先new一个再返回,完美!
public class Singleton2 {
private static Singleton2 instance;
// 重要:写单例的第一件事就是将默认构造函数私有化,避免外部通过new来创建对象
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
为了模拟正常初始化的运行时间,在制造函数中给1ms的sleep。
private Singleton2() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
然后我们在Main函数中疯狂使用。
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Singleton2.getInstance().toString());
}
}).start();
}
-----------打印结果---------------
singleton.Main$Singleton2@6d4bc03f
singleton.Main$Singleton2@7f5b7c1b
singleton.Main$Singleton2@b148489
singleton.Main$Singleton2@30f51e1e
singleton.Main$Singleton2@42f5503f
singleton.Main$Singleton2@1bcc4803
singleton.Main$Singleton2@7bae2ed5
singleton.Main$Singleton2@701e866
singleton.Main$Singleton2@4cf319d1
singleton.Main$Singleton2@60174d9
singleton.Main$Singleton2@71d9ab02
singleton.Main$Singleton2@6d4bc03f
singleton.Main$Singleton2@b20568f
singleton.Main$Singleton2@71d9ab02
singleton.Main$Singleton2@71d9ab02
singleton.Main$Singleton2@71d9ab02
singleton.Main$Singleton2@6299d4f3
singleton.Main$Singleton2@9a662fb
singleton.Main$Singleton2@1b73c3bb
singleton.Main$Singleton2@3b637176
singleton.Main$Singleton2@3b637176
singleton.Main$Singleton2@3b637176
singleton.Main$Singleton2@3b637176
会发现在多线程并发环境下,不能保证单例的线程安全。在第一次请求创建过程中,另外的线程进行读取,对象instance仍为空,开始了另一个对象的创建。
于是想到了给getInstance方法加锁,保证线程安全
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
但这样造成每次获取实例都要进行加锁耗时,我们想要的只是在第一次创建对象的时候加个锁。解决方式也很简单,于是给getInstance改写成给代码块加锁的方式。
public static Singleton2 getInstance() {
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
也就是所说的双重检查锁(double checked locking)。
然而这并不是我们所见的标准DCL单例的写法,new Singleton2()创建对象并非是一个原子操作(类比 i = i + 1,首先是读取,加一,再赋值,需要多步完成),创建对象需要
- 分配内存
- 将对象指向内存
- 初始化对象
在不同的编译器可能对2,3步骤进行重排序,即先赋值给对象然后才进行初始化,会导致getInstance返回的实例为null的情况。于是instance对象加上volatile修饰,保证对象初始化和赋值操作不进行重排,避免创建了对象但是instance为空的情况。
private volatile static Singleton2 instance;
静态内部类实现
需要熟悉类加载机制。静态内部类的方式即能保证在使用时创建,也能保证线程安全,但是不能在初始化时传递参数。如我们需要对单例设置如Context时,选择DCL的方式更为合适。
public static class Singleton3 {
private Singleton3() {
System.out.print("Singleton3初始化");
}
public static Singleton3 getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton3 INSTANCE = new Singleton3();
}
}
其他
还有通过枚举类来创建单例的,感觉平时不会用到。
涉及知识点 static final synchronized关键字,类加载机制,原子性,重排序。
网友评论