目录
饿汉式
/**
* 懒汉式 ---> 只要类一加载就会创建实例
*/
public class Singleton1 {
//1.单例的构造必须私有
private Singleton1(){
System.out.println("private Singleton1");
}
//2.这个实例变量要用static和final修饰 【给静态变量进行赋值】是线程安全的,【静态代码块】的执行由JVM保证它的线程安全
private static final Singleton1 INSTANCE = new Singleton1();
//3.【静态变量】一般都是私有的,私有的外界是不能直接访问,所以一般会提供一个公共的静态方法给外界来访问
public static Singleton1 getInstance(){
return INSTANCE;
}
}
注意:
-
如果饿汉式的单例类实现了Serializable接口,那么这个单例是可以被反序列化破坏的。
-
//重写readResolve,在反序列化的时候发现你重写了readResolve方法,那么就会把readResolve方法中的返回值作为反序列化的返回结果 public Object readResolve(){ return INSTANCE; }
-
私有的构造方法还可以被人用反射破坏 ---> 这个可以在构造器中加入代码判断来进行避免
private Singleton1(){
if (INSTANCE != null){ //用来避免反射破坏单例,因为反射是通过再次调用这个构造器来创建新的对象实例的,所以我们可以提前做好判断,如果实例已经存在,那么构造方法就不能再被调用了(因为我们的实例是用final和【static】修饰的,所以在类初始化的时候这个实例就会完成赋值,后面你再通过反射来调构造器就抛异常不让你调用了)
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("private Singleton1");
}
-
使用Unsafe类破坏单例
注意:使用这种方式破坏单例,我们预防不了。
枚举实现饿汉式单例
枚举类是一种语法糖的写法,是编译器在帮我们实现这个单例。
枚举类中声明的变量在编译的时候编译器会帮我们把它变成static,final修饰的变量。
/**
* 枚举实现的单例是饿汉式的
*/
public enum Singleton2 {
INSTANCE;
public static Singleton2 getInstance(){
return INSTANCE;
}
}
好处:
-
可以防止反序列化来破坏单例(因为反序列化的时候ObjectInputStream 会对枚举做特殊的处理,帮我们直接把枚举中的单例直接返回).
-
可以避免反射来破坏单例。(使用反射破坏枚举会抛异常)
-
但是不能防止unsafe来破坏这个单例。
懒汉式
/**
* 懒汉式创建单例
*/
public class Singleton3 {
private Singleton3(){
}
private static Singleton3 INSTANCE = null; //没有final修饰,多线程会不安全
//该方法可以在并发环境中被多个线程调,用为了保证在并发环境中保证单例的安全,所以在该方法加synchronized,
//但是直接在方法上加锁,会导致线程每次来都要阻塞
public static synchronized Singleton3 getInstance(){
if (INSTANCE==null){
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
双检索懒汉式
这是对懒汉式的优化。
public class Singleton4 {
private Singleton4(){
}
private static volatile Singleton4 INSTANCE = null; //为了保证有序性,因为在执行INSTANCE = new Singleton4();这行代码的时候其对应的字节码指令一共有4步(new,dup,invokespecial,putstatic),而new,dup这两步是有因果关系的所以这两步的顺序是确定的,但是invokespecial,putstatic都是赋值操作并且没有因果关系,所以这两步在CPU看来先执行谁都一样,所以就会导致指令重排,在单线程情况下这两步发生顺序调换是不会产生线程安全的,但是在多线程的情况下就会出现:获取到的实例是未完全初始化的实例(未经过构造器初始化赋值的实例)。
public static Singleton4 getInstance(){
if (INSTANCE==null){//加锁之前先判断一次,可以尽可能的避免线程进入synchronized代码块
synchronized (Singleton4.class){
if (INSTANCE==null){//在最开始的时候实例还没有创建好,但是【有多个线程同时进入了第一个if】,那么进入这个synchronized代码块的线程肯定有先后了,先进来的已经创建了实例,那么后进来这个synchronized的当然不需要再创建实例了
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
}
静态内部类实现懒汉单例
我们可以发现饿汉式创建单例是不用考虑线程安全的,我们把饿汉式和懒汉式进行对比,发现主要的原因是实例对象的赋值是不是在静态代码块中完成,饿汉式完成对象的赋值操作是在静态代码块中完成的,静态代码块的线程安全是由JVM帮我们保证的。 所以我们只要保证实例的赋值是在静态代码块中完成,就可以保证线程安全。所以静态内部类创建单例就出现了。
注意:
-
静态内部类是可以访问外部类的所有变量的。
-
类只有在第一次使用它,它才会加载。所以我们可以利用这个特性间接的控制静态内部类的加载时机。
public class Singleton5 {
private Singleton5(){}
public static Singleton5 getInstance(){
return Holder.INSTANCE;
}
//创建静态内部类,只有在调用getInstance的时候才会加载这个内部类
public static class Holder{
static Singleton5 INSTANCE = new Singleton5(); //使用静态代码来保证线程安全
}
}
单例在jdk的体现
注意:自己在项目中不要随便使用单例模式,是非常容易出错误的。如果面试官问你在哪里用到单例模式,最好不要说在项目中使用到了单例! 说jdk中的库就行。
Runtime类
Runtime类就是使用的单例,其实我们在很多地方都用过,比如使用system的exit方法的时候,使用垃圾回收的gc方法的时候,调用的也是这个Runtime。
system中的Console对象
system中的Console对象: 懒汉式的双检索单例。
collections中的REVERSE_ORDER
collections类中的reverseOrder方法(反序比较方法)中的 REVERSE_ORDER 也是单例的:
Comparators中的枚举单例
在Comparators中的使用到了枚举实现单例。