确保某一个类只有一个实例,并且提供一个全局访问点。
3.详解常见的单例模式实现方式有五种:饿汉式、懒汉式、双重检测锁式、静态内部类式和枚举单例。而在这五种方式中饿汉式和懒汉式又最为常见。
饿汉式:线程安全,调用效率高。但是不能延时加载。示例:
public class Hungry {
private static Hungry hungry = new Hungry();
private Hungry(){}
public static Hungry getInstance(){
return hungry;
}
}
饿汉式顾名思义随着类加载的时候 就已经创建对象了 ,因为饿,所以急不可耐的就吃了。。。
由于该模式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,且是线程安全的
懒汉式(双重同步锁)
public class Lazy {
private static volatile Lazy lazy = null;
private Lazy() {
}
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}
如果不加锁的情况下懒汉式的单例模式是线程不安全的,所以需要加锁
为什么要使用volatile关键字?
因为java初始化时有可能会进行指令重排
指令重排:
一般而言初始化操作并不是一个原子操作,而是分为三步:在堆中开辟对象所需空间,分配地址根据类加载的初始化顺序进行初始化将内存地址返回给栈中的引用变量
由于 Java 内存模型允许“无序写入”,有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了
在堆中开辟对象所需空间,分配地址将内存地址返回给栈中的引用变量(此时变量已不在为null,但是变量却并没有初始化完成)根据类加载的初始化顺序进行初始化
会出现以下情况:
| 线程一 | 线程二 |
| 第一次判空为true | |
| 获取锁 | |
| 再次判空还为true | |
| 1.在堆中开辟对象所需空间,分配地址 | |
| 3.将内存地址返回给栈中的引用变量(此时变量已不在为null,但是变量却并没有初始化完成) | 此时线程二来进行第一次判空为false |
| 2.根据类加载的初始化顺序进行初始化 | 然后来使用该对象,此时对象还未初始化完成 |
加入volatile关键字修饰之后,会禁用指令重排,这样就保证了线程同步。
由于剩下的几种实现方式暂没有接触过,不做举例。
注:注意单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。 (这句话表述的有点问题,单例类一般情况只想内部保留一个实例对象,所以会选择将构造函数声明为私有的,这才使得单例类无法被继承。单例类与继承没有强关联关系。)
类加载的初始化顺序:
父类静态属性->父类静态代码块->子类静态属性->子类静态代码块->父类普通属性->父类代码块->父类构造方法->子类普通属性->子类代码块->子类构造方法



