背景
武器库(WeaponStore)是存放武器的地方,所有机体都在武器库拿武器,那么武器库就是单例的。
常规实现
武器库:存放武器,设置为单例
/**
* 武器库
*/
public class WeaponStore {
/**
* 武器:Map<武器名,数量>
*/
private Map<String, Integer> weapons = new HashMap<>();
private static WeaponStore weaponStore;
/**
* 单例模式不能够实例化
*/
private WeaponStore() {
super();
weapons.put("狮子王剑", 1);
weapons.put("重力炮", 2);
weapons.put("等离子切割刀", 10);
weapons.put("马加爵之锤", 4);
}
public static WeaponStore getInstance() {
if (weaponStore == null) {
weaponStore = new WeaponStore();
}
return weaponStore;
}
/**
* 拿武器
*/
public void getWeapon(String weaponName) {
Integer count = weapons.get(weaponName);
if (null != count && count.intValue() > 0) {
count--;
weapons.put(weaponName, count);
System.out.println("装备" + weaponName);
} else {
System.out.println(weaponName + "库存不足");
}
}
}
测试类:测试100个机体同时在武器库拿武器的情况
public class WeaponStoreTest {
public static void main(String[] args) {
for (int i=0; i<100; i++) {
// 现在有100个机体同时在仓库拿狮子王剑,会出现初始化多个单例对象的bug
Thread ancientIronThread = new Thread(new Runnable() {
@Override
public void run() {
WeaponStore.getInstance().getWeapon("狮子王剑");
}
});
ancientIronThread.start();
}
}
}
执行结果
装备狮子王剑
狮子王剑库存不足
装备狮子王剑
狮子王剑库存不足
狮子王剑库存不足
狮子王剑库存不足
狮子王剑库存不足
狮子王剑库存不足
...
分析
武器库原本是单例的,但在多线程情况下创建了多个武器库,不符合预期。
有一种解决方法是在getInstance方法上加synchronize修饰符,不过不推荐使用,会影响效率。
另一种解决方法是使用双重检查锁
。
使用双重检查锁改进武器库(部分代码)
private static volatile WeaponStore weaponStore;
public static WeaponStore getInstance() {
// 双重检查锁:两次检查是否为空实例
if (weaponStore == null) {
synchronized (WeaponStore.class) {
if (weaponStore == null) {
weaponStore = new WeaponStore();
}
}
}
return weaponStore;
}
- 双重检查锁
在获取实例的时候,判断实例是否存在,若不存在,进入阻塞代码,继续判断实例是否存在,如果不存在,就实例化对象。
为什么双重检查锁需要volatile关键字?
因为weaponStore = new WeaponStore不是原子操作,可简单分为三个操作:
- 分配内存memory = allocate()
- 初始化对象initInstance(memory)
- 指向分配的内存空间weaponStore = memory
由于Java有指令重排的特点,上面三个操作可能会以132的顺序执行,那么如果一个线程执行3操作后,另一个线程去获取实例,那么获取到的实例就是半初始化的。使用volatile是因为volatile有禁止指令重排的特点。
总结
上面的实现方式叫:懒汉式单例模式。
懒汉式单例模式:就是在类加载的时候不实例化对象,而是调用的时候再实例化(懒到什么程度呢?用到的时候再初始化。类比懒加载或许更好理解),这种方式会有线程安全问题,解决方案是使用双重检查锁。
另一种实现方式叫:饿汉式单例模式
饿汉式单例模式:就是在类加载的时候实例化对象,调用的时候直接返回(饿到什么程度呢?不管有没有用到,先初始化再说),这种方式没有线程安全问题,但是每次类加载的时候都会实例化对象,会造成不必要的性能开销。