1.懒汉式加载,最简单的单例模式,只需要2两步,
a.把自己的构造方法设置为私有的,不让别人访问你的实例,b.提供一个static方法给别人获取你的实例
public class Singleton{
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
该方式,在单线程情况下是没问题的,但是在多线程情况下还是会创建多个实例,测试代码如下:
public class SingletonTest {
public static void main(String[] args) throws InterruptedException {
Set<Singleton> set = new HashSet<>();
Set<Singleton> singletonSet = Collections.synchronizedSet(set);
for (int i = 0; i <1000 ; i++) {
new Thread(()->{
singletonSet.add(Singleton.getInstance());
}).start();
}
Thread.sleep(10000);
for (Singleton singleton : singletonSet) {
System.out.println(singleton);
}
}
}
为什么会这样呢?我们假设第一个线程进入getInstance方法,判断实例为null,准备进入if块内执行实例化,这时线程突然让出时间片,第二个线程也进入方法,判断实例也为null,并且进入if块执行实例化,第一个线程唤醒也进入if块进行实例化。这时就会出现2个实例。所以出现了bug
2.饿汉式加载,即在jvm加载这个类时,就进行实例的初始化,当调用getInstance()时直接返回;
public class Singleton{
private static final Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
}
这种方式是能保证单例,但缺点是,当你没有用到这个实例时,该实例已经被加载了,如果该单例很大的话,将浪费很多的内存。
3.synchronized 同步式,同步静态getInstance()方法;
public class Singleton{
private static Singleton singleton;
private Singleton(){
}
//同步静态方法
public synchronized static Singleton getInstance(){
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
}
这种方式是能够保证线程安全和单例,但是在多线程情况下,每个线程进入该方法都要阻塞排队等待,当实例已经初始化之后,还需要做同步控制吗?显然这种方式,对性能的影响是很大的;
4.双重锁验证
public class Singleton{
private static volatile Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
//多线程直接访问,不做控制,不影响性能,
if(singleton==null){
//此时,如果有多个线程进入,则进入同步块,其余线程等待
synchronized(this){
//此时,第一个线程进来判断为null,但是第二个线程进来已经不是null了
if(singleton==null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
首先看getInstance方法,我们在方法声明上去除了synchronized关键字,多线程进入方法内部,判断是否为null,如果为null,多个线程同时进入if块内,此时,我们是用Singleton Class对象同步一段方法。保证只有一个线程进入该方法。并且判断是否为null,如果为null,就进行初始化。我们想象一下,如果第一个线程进入进入同步块,发现该实例为null,于是进入if块实例化,第二个线程进入同步内则发现实例已经不是null,直接就返回 了,从而保证了并发安全。那么这个和第三种方式又什么区别呢?第三种方式的缺陷是:每个线程每次进入该方法都需要被同步,成本巨大。而第四种方式呢?每个线程最多只有在第一次的时候才会进入同步块,也就是说,只要实例被初始化了,那么之后进入该方法的线程就不必进入同步块了。就解决并发下线程安全和性能的平衡。虽然第一次还是会被阻塞。但相比较于第三种,已经好多了;为什么要使用volatile关键字呢?
首先我们看,Java虚拟机初始化一个对象都干了些什么?总的来说,3件事情:
-
在堆空间分配内存
-
执行构造方法进行初始化
-
将对象指向内存中分配的内存空间,也就是地址
但是由于当我们编译的时候,编译器在生成汇编代码的时候会对流程进行优化(这里涉及到happen-before原则和Java内存模型和CPU流水线执行的知识,就不展开讲了),优化的结果式有可能式123顺序执行,也有可能式132执行,但是,如果是按照132的顺序执行,走到第三步(还没到第二步)的时候,这时突然另一个线程来访问,走到if(Singleton == null)块,会发现Singleton 已经不是null了,就直接返回了,但是此时对象还没有完成初始化,如果另一个线程对实例的某些需要初始化的参数进行操作,就有可能报错。使用volatile关键字,能够告诉编译器不要对代码进行重排序的优化。就不会出现这种问题了。
5.静态内部类实现,即是懒汉式加载,也能够保证线程安全
public class Singleton {
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance3(){
return SingletonFactory.singleton;
}
//使用静态内部类
private static class SingletonFactory{
private static Singleton singleton = new Singleton();
}
}
引入了内部类的方式,虚拟机的机制是,如果你没有访问一个类,那么是不会载入该类进入虚拟机的。当我们使用外部类的时候其他属性的时候,是不会浪费内存载入内部类中的单例的,而也就保证了并发安全和防止内存浪费。
6.由于序列化和反序列化会破坏单例,因此对于实现Serializable或Externalizable的单例,需要定义一个readResolve方法
public class Singleton implements Serializable{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance3(){
return SingletonFactory.singleton;
}
private static class SingletonFactory{
private static Singleton singleton = new Singleton();
}
public Object readResolve(){
return SingletonFactory.singleton;
}
}
重写readResolve() 方法,防止反序列化破坏单例机制,这是因为:反序列化的机制在反序列化的时候,会判断如果实现了serializable或者externalizable接口的类中包含readResolve方法的话,会直接调用readResolve方法来获取实例。这样我们就制止了反序列化破坏我们的单例模式。
7.使用枚举
public enmu Singleton{
SINGLETON;
//获取实例
public Singleton getInstance(){
return SINGLETON;
}
}
为什么使用枚举可以呢?枚举类型反编译之后可以看到实际上是一个继承自Enum的类。所以本质还是一个类。 因为枚举的特点,你只会有一个实例。