简单来说就是一个类只能构建一个对象的设计模式

第一种不加锁的单例

线程不安全

public class Singleton {
    private Singleton() {}  //私有构造函数
    private static Singleton instance = null;  
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

饿汉式

在类被初始化的时候就已经在内存中创建了对象,以空间换时间,不存在线程安全问题。

public class SingleTon{
    private static SingleTon INSTANCE = new SingleTon();
    private SingleTon(){}
    public static SingleTon getInstance(){
        return INSTANCE;
    }
}

懒汉式

在方法被调起后才创建对象,以时间换空间,在多线程环境下存在风险。


public class SingleTon{
   private static SingleTon  INSTANCE = null;
   private SingleTon(){}
   public static SingleTon getInstance() {  
   if(INSTANCE == null){
      INSTANCE = new SingleTon(); 
    } 
    return INSTANCE;
  }
  }

简单加上synchronized锁

双重检测(double check lock)是为了b线程在a线程刚结束的临界点执行锁内任务,存在DCL失效问题

public class Singleton {
    private Singleton() {}  //私有构造函数
   private static Singleton instance = null;  //单例对象
   //静态工厂方法
   public static Singleton getInstance() {
        if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
               }
            }
         }
        return instance;
    }
}

以上实现还是有问题 ,单例第三种写法

JVM编译器有个指令重排的概念
* 什么是指令重排
java一个简单的instance = new instance()会被jvm编译成如下jvm指令

1. memory  = allocate;分配对象的内存地址
2. creatinstance(memory);初始化对象
3. instance = memory设置instance指向刚分配的内存地址

但是这些指令顺序并非是一重不变,可能会经过jvm和cpu的优化,进行重排序

1.分配对象内存地址
3.设置instance指向刚分配的内存地址
2.初始化对象

当线程A执行完13的时候,线程B抢到资源,返回了一个还没有初始化完成的对象 * 如何避免以上问题?
在instance对象前面增加一个volatile修饰符

public class Singleton {
    private Singleton() {}  //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
          if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}

volatile阻止了变量访问访问前后指令重排序,对于线程B来说,instance要么指向null,要么指向一个完整的instance,而不会出现中间状态

不仅可以防止指令重排,也可以保证线程访问的变量值是主内存种最新值,jdk1.6及以后才支持,每次均从主内存中读取,牺牲点效率,无伤大雅。

静态内部类

静态内部类只会被加载一次,首次调用,顾线程安全,类加载初始化阶段是单线程的同时延迟了初始化。

类加载时机

1. new,involk static,putstatic,getstatic,指令时若类未加载则触发
2. 反射使用某个类时,若类未加载则触发
3. 子类加载时若父类未加载则触发
4. 程序开始时主方法所在类会被加载

静态内部类的懒加载应该属于第一种情况,为什么外部类加载时,内部类未加载,静态内部类只是刚好写在了另一个类里面,实际上和外部类没什么附属关系。

public class Singleton{
private SingleTon (){}
private static class Holder{
//这里的私有没有什么意义
    private static SingleTon instance = new Singleton();
} 

    public static Singleton getInstance(){
        return Holder.instance
    }
}

如何通过反射打破单例模式,只能构建一个对象

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));

如何防止反射?
使用枚举实现单例

public enum SingletonEnum {
    INSTANCE;
}

使用枚举单例,不仅可以防止反序列化,而且可以保证枚举对象被反序列化的时候,返回的对象是同一个对象

public enum SingletonEnum {
    INSTANCE;
    public void doSomething(){
        System.out.printLn("do something");
    }
}
public enum SingletonEnum{
    PERSON;
    private Person person = null;
    private SingletonEnum(){
        person = new Person();
    }
    public Person getPerson(){
        return person;
    }

}

public class Person{

}

防止内存泄漏单例

public class A8PinPadManage {


    private A8PinPadManage(){} //私有构造函数
    private volatile static Pinpad instance; //单例对象

    public static Pinpad getInstance(Context context) {
        WeakReference<Context> contextWeakReference = new WeakReference<>(context);
        Context weakReferenceContext = contextWeakReference.get();

        if (instance == null) {
            synchronized (A8PinPadManage.class) {
                if (instance == null) {
                    instance = new Pinpad((int) SharedPrefsUtil.get(weakReferenceContext, CIL_KAP_ID, 2), "IPP");
                }
            }
        }
        return instance;
    }
}
  • 解读
    • 私有构造函数,全局只能构建一个对象,不能让随便的就去new,所以要私有
    • 懒汉,单例刚开始没有构建,调用才构建
    • 饿汉,调用new singleton主动构建,不需要判空
    • instance的初始值可以写成null或者new Singleton()
    • 不加锁,线程不安全
    • 不加双重验证直接锁getinstance方法,可以,增加内存开销,只用synchronize里面的判空,增加开销

参考 小灰漫画

https://www.zhihu.com/search?type=content&q=%E5%8D%95%E4%BE%8B