单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
类加载的同时, 立即创建实例
class Singleton{
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
private static Singleton instance = new Singleton();
: 带有static的属性称为类属性, 由于每个类的类对象(类名.class)是单例的, 则类对象的属性也就是单例的了. 这个代码时Singleton类被JVM加载的时候执行的.
将_构造方法置成private_, 就可以防止该类在类外被创建的情况, 也就可以形成单例模式.
如何创建实例?
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
//Singleton s2 = new Singleton();报错
}
}
Singleton.getInstance
得到的一直是同一个对象.
在多线程中, 如果有多个线程同时调用getInstance方法, 他读到的都是同一个变量, 所以
饿汉模式是线程安全的
类加载的时候不创建实例. 第一次使用的时候才创建实例.
一般情况下, 懒汉模式一般效率比较高.
例如, 编辑器要打开一个超大的文件(10G), 有的编辑器会尝试把所有的内容都加载到内存中, 再显示出来; 而有的编辑器则只加载一部分内容, 其他部分, 等用户需要用到了再加载
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {}
}
public class Main {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
}
}
很明显, 这里是调用getInstance后才创建实例.
那么如果多个线程同时调用getInstance, 就会很容易引发线程安全问题, 因为上述代码的赋值操作并不是原子的, 所以懒汉模式是线程不安全的
如何解决线程安全问题
我们可以想到:加锁, 让赋值操作变成原子性的.
但是加锁是一个成本比较高的操作, 加锁可能会引起阻塞等待, 所以非必要不加锁. 如果无脑加锁, 就会导致程序执行效率受到影响. 所以这里该如何加锁?
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy() {}
}
如果是上述这样加锁, 那么每一次执行到这个代码的时候都会进行加锁, 但这并不是必要的, 因为引发线程不安全, 是在首次new对象的时候, 一旦把对象new好了, 就不会再出现线程不安全的问题了. 所以这样写会降低效率.
所以只需在instance是null, 需要new对象的时候才需要进行加锁. 所以代码应该如下:
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {}
}
T1 | T2 | |
---|---|---|
t1 | if(instance == null) | |
t2 | if(instance == null) | |
t3 | synchronized (SingletonLazy.class) | 阻塞等待 |
t4 | if(instance == null)… | |
t5 | 释放锁 | synchronized (SingletonLazy.class) |
t6 | if(instance == null) |
问题又来了, T2线程一定能读到T1线程修改的值吗?
所以这里可能又存在了一个内存可见性问题, 所以需要给instance加上一个volatile
class SingletonLazy {
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {}
}
同时, volatile还可以解决指令重排序的问题
instance = new SingletonLazy();
这个操作可以分成三步:
一般的执行顺序使1->2->3, 但也可能会被优化成1->3->2. 假设T1是按照1->3->2执行的, 并且在执行完3后2之前出现了线程切换, 此时还未对对象进行初始化就调度给别的线程了. T2线程执行的时候, 判断instance非空, 于是直接返回instance, 并且后续可能会使用到instance中的一些属性和方法, 这样就可能会出现一些问题.
给instance加上volatile之后, 针对instance的操作就不会发生指令重排序的问题了.