单例模式 (非常经典的设计模式)
单例 => 单个实例(对象)
在某些场景中,希望有些类,是只能有一个实例的
例如: 很多管理数据的对象就应该要是"单例"的, 例如 DataSource ;
如果不使用单例模式, 就全靠程序员自己自觉了,例如: 一个类只能有一个对象,但是程序员一段时间后忘记了,有创建了新对象,或者换了一个人来写代码,这个人不知道只能有一个对象,就疯狂new新对象 ;
单例模式让编译器强制要求,保证这个类的对象不会 出现多个
同样的思想例如: final , interface , @Override , throws ;
语法中没有单例这样的支持, 所以通过我们的编程技巧来达成;
实现单例模式
饿汉模式(在类被加载的时候就创建了类的实例,比较早)
-
创建一个Singleton类 (single : 唯一的,仅有一个的)
-
让类中有一个静态的实例 , 静态成员, 在类加载的时候创建;
-
创建一个静态的方法获取类的实例, 静态方法通过类名调用,就不用对象调用
-
让 类的构造方法 private ,让其他地方无法new 类的对象
class Singleton{ // 创建一个静态的 实例对象 private static Singleton instance = new Singleton(); // 创建一个静态方法,用来获取类的实例 public static Singleton getInstance(){ return instance; } //将构造方法给private,让其他地方无法new对象 private Singleton(){}; }
这时候只能通过getInstance(),来获取类的实例,并且不能new 一个对象

懒汉模式 (相比第一个没有那么早,在第一次使用的时候才创建实例)
和上面饿汉的区别,
在首次调用 getInstance() 的时候,才会真正创建出实例 , (如果一直都没有调用,就不创建实例)
class Singleton2{ //让类的实例为null private static Singleton2 instance = null ; //一个静态方法里,如果第一次调用才创建类的实例 public static Singleton2 getInstance(){ if(instance == null){ instance = new Singleton2(); } return instance; } private Singleton2(){}; }
饿汉式和懒汉式的例子;
一个文件10个g,
-
把全部内容都加载了,到内存中, 在把内容显示出来, (加载过程漫长)
-
把小部分数据加载到内存中,显示这部分内容,等用户翻页了,在动态加载其他内容
上面的饿汉和懒汉模式的 线程安全问题
上面的饿汉模式是线程安全的, 懒汉模式是线程不安全的 (懒汉的if 和 new 不是原子的)

1. 用加锁, 解决懒汉模式的线程安全问题后, 但是懒汉模式又变的不高效了(效率低了)
加锁后,每次线程都要判断锁,如果锁竞争还要阻塞掉, 这就非常的降低效率,本来懒汉模式,就是为了比饿汉模式高效点

2. 多加一个if判断 ,在用来判断是否要加锁;
懒汉模式只有在 第一次 调用的时候才涉及 线程不安全,加了锁之后,虽然安全了,但是效率不高了;
多一个if 判断是否要加锁, 这样就可以只在第一次调用时,才出现锁,后面就都不会加锁

虽然两个if看起来一样,但是两个if都不一样, 第一个用来判断是否要加锁, 第二个用来判断是否要new对象,
只有在第一次调用才会出现锁竞争, 后续都没有锁竞争,提高效率;
指令重排序问题
指令重排序和 内存可见性 同样是 编译器给我们优化时, 可能出现的线程安全问题;
指令重排序 : 在不改变代码原来的逻辑情况下, 编译器给我们的代码进行优化, 调整我们的代码顺序, 来提高效率;

虽然我们买的是同样的东西,但是经过编译器的优化,指令重排序 ; 效率提高了
但是在多线程中就可能会出现线程安全问题 ;
new 一个对象 可能会出现线程安全问题
我们上面已经设置了两个if() 和 加锁 来防止 new 出多个对象 ;但是 new 一个对象 可能会出现问题
new 一个对象要做的步骤
-
首先 要申请内存空间
-
然后在内存空间里 , 调用构造方法,初始化对象
-
返回内存指针 给 instance

一旦new 一个对象 的顺序 是 1. 3 . 2 . , 然后 开始调度t2线程, t2 线程在第一个if就发现 instance不为空, 拿到了Instance (未初始化的非法对象)就出现大问题了

此时指令重排序, t2 线程 拿到一个非法的对象 , 掉用 对象里的成员 , 就会出现问题
加volatile 解决 指令重排序问题

给 instance 加上 volatile , 就编译器就不会给我们进行优化了, 就不会 让new 的顺序改变 ;
单例总结
懒汉模式分3步骤
-
if判断加锁 , 让 if 判断和 new 变的原子
-
再加一个if判断 , 是否要加锁
-
让new 对象 , 不出现指令重排序 , 加上volatile (同时解决"内存可见性")

这3点保证了在一般日常的情况想单例的稳定 ;
仍要其他问题打破单例模式
-
使用反射 , 打破单例模式
-
使用序列化 , 反序列化,打破单例
但都是过于挑刺了, 一般日常 不会使用到这两个来打破 单例, 这两个情况的使用范围都非常严谨的