Java单例模式写法
目录
- 单例模式
- 饿汉模式实现单例
- 懒汉模式实现单例
- 单线程版
- 多线程版
- 多线程版优化
- 小结
单例模式
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
为什么要保证只存在一份对象呢?
因为有些对象管理的内存数据可能会很多, 可能有些项目里就一个对象运行起来就吃上百G的内存空间, 如果不小心多new了几个, 那系统可能直接崩溃了.
饿汉模式实现单例
类加载的同时, 创建实例.
class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {} //不允许外部调用构造方法
public static Singleton getSingleton() {
return singleton; //将创建好的实例返回
}
}
这里只是单纯的读操作, 因此该模式是线程安全的.
懒汉模式实现单例
核心思想 :非必要不创建.
加载的时候不创建实例. 第一次使用的时候才创建实例.
单线程版
class Singleton {
private static Singleton singleton = null;
private Singleton() {} //修改了构造方法的访问修饰权限符, 只有在类内部才能访问构造方法
public static Singleton getSingleton() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
上面的懒汉模式实现是线程不安全的.
为什么不安全呢?
比如两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 它两都 new 出了对象, 这与我们预期的创建一个对象不符, 所以线程不安全.
我们可以通过加锁来解决这一问题, 下面是多线程版 , 线程安全.
多线程版
怎么加锁呢?
看看这样加锁是否可行:
class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getSingleton() {
if(singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
显然这样是不行的, 当两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 然后通过竞争锁, 竞争成功的线程先 new 对象, 另一个线程后 new 对象, 这也是 new 了多个对象, 与预期不符.
我们再来看看给方法加锁:
class Singleton {
private static Singleton singleton;
private Singleton() {}
public synchronized static Singleton getSingleton() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
当多个线程同时调用 getSingleton 方法时, 通过竞争锁, 竞争成功的线程先进去创建完对象出来后, 其他线程再来获取对象就不会再创建对象了.
其实这里还有一个问题, 那就是指令重排序问题, 什么意思呢?
举个例字 :
假设我们有两个线程 t1 和 t2,
t1 有个操作是: s = new Student();
t2 有个操作是: if(s != null) { s.learn(); }
这个操作 : s = new Student(); 大体可以分为三个步骤:
- 申请内存空间
- 调用构造方法(初始化内存的数据)
- 把对象的引用赋值给 s (内存地址的赋值)
如果是单线程, 此处进行指令重排序,步骤2和步骤3是可以调换顺序的, 重排序后可能就是132执行了, 这对单线程结果没影响, 但多线程就不行了.
回到上面的 t1 和 t2, 如果 t1 的操作进行指令重排序, 就会先申请内存, 然后把这个内存地址赋值给 s (注意这里还没有调用构造方法, 没有new对象), 这时 s 还是 null ,这个时候如果线程 t2 刚好进行 if 判断则会直接进入, 然后调用 s 的方法, 因为 s 为空, 就会抛出空指针异常.
这也是上面给方法加锁代码存在的问题, 如果得到的 singleton 为空就调用这里面的方法, 那就会产生空指针异常.
如何避免发生指令重排序呢?
我们可以加个 volatile 来禁止指令重排序, 在下面代码中体现.
多线程版优化
class Singleton {
private volatile static Singleton singleton; //禁止对singleton进行指令重排序
private Singleton() {}
public static Singleton getSingleton() {
//注意这里有两个 if 判断是否为空!!!
if(singleton == null) {
synchronized (Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么要两个 if 判断呢?
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.因此后续使用的时候, 不必再进行加锁了.
这样可以让加锁操作只在第一次创建实例的时候出现.
小结
单例模式线程安全问题 :
-
饿汉模式, 天然就是安全的, 只是读操作.
-
懒汉模式, 不安全的, 有读也有写.
如何将懒汉模式变安全:
- 加锁, 把 if new 变为原子操作.
- 双重 if, 减少不必要的加锁操作.
- 使用 volatile 禁止指令重排序, 保证后续线程拿到的是完整对象.