设计模式--单例模式(懒汉、饿汉)
单例模式
- 掌握五种单例模式的实现方式
- 理解为何 DCL 实现时要使用 volatile 修饰静态变量
- 了解 jdk 中用到单例的场景
一、饿汉式
public class Singleton1 implements Serializable {//实现了Serializable(转成字节保存在磁盘或是用于网络传输,都要实现它)
//1.构造私有
private Singleton1() {
System.out.println("初始化啦");
}
//2.静态的成员变量,类型为这个单例类型
private static final Singleton1 INSTANCE = new Singleton1();//内部才能new
//3.静态方法,返回实例
public static Singleton1 getInstance() {
return INSTANCE;
}
//4.用于测试对象在类初始化时便创建了(在测试类先调用这个方法,在getInstance)
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
反射破坏单例预防
//在refection方法中传入单例类,可以通过反射得到私有构造器并执行,得到新的对象,不再是单例
private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println("反射创建实例:" + constructor.newInstance());
}
解决方法,在私有构造器中添加代码
if (INSTANCE != null) { throw new RuntimeException("单例对象不能重复创建"); }
反序列化破坏单例预防
//此方法参数为单例对象
private static void serializable(Object instance) throws IOException, ClassNotFoundException {
//先把对象变成字节流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance);
//再把字节流还原成对象,造出的新对象不走构造方法的
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化创建实例:" + ois.readObject());
}
解决方法
在单例类中添加犯法readResolve方法,返回Instance
详细讲解参考这篇文章
完整版实现
public class Singleton1 implements Serializable {//实现了Serializable(转成字节保存在磁盘或是用于网络传输,都要实现它)
//1.构造私有
private Singleton1() {
System.out.println("初始化啦");
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("private Singleton1()");
}
//2.静态的成员变量,类型为这个单例类型
private static final Singleton1 INSTANCE = new Singleton1();//内部才能new
//3.静态方法,返回实例
public static Singleton1 getInstance() {
return INSTANCE;
}
//4.用于测试对象在类初始化时便创建了
public static void otherMethod() {
System.out.println("otherMethod()");
}
//5.预防反序列化的破坏
public Object readResolve() {
return INSTANCE;
}
}
二、枚举饿汉式
枚举类
enum Sex {
MALE, FEMALE;
}
//编译之后的样子
/*final class Sex extends Enum<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private Sex(String name, int ordinal) {
super(name, ordinal);
}
//类一加载/初始化,这两个对象便被创建出来
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = values();
}
private static final Sex[] $VALUES;
private static Sex[] $values() {
return new Sex[]{MALE, FEMALE};
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String value) {
return Enum.valueOf(Sex.class, value);
}
}*/
实现
public enum Singleton2 {
INSTANCE;
//枚举类的构造默认就是private,不写也可
private Singleton2() {
System.out.println("private Singleton2()");
}
@Override
//重写tostring,打印下hashcode
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//不提供这个方法也能用,因为枚举变量时公共的(直接Singleton2.INSTANCE)
public static Singleton2 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
- 枚举饿汉式能天然防止反射、反序列化破坏单例(内部已经做好预防)
三、懒汉式
public class Singleton3 implements Serializable {
private Singleton3() {
System.out.println("private Singleton3()");
}
private static Singleton3 INSTANCE = null;
// 静态方法,Singleton3.class作为锁对象
public static synchronized Singleton3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
懒汉式调用otherMethod方法时,无事发生,构造器并未执行
弊端
其实只有首次创建单例对象时才需要同步,之后的线程在进入方法都是直接返回第一个线程创建好的对象;但该代码实际上每次调用都会同步
我们希望只有首次创建创建单例对象才调用,后续在调用方法便不要synchronized的保护
四、双检锁懒汉式
两次检查null,=>双检
public class Singleton4 implements Serializable {
private Singleton4() {
System.out.println("private Singleton4()");
}
private static volatile Singleton4 INSTANCE = null; //volatile关键字 解决共享变量的可见性,有序性
public static Singleton4 getInstance() {
//第一个if判断时提高性能的,只有首次创建单例对象时才需要同步,后面直接返回第一个线程创建好的对象
if (INSTANCE == null) {
synchronized (Singleton4.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
为何必须加 volatile:
反编译这个单例类,观察getInstance方法
如果先执行给INSTANCE赋值(即给他一个引用地址,指向单例对象,此时单例对象还没有完成赋值),INSTANCE不为bull,变直接执行return了,但这是个没有完成构造赋值的对象,问题便出现了
-
INSTANCE = new Singleton4()
不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造(单线程下没影响,多线程可能会) -
如果线程1 先执行了赋值,线程2 执行到第一个
INSTANCE == null
时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象
volatile修饰的变量,赋值的时候,会在变量的赋值语句之后加上一个内存屏障;以阻止之前的赋值语句越过屏障,在他之后(简单来说就是防止指令重排序优化)
思考
饿汉式为何不需要考虑多线程下对象(重复)创建的问题
private static final Singleton1 INSTANCE = new Singleton1();
- 饿汉式对象创建是赋值给了静态变量(这个操作最终会放在静态代码块里执行,jvm虚拟机会保证静态代码块执行的线程安全)
- 枚举饿汉式中枚举变量的创建也是在静态代码块中执行的
五、内部类懒汉式
根据把对象的创建放入静态代码块便是线程安全的思路,得到这种方式
避免了双检锁的缺点
public class Singleton5 implements Serializable {
private Singleton5() {
System.out.println("private Singleton5()");
}
//静态内部类,可以访问外部的私有变量
//没用到内部类时不会触发类的加载、链接、初始化
private static class Holder {
//内部类的静态变量(这个操作最终会放在静态代码块里执行,jvm虚拟机会保证静态代码块执行的线程安全)
//只执行一次
static Singleton5 INSTANCE = new Singleton5();
}
//
public static Singleton5 getInstance() {
return Holder.INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
六、JDK 中单例的体现
-
Runtime 体现了饿汉式单例
-
Console 体现了双检锁懒汉式单例
-
Collections 中的 EmptyNavigableSet 内部类懒汉式单例
-
ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
-
Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例