/*
* 单例类,只能实例化一个对象(懒汉式)
*/
public class Danli {
private static Danli dan; //静态
private Danli(){}; //构造器私有,不让外界创建对象
public static Danli getObj(){
if(dan==null){
dan = new Danli(); //如果第一次,实例化
}
return dan; //否则直接返回已存在的静态对象
}
}
/*
* 单例类,只能实例化一个对象(饿汉式)
*/
public class Danli {
private static Danli dan = new Danli(); //静态
private Danli(){}; //构造器私有,不让外界创建对象
public static Danli getObj(){
return dan;
}
}
public class Main {
public static void main(String[] args){
//Danli a = new Danli(); 报错,构造函数不可视
Danli d = Danli.getObj();
Danli d1 = Danli.getObj();
System.out.println(d1==d); //true
}
}
但上面这个单利模式并不是线程安全的。所以我们需要一种方式实现线程安全。
public class Danli {
private volatile static Danli danli;
private Danli(){}
public Danli getInstace(){
if(danli == null){
synchronized(Danli.class){
if(danli == null){
danli = new Danli();
}
}
}
return danli;
}
}
首先我们解释上面为什么要使用volatile关键字,这是因为:创建对象分为三个步骤
1. 分配内存 2. 初始化对象 3. 将 内存空间地址赋给对应得引用
但是由于编译器指令重排序的存在,上面的步骤可能会变成1,3,2,这样的话另一个线程在此线程执行完1,3,后,另一个线程执行,此时虽然为初始化对象,但指向对象的引用已经不为null,所以另一个线程进入第一个检测,就会直接返回未被初始化完成的对象,而造成程序的错误。使用volatile可以确保上面的三个指令按顺序1,2,3执行,而不会出现重排序,从而保证了返回安全的对象。
再解释为什么需要第二次检查未null。
设想一种情况,一个线程此时已进入同步代码块,还没有开始创建对象,而此时另一个线程也进行第一次的null判断,为null,进入,此时进入已进入同步代码块的线程执行创建对象操作退出代码块,这里如果没有同步代码块的第二次检测,则可以直接进来创建对象此时进来的线程再次创建一个对象,就有了两个对象,所以出错了。所以这就是第二次为什么还要进行null的判断
https://blog.csdn.net/xiakepan/article/details/52444565
枚举实现单例模式
/*
* 需要实现的单例类
*/
public class Student {
}
/*
* 枚举类
*/
public enum EnumSingleton {
INSTANCE;
private Student student = null;
private EnumSingleton(){
student = new Student();
}
public Student getInstance(){
return student;
}
}
/*
* 测试类
*/
public class test {
public static void main(String[] args) {
Student e = EnumSingleton.INSTANCE.getInstance();
Student e1 = EnumSingleton.INSTANCE.getInstance();
System.out.println(e == e1); // true
}
}
上述代码,我们其实已经用枚举实现了一个线程安全的单例模式了,上面并没有加锁的操作,这是如何实现线程安全的呢?
我们如果反编译上面枚举类编译后的字节码,就会看到INSTANCE是声明为static的,而又因为虚拟机会保证一个类的<clinit
>()方法在多线程中被正确地加锁、同步(如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的<clinit>()方法,其他的线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕,需要注意的是,其他的线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次)。
<init>方法和类构造器<clinit>方法的区别
1. 执行时机不同 init是实例构造器方法,也就是说在程序执行new一个对象调用该对象的构造器方法时才会执行init方法,而clinit是类构造器方法,也就是jvm进行类加载-验证-解析-初始化,中的初始化阶段jvm会调用clinit方法。
2. 执行目的不同 init是实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。
<clinit>()方法的执行
—— <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
—— <clinit>()方法与实例构造器<init>()方法不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
—— 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
—— <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
—— 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法,但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
—— 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的<clinit>()方法,其他的线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。(需要注意的是,其他的线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他的线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次)。
静态内部类实现单例
public class Demo1 {
//私有构造器
private Demo1(){
}
//静态内部类
private static class Stu{
private static final Demo1 d = new Demo1();
}
public static Demo1 getInstance(){
return Stu.d;
}
}
静态内部类可以实现延迟加载,只有在调用的时候才会加载并分配内存。
什么情况下开始加载类?
Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
初始化阶段,虚拟机规范规定有且仅有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类还没被初始化,就需要先初始化。这4种指令最常见Java场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候、调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则先需要触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则先需要触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
- 当时用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
名称 | 优点 | 缺点 | 备注 |
---|---|---|---|
简单模式 | 实现简单 | 线程不安全 | |
饿汉模式 | 线程安全 | 内存消耗太大 | |
懒汉模式 | 线程安全 | 同步方法消耗比较大 | |
DCL模式 | 线程安全,节省内存 | jdk版本受限、高并发会导致DCL失效 | 推荐使用 |
静态内部类模式 | 线程安全、节省内存 | 实现比较麻烦 | 推荐使用 |
枚举模式 | 线程安全、支持反序列化 | 个人感觉比较怪异 | |