本文在CS-Notes的单例模式基础上添加了部分个人理解,方便自己复习。
安利CS-Notes,涵盖所有计算机基础课程的知识点总结笔记。
单例
一个类只有一个实例
五种Java实现方式
- 懒汉模式 - 线程不安全
最基础的一种方式,判断单例有没有创建,没有创建就去new一个
public class Singleton{
private static Singleton instance; // 因为要在静态方法中使用,所以加static关键字
private Singleton(){} // 实现单例模式首先要让构造函数私有
// 声明为静态方法是让其他类可以不创建实例就访问,形如Singleton.getInstance()
// 要通过这个方法才能创建实例,当然要提供不创建实例就能访问的接口
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
单线程情况下此方法没有问题。但如果是多线程,如果一个线程进入instance == null内去new一个实例,在还没有完全创建成功的情况下,另一个线程调用getInstace()方法, 此时实例仍旧为null,此时这个线程也会创建实例。
注意:instance属性加static关键字,构造函数私有,获取实例方法加static关键字,以上三点是所有单例模式实现方式通用的
- 饿汉模式 - 线程安全
如名字所示,饿汉模式是一开始就创建实例。
public class Singeleton{
private static Singleton instace = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这样做虽然线程安全,但是不能延迟实例化,浪费系统资源。
- 懒汉模式 - 线程安全
第一种懒汉模式导致线程不安全的原因是会有多个线程进入创建实例的代码块,借助Java的锁机制即可实现同时只有一个线程能够进入创建实例的代码块。
pubic class Singleton{
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
乍一看与第一种懒汉模式几乎没差,区别只在于synchronized关键字,使用这个关键字会使同一时间只有一个线程能够进入该方法,这样即可保证只创建一个实例,其他线程进入后instance!=null,也就不会再创建。
但是这种方式也有缺点,即使在实例被创建后,也只有一个线程能够进入方法,其他线程会被阻塞,直到当前线程获取实例并退出方法。这导致了不必要的阻塞,实例创建后的获取使不需要控制互斥的,效率太低。
- 双重锁校验 - 线程安全
上一种方法的弊端在于 实例创建后仍旧需要互斥访问,双重锁校验的改进在于只在创建实例时加锁,而获取实例不加控制。
public class Singleton{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
双重锁的双是指加在instance前的volatile和创建实例前的synchronized。
加volatile关键字的原因:volatile有个很重要的作用是禁止指令重排,instance=new Singleton()具体执行要分以下三步:
1.为instance分配空间
2.初始化
3.instance指向分配的内存空间
如果不禁止指令重排,可能执行了1 3,但 2还没执行,此时其他线程判断instance显然不是null,因为已经给它分配内存空间了,但instance并没有被初始化,操作instance会抛出错误。
synchronized之后再次判断instance==null的原因:
如果两个线程都通过第一个判断,而一个线程进入了synchronized,另一个则会因此阻塞,当第一个线程退出函数后,被阻塞的线程会被唤醒,继续执行之前的任务,即synchronized之后的语句,如果不再次判断,则仍旧会创建多次。
- 静态内部类实现
此方式借助JVM特性,类被加载时,静态内部类不会被加载入内存,只有真正调用时才会加载。
public class Singleton{
private Singleton(){}
private static class SingletonHolder{
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
除此之外,还有一种枚举类的实现方式,可以防止通过反射修改私有构造函数为公有,因为这一部分我还不是很懂,所以这里就不介绍了。
以上内容其实涉及很多知识点,static、volatile和synchronized关键字的作用,类加载顺序,先挖个坑,以后来填。
以上。