定义:
所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。在Java中,一般用在工具类或者创建比较耗资源的对象上。
实现方式:
1:立即加载/饿汉模式
优缺点:
1:线程安全
2:没有调用方法前就被加载,会占用内存
public class HungryMan {
private static HungryMan hungryMan = new HungryMan();
private HungryMan(){
}
public static HungryMan getInstance(){
return hungryMan;
}
}
2:延迟加载/懒汉模式
优缺点:
1:只有调用方法才创建对象,不会占用内存
2:非线程安全
public class LazyMan {
private static LazyMan lazyMan;
public static LazyMan getInatance(){
if(lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
3:双检查锁机制
优点:
1:可以确保线程安全
基本概念:
双检查:第一次检查变量是否被初始化,此时不去获取锁,如果已经被初始化则直接用,如果没有被初始化,则获取锁,第二次检查变量是否被初始化,如果其他线程曾获取过锁,那么变量被初始化,返回初始化变量。否则,初始化并返回对象
volatile作用:在doubleCheck = new DoubleCheck()过程中,可以分解为1、分配对象的内存空间。2、初始化对象。3、设置instance指向刚分配的内存空间。其中第二步和第三步在某些编译器编译时可能出现重排序(主要为了代码优化),即1、3、2的顺序。单线程下执行的时序图如下:
多线程下执行时序图:
由于单线程中遵守intra-thread semantics,从而能保证即使②和③交换顺序后其最终结果不变。但是当在多线程情况下,线程B将看到一个还没有被初始化的对象,此时将会出现问题。解决方案是不允许②和③进行重排序,使用volatile
public class DoubleCheck {
private static volatile DoubleCheck doubleCheck;
private DoubleCheck(){};
public static DoubleCheck getInstance(){
if(doubleCheck==null){
synchronized(DoubleCheck.class){
if(doubleCheck==null){
doubleCheck = new DoubleCheck();
}
}
}
return doubleCheck;
}
}
4:静态内部类实现
优点:
1:可以确保线程安全
public class StaticInnerClass {
private StaticInnerClass(){};
private static class InnerClass{
private static StaticInnerClass staticInnerClass = new StaticInnerClass();
}
public static StaticInnerClass getInatance(){
return InnerClass.staticInnerClass;
}
}
5:枚举实现单例(推荐)
优点:
1:线程安全。
2:天生保证序列化单例。
public enum EnumType {
INSTANCE;
private String name;
public String getName(){
name = new String("李白");
return name;
}
public static String getInstance(){
return EnumType.INSTANCE.getName();
}
}
原理:枚举是一种语法糖,它是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是但是更方便程序员使用。只是在编译器上做了手脚,却没有提供对应的指令集来处理它。
拿枚举来说,其实Enum就是一个普通的类,它继承自java.lang.Enum类。
public enum DataSourceEnum {
DATASOURCE;
}
把上面枚举编译后的字节码反编译,得到的代码如下:
public final class DataSourceEnum extends Enum<DataSourceEnum> {
public static final DataSourceEnum DATASOURCE;
public static DataSourceEnum[] values();
public static DataSourceEnum valueOf(String s);
static {};
}
由反编译后的代码可知,DATASOURCE 被声明为 static 的,根据底下的总结可以知道虚拟机会保证一个类的构造方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。
接下来看看序列化问题:
Java规范中规定:每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。也就是说,以下面枚举为例,序列化的时候将 DATASOURCE 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
public enum DataSourceEnum {
DATASOURCE;
}
由此可知,枚举天生保证序列化单例。
6:序列化和反序列化
7:使用static代码块实现单例
优点:
1: 线程安全,因为该对象只在加载类之前就被初始化了
public class DaimaKuai{
private static DaimaKuai daimaKuai=null;
private DaimaKuai(){
}
static{
daimaKuai=new DaimaKuai();
}
public static DaimaKuai getInstance(){
return daimaKuai;
}
}
总结:在介绍饿汉式时我们大多会说这种实现是线程安全的,实例在类加载时实例化,有JVM保证线程安全。虚拟机是怎么保证饿汉式实现的线程安全?
首先,hungryMan 作为类成员变量的实例化发生在类HungryMan 类加载的初始化阶段,初始化阶段是执行类构造器<clinit>() 方法的过程。
<clinit>() 方法是由编译器自动收集类中的所有类变量(static)的赋值动作和静态语句块(static{})块中的语句合并产生的。因此,private static HungryMan hungryMan = new HungryMan();也会被放入到这个方法中。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。