专栏简介: JavaEE从入门到进阶
题目来源: leetcode,牛客,剑指offer.
创作目标: 记录学习JavaEE学习历程
希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
单例模式
单例模式是常见的设计模式之一.
什么是设计模式?
设计模式就好比象棋中的棋谱 , "红方当头炮 , 黑方马来跳" , 针对红方的走法 , 黑方可以使用一些固定套路来应对 . 软件开发中也是如此 , 针对一个固定的问题场景 , 业界大佬以及总结出一些固定的套路 , 按照这个固定套路可以少走不少弯路.
单例模式能保证每个类只有一个实例对象 , 而不会创建出多个实例.
这一点在很多场景都有应用 , 如JDBC编程中 , 要求DataSource类只能创建一个实例对象.
单例模式的具体实现方式主要分为两种 , "饿汉模式"和"懒汉模式".
1.饿汉模式--单线程版
顾名思义 , 一个饿了很久的人 , 看到吃的就会很急切."饿汉模式"在类加载阶段 , 就把实例对象给创建出来 , 给人一种很急切的感觉.
代码示例:
这段代码最关键的就是 , 在类中创建实例对象时用static修饰 .
- 1.static 保证了这个实例的唯一性 , static操作让instance属于类属性 , 类属性属于类对象 , 类对象又是唯一的 , 因此这个实例对象也是唯一的.
- 2.static 保证对象在类加载阶段就创建好 , 否则由于构造方法被private修饰 , 在main方法中无法直接创建实例对象.
class Singleton{
//此处 , 先把实例创建出来.
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){}
}
public class ThreadDemo4 {
public static void main(String[] args) {
Singleton s = Singleton.getInstance();
}
}
2.懒汉模式--单线程版
顾名思义 , 一个很懒的人绝对不会提前做一件事 , 只有等到非做不可的时候才去做.同样在代码中 , 这个实例并非是类加载的时候创建 , 而是第一次使用的时候才去创建.
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if (instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){}
}
public class ThreadDemo4 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);//true
}
}
3.懒汉模式--多线程版
说到多线程 , 最先考虑的就是安全性 , 通过代码可以看出 , 饿汉模式多线程调用, 只涉及到读的操作 , 因此饿汉模式在多线程中是天然安全的.而懒汉模式多线程调用象 , 即需要读也需要写.
由于读和写操作不是原子性的 , 很容易出现出现多个线程同时load到null , 这时就会创建多个实例对象 , 很显然不符合单例模式.
解决方式:
出现上述线程安全问题 , 本质上是读 比较 和写操作不是原子的 , 因此需要加锁 , 才能保证这些操作是一个整体.
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对象之后 , 后续调用getnstance还有必要吗?后续instance的值一定是非空的 , 因此一定会触发return操作 , 相当于一个比较操作和一个返回操作 , 这两个操作都是读操作 , 此时不加锁也没事 , 因此我们对代码进行更进一步的修改.
此时不再是无脑加锁 , 只有满足条件才加锁.
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(){}
}
此时多线程安全性还是没有做到万无一失 , 这段代码还存在指令重排序问题 , 之前我们把new操作看做是一个整体 , 这在单线程中没有任何问题 , 可一旦涉及到多线程问题就显而易见.new操作通常需要三步:
- 1.申请内存空间
- 2.调用构造方法 , 把内存空间初始化成一个合理的对象.
- 3.把内存对象地址赋值给instance引用.
正常情况下是按照1->2->3的顺序来执行 , 但有时编译器为了提高执行效率 , 会进行指令重排序的操作 , 此时执行顺序就可能是1->3->2 , 如果是单线程没有任何区别 , 但如果是多线程就有问题了 , 假设 t1 按照1->3->2的顺序执行 , 当 t1 执行到1->3之后就被CPU挂起 , 此时 t2 来执行 , 站在 t2 的视角此时的 instance 引用就非空了 , 那么 t2 就会返回 instance 引用 , 并且尝试使用引用中的属性 , 但由于 t1 中的->2还没执行完 , 所以 t2 拿到的就是非法对象.
所以我们需要使用 volatile 去修饰该对象 , 来防止指令重排序.
volatile有两个功能:
- 1.解决内存可见性问题.
- 2.禁止指令重排序.
最终完全版:
class SingletonLazy{
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
synchronized (SingletonLazy.class) {
if (instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}