Java单例的实现和分析

1.基本概念

目的: 保证类在内存中只有一个对象,可以直接访问,不需要实例化该类的对象
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项: getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

2.代码实现

2.1.饿汉式(即时创建对象)

这种方式比较常用,但容易产生垃圾对象。空间时间
是否多线程安全:
实现难度:
优点: 没有加锁,执行效率会提高。
缺点: 类加载时就初始化,浪费内存。

public class Main {
    public static void main(String[] args) {
        //通过方法获得唯一对象
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //判断是否为唯一对象
        System.out.println(s1 == s2);//运行结果为true
    }
}
class Singleton {
    //1.私有构造方法,其他类不能访问该构造方法
    private Singleton() {}
    //2.创建本类对象
    private static Singleton s = new Singleton();
    //3.对外提供公共的访问方法
    public static Singleton getInstance() {//获取实例
        return s;
    }
}

2.2.懒汉式(使用时才创建对象)

2.2.1.线程不安全的懒汉式

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式,不要求线程安全,在多线程不能正常工作。
是否多线程安全:
实现难度:

public class Main {
    public static void main(String[] args) {
    	//通过方法获得唯一对象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
class SingletonLazy {
    //1.私有构造方法,其他类不能访问该构造方法
    private SingletonLazy() {}
    //2.声明一个引用
    private static SingletonLazy s;
    //3.对外提供公共的访问方法
    public static SingletonLazy  getInstance() {//获取实例
        //当没有对象时,进行创建
        if(s==null){
            s = new SingletonLazy();
        }
        return s;
    }
}

2.2.2.线程安全的懒汉式

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
是否多线程安全:
实现难度:
优点: 第一次调用才初始化,避免内存浪费。
缺点: 必须加锁 synchronized 才能保证单例,但加锁会影响效率。

public class Main {
    public static void main(String[] args) {
    	//通过方法获得唯一对象
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
class SingletonLazy {
    //1.私有构造方法,其他类不能访问该构造方法
    private SingletonLazy() {}
    //2.声明一个引用
    private static SingletonLazy s;
    //3.对外提供公共的访问方法(使用synchronized锁,防止线程抢占)
    public static synchronized SingletonLazy  getInstance() {//获取实例
        //当没有对象时,进行创建
        if(s==null){
            s = new SingletonLazy();
        }
        return s;
    }
}

2.3.final实现方式(了解)

通过final关键字,实现对象在创建之后不可被更改。

public class Main {
    public static void main(String[] args) {
		//获得唯一对象
        Singletonfinal s1 = Singletonfinal.s;
        Singletonfinal s2 = Singletonfinal.s;
        System.out.println(s1 == s2);//结果为true
    }
}
class Singletonfinal{
    //1.私有构造方法,其他类不能访问该构造方法
    private Singletonfinal() {}
    //2.声明一个引用,final表示了地址不可被修改
    public static final Singletonfinal s = new Singletonfinal();
}

3.双重校验锁(DCL,即double-checked locking)

3.1.DCL代码实现

是否 Lazy 初始化:
是否多线程安全:
实现难度: 较复杂
描述: 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。

public class Main {
    public static void main(String[] args) {
        SingletonDCL s1 = SingletonDCL.getInstance();
        SingletonDCL s2 = SingletonDCL.getInstance();
        System.out.println(s1 == s2);//结果是true
    }
}
class SingletonDCL {
    //1.构造私有方法
    private SingletonDCL() {}
    //2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
    private volatile static SingletonDCL singletonDCL;
    //3.提供外部获得实例的方法
    public static SingletonDCL getInstance() {
        //如果实例未创建,第一重
        if (singletonDCL == null)
            synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
                //加上锁后再次判断,第二重
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        return singletonDCL;//返回对象
    }
}

3.2.使用volatile的原因(禁止指令重排序)

指令重排序: 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
例如:

 singletonDCL = new SingletonDCL();//实例化对象这行代码

这行代码实际上可以分解成三个步骤:
1.分配内存空间。
2.初始化对象。
3.将对象指向刚分配的内存空间。

但是有些编译器因为性能的原因,可能会改变2和3的顺序,就成了:
1.分配内存空间。
2.将对象指向刚分配的内存空间。
3.初始化对象。

在不使用volatile且发生重排序的情况下,调用顺序如下

线程一 线程二
检查到singletonDCL为null -
获取锁 -
再次检查到singletonDCL为null -
为singletonDCL分配内存空间 -
将singletonDCL指向内存空间 -
- 检查到singletonDCL不为空
- 访问singletonDCL(此时线程一还未初始化完成对象)
初始化singletonDCL -

在这种情况下,在线程二访问singletonDCL时,访问的是一个初始化未完成的对象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章