饿汉式
- 优点: 不用考虑多线程问题,利用clinit机制实现单例对象
- 缺点:类加载时,即使用不到当前对象也要创建对象,占用内存
public class SingleTon {
private static SingleTon instance = new SingleTon();
private SingleTon() {
}
public static SingleTon getInstance() {
return instance;
}
}
懒汉式
加锁双重校验
- 优点: 当对象第一次使用时才会被创建
- 缺点: 相对于后两种代码相对复杂, synchronized会成为系统瓶颈
- 加锁所双重校验的方式不仅可以用在单例模式中,在多线程环境下控制方法执行一次过程中,如: 实现读取配置文件,根据一个id生成一个对应文件等场景都可以使用,而且很灵活.
为什么要判断两次instance是否为null
- 如果去掉外层的判null,则进入getInstance方法时,就会锁定当前类class,此时如果instance已经初始化过每次获取实例对象都会进行一次加锁操作,非常消耗性能.
- 如果去掉内层循环,instance如果没有初始化,经过外层判断后,大量线程进入等待锁状态,得到锁的线程初始化instance成功后之后的每一个线程都会重新初始化一次instance, 这就失去了加锁的意义
为什么instance要添加volatile关键字
- volatile 的作用有两个
- 变量修改时会通知其他线程修改
- 防止指令重排序
- 查看一个Object o = new Object();的过程.
1. new #5 <java/lang/Object>
2. dup
3. invokespecial #1 <java/lang/Object.<init>>
4. astore_1
5. return
- 因为步骤3执行前,对象的地址已经出现在了操作数栈顶的位置,所以单线程中,构造方法的执行与将地址赋值给变量不存在因果关系,所以会存在步骤4 要早于步骤3执行情况, 即o已经不是null了,但是o指向的是还没有执行构造方法的Object对象.
- 回到getInstance方法, 同样,当下一个线程进来的时候,发现instance已经不是null,但是此时的instance还没有执行过构造方法,调用者得到的则是一个实例的"半成品"
- 使用volatile关键字,就是防止以上指令被重排序的情况.
public class SingleTon {
private static volatile SingleTon instance;
private SingleTon() {
}
public static SingleTon getInstance() {
if (null == instance) {
synchronized (SingleTon.class) {
if (null == instance) {
instance = new SingleTon();
}
}
}
return instance;
}
}
静态内部类的方式
- 优点: 使用内部类的加载机制实现懒加载, 官方推荐
- 缺点: 创建对象流程复杂时可能不太灵活
public class SingleTon {
private static class SingletonHolder {
private static final SingleTon INSTANCE = new SingleTon();
}
private SingleTon (){
}
public static final SingleTon getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举
- 枚举值就是单一实例,而且线程安全,懒加载,代码简单
- 缺点: 创建对象流程复杂时可能不太灵活
public enum SingleTon {
INSTANCE;
}