单例模式
1.1模式定义
保证一个类只有一个实例,并且提供一个全局访问点。最重要的就是保证构造器私有。
1.2实现方式
1.2.1懒汉模式
public class LazySingleton {
private static LazySingleton instance;
//构造器私有
private LazySingleton(){
}
//静态方法返回对象
public static LazySingleton getInstance() {
if (instance == null){
instance = new LazySingleton();
}
return instance;
}
}
存在一个问题,如果是多线程环境下,会有可能出现实例化两个对象。因此可以通过加锁的方式解决.
public class LazySingleton {
private static LazySingleton instance;
//构造器私有
private LazySingleton(){
}
//加synchronized锁
public static synchronized LazySingleton getInstance() {
if (instance == null){
instance = new LazySingleton();
}
return instance;
}
}
加锁解决了多线程条件下实例化多个对象的问题,但是牺牲了效率。可以进行优化。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton(){
}
//双重检测锁模式
//1、检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回。2、获取锁。3、再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象
public static LazySingleton getInstance() {
if (instance == null){
synchronized (LazySingleton.class) {
if(instance == null){
instance = new LazySingleton();//java中new对象不是原子性操作
}
}
}
return instance;
}
}
但是这种模式下还存在问题,java的new操作并不是一个原子操作,大致分三步:1.在堆中开辟对象所需空间,分配内存地址、2.根据类加载的初始化顺序进行初始化、3.将内存地址返回给栈中的引用变量。如果发生指令重排,将后两步顺序颠倒,在多线程环境中就会发生使用未初始化的对象的问题。因此需要加volatile关键字,禁止指令重排。
1.2.2饿汉模式
本质上借助jvm类加载机制,保证实例的唯一性。但是比较浪费内存资源,因为一开始,无论是否需要都会加载。
//类加载过程
//1.加载二进制数据到内存中,生成对应的二进制的Class数据结构
//2.连接:a、验证 b、准备(给静态成员变量赋默认值)c、解析
//3.初始化:给静态变量赋初值
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
//构造器私有
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return instance;
}
}
关于饿汉模式中是否需要加final需要看情况而定,声明final的变量,必须在类加载完成时已经赋值。存在释放资源的情况下,如果需要重新使用这个单例,就必须存在重新初始化的过程,就不能加final,对于不需要释放资源的情况,可以加final。final static修饰的成员变量必须直接赋值或者在静态代码块中赋值。
1.2.3静态类部类
//结合了懒汉模式和饿汉模式的优点:不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
public class InnerClassSingleton {
//构造器私有
private InnerClassSingleton(){
}
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
首先,我们getInstance()函数并不直接new创建对象,而是取的是InnerClassHolder里的instance对象。所以无论多少线程一起调用getInstance函数都只会返回同一个单例。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建。那在这里又是怎么保证单例的呢?主要是虚拟机会保证一个类的
通过反射方式可以破坏单例模式,通过反射获得单例类的构造函数,由于该构造函数是private的,通过setAccessible(true)指示反射的对象在使用时应该取消 Java 语言访问检查,使得私有的构造函数能够被访问,这样使得单例模式失效。可以对实例化次数进行统计,当大于一次时就就抛出异常,(当然也不安全,可以通过反射修改这个值)。然后对构造函数加上synchronized关键字防止多线程情况下实例出多个对象。clone()不会破坏单例模式,虽然clone()方法,是直接从内存区copy一个对象,但是单例的类不能实现cloneable接口,普通对象直接调用clone()会抛出异常。
1.2.4枚举
- 天然不支持反射创建对应实例
单例类的修饰是abstract的,所以没法实例化。 - 自己的反序列化机制
枚举序列化的时候只将名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。 - 利用类加载机制保证线程安全
被声明为static,类加载时只会初始化一次.