什么是单例模式?
- 单例模式是一种常见的设计模式 所谓设计模式就是在开发过程中为了避免出现很多问题所规定的一套"棋谱"一样的东西
- 常见场景: 在数据库JDBC编程中 我们用到的DataSource这样的类就是一个单例模式的类 在一个程序中 就只有一个实例 不应该创建多个DataSource对象 还有一些服务器里用到的数据管理器 也是单例的
- 使用俩种经典的方式饿汉模式 和 懒汉模式 就可以实现以上场景 保证一个类只能有一个实例
饿汉模式
- 饿汉模式的类在类被加载的过程中就会立刻实例化一个对象 所以后续无论如何操作 只要严格使用get方法 就不会出现其他的实例
- 由上可知 饿汉模式是线程安全的 但是他也会有一个问题就是即使我不使用这个类 他还会创建一个实例来占用我们的内存 这就导致他的效率就不高, 还有就是这个版本的单个实例不能有其他实例变量, 不然还是会出现非线性安全问题 (非线程安全问题就是有个线程对用一个实例变量修改造成数据的脏读)
//饿汉模式 饿汉模式就是在类加载的时候就将唯一的实例创建好 线程安全 但是低效率
class HungrySingle {
private HungrySingle() {
}
private static HungrySingle single = new HungrySingle();
public static HungrySingle getSingle() {
return single;
}
}
public class ThreadTest {
public static void main(String[] args) {
HungrySingle single = HungrySingle.getSingle();
HungrySingle single1 = HungrySingle.getSingle();
System.out.println(single == single1);
}
}
懒汉模式
- 懒汉模式就改进了饿汉模式的缺点 他只有在使用的时候才会让该类去实例化一个对象 并且此后再去获取对象只能获取这一个对象
- 所以我们一般认为懒汉模式比饿汉模式效率更高 但是懒汉模式也有缺点他线程不安全 这点在高性能版懒汉模式解决
//懒汉模式 懒汉模式是在用到该实例的时候才去创建一个实例 后序再去创建实例只是返回原有实例 线程不安全
class LazySingle {
private LazySingle() {
}
private static LazySingle single;
public static LazySingle getSingle() {
if (single == null) {
single = new LazySingle();
}
return single;
}
}
public class ThreadTest2 {
public static void main(String[] args) {
LazySingle single = LazySingle.getSingle();
LazySingle single1 = LazySingle.getSingle();
System.out.println(single == single1);
}
}
高性能版懒汉模式
- 当我们考虑多线程的时候 我们就会发现懒汉模式的单例模式在多线程中是一个多个线程去修改同一变量的操作 那么他必然会导致线程不安全 如何解决这点我在之前博客讲到过 就是 实现原子操作即可
点击跳转:多线程带来的风险(线程安全问题) - 而实现原子操作就是为其上锁 将 load change save 成原子操作
class LazySingle {
private LazySingle() {
}
private static LazySingle single;
public synchronized static LazySingle getSingle() {
if (single == null) {
single = new LazySingle();
}
return single;
}
}
- 但是我们发现上面写法将return语句也包含在原子操作里面 这样可以解决线程安全问题 但是 锁的粒度太大 将一些原本无关紧要的代码或者说本来可以并发执行的代码也变成原子操作 这样导致的粒度大就会降低这整段代码的并发执行能力降低了效率
- 所以我们对其再次进行优化
class LazySingle {
private LazySingle() {
}
private static LazySingle single;
public static LazySingle getSingle() {
synchronized (LazySingle.class) {
if (single == null) {
single = new LazySingle();
}
}
return single;
}
}
- 此时我们继续观察就会发现还有个特别重要的问题需要优化 如果有很多线程 后面一些线程就不是迸发执行 那样再让他们去尝试获取锁释放锁这样的操作就会降低他们的效率 所以我在加锁的外面再判断一次是否为空 这样 就可以保证后序线程的效率
class LazySingle {
private LazySingle() {
}
private static LazySingle single;
public static LazySingle getSingle() {
if (single == null) {
synchronized (LazySingle.class) {
if (single == null) {
single = new LazySingle();
}
}
}
return single;
}
}
最终版本
- 当我们在画时间线会发现 线程2一个线程会尝试去获取俩次single对象那么久有可能编译器对其进行优化 这样一旦优化就会出现内存可见性的安全问题 所以我们需要给single对象加上volatile去保证他的内存可见性
class LazySingle {
private LazySingle() {
}
private volatile static LazySingle single;
public static LazySingle getSingle() {
if (single == null) {
synchronized (LazySingle.class) {
if (single == null) {
single = new LazySingle();
}
}
}
return single;
}
}