Version: Next

单例模式

构造方法私有

饿汉式

  • 立即创建当前类的实例化对象
  • 将构造方法私有
class Hugry{
private Hugry(){}
private static final Hugry HUGRY = new Hugry();
public static Hugry getInstance() {
return HUGRY;
}
}

问题:不管对象是否真的被使用了,直接创建,有可能浪费资源

懒汉式

  • 将构造方法私有
  • 在调用getInstance()方法尝试获得对象实例时,才创建对象
class Lazy{
private Lazy(){}
private static Lazy LAZY;
public static Lazy getInstance(){
if (LAZY == null) {
LAZY = new Lazy();
}
return LAZY;
}
}

问题:多线程环境下,可能创建出多个对象

  • 测试
class Lazy{
private Lazy(){
System.out.println(Thread.currentThread().getName() + "创建了一个对象");
}
private static Lazy LAZY;
public static Lazy getInstance(){
if (LAZY == null) {
LAZY = new Lazy();
}
return LAZY;
}
}
public class Demo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
},"Thread " + i).start();
}
}
}
Thread 5创建了一个对象
Thread 1创建了一个对象
Thread 9创建了一个对象
Thread 2创建了一个对象
Thread 6创建了一个对象
Thread 7创建了一个对象
Thread 4创建了一个对象
Thread 3创建了一个对象
Thread 8创建了一个对象
Thread 0创建了一个对象

显然已经不是单例了

DCL 懒汉式

解决方法——Double Checked Locking DCL懒汉式单例:

  • 执行getInstance()方法时,先判断对象是否已被创建,没创建,就先把整个类模板锁起来
  • 在锁里在检测一次对象是否已被创建,此时如果对象未被创建,才创建实例化对象
class Lazy{
private Lazy(){
System.out.println(Thread.currentThread().getName() + "创建了一个对象");
}
private static Lazy LAZY;
public static Lazy getInstance(){
if (LAZY == null) {
synchronized (Lazy.class) {
if (LAZY == null) {
LAZY = new Lazy();
}
}
}
return LAZY;
}
}
public class Demo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
},"Thread " + i).start();
}
}
}
Thread 6创建了一个对象

问题:

  • 在使用new Lazy()构造方法创建实例化对象时,可能有问题
  • 因为构造方法不是一个原子性操作,在极端情况下依然会出现并发问题
    1. 分配内存空间
    2. 执行构造方法,初始化对象
    3. 栈引用指向堆内存中的对象
  • 这三个步骤如果发生指令重排,就会有问题,因此必须使用Volatile来禁止指令重排
class Lazy{
private Lazy(){
System.out.println(Thread.currentThread().getName() + "创建了一个对象");
}
private static volatile Lazy LAZY;
public static Lazy getInstance(){
if (LAZY == null) {
synchronized (Lazy.class) {
if (LAZY == null) {
LAZY = new Lazy();
}
}
}
return LAZY;
}
}

静态内部类实现

  • 定义一个静态的内部类
  • 在内部类中创建外部类的实例
  • 在外部类创建getInstance()方法,返回内部类中的外部类实例
class Holder{
private Holder(){}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}

反射创建实例破坏单例

虽然构造方法被设置为了私有,但是通过反射依然可以强行调用它来创造实例对象,从而破坏单例

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Lazy lazy1 = Lazy.getInstance();
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 无视私有
Lazy lazy2 = constructor.newInstance();
System.out.println(lazy1.hashCode());
System.out.println(lazy2.hashCode());
}
1580066828
491044090

可以看到创建出了两个不同的实例化对象

解决方法:在构造方法中锁类模板,如果在执行构造方法时发现已经有对象了,直接抛出一个异常

class Lazy{
private Lazy(){
synchronized (Lazy.class) {
if (LAZY != null) {
throw new RuntimeException("不允许通过反射调用构造方法创建对象");
}
}
}
private static volatile Lazy LAZY;
public static Lazy getInstance(){
if (LAZY == null) {
synchronized (Lazy.class) {
if (LAZY == null) {
LAZY = new Lazy();
}
}
}
return LAZY;
}
}
public class Demo {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Lazy lazy1 = Lazy.getInstance();
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 无视私有
Lazy lazy2 = constructor.newInstance();
System.out.println(lazy1.hashCode());
System.out.println(lazy2.hashCode());
}
}
Exception in thread "main" java.lang.reflect.InvocationTargetException
...
Caused by: java.lang.RuntimeException: 不允许通过反射调用构造方法创建对象
at singleton.Lazy.<init>(Demo.java:32)
... 5 more

多次用反射创建多个对象破坏单例

如果没用构造方法创建实例,而是直接用反射创建多个实例,还是会破坏单例

public class Demo {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 无视私有
Lazy lazy1 = constructor.newInstance();
Lazy lazy2 = constructor.newInstance();
System.out.println(lazy1.hashCode());
System.out.println(lazy2.hashCode());
}
}
1580066828
491044090

可以看到又创建出多个对象了

解决方法:定义一个名字奇奇怪怪的谁也不知道的标志位,通过标志位来标记构造方法是否被执行过

class Lazy{
private static boolean mySecretFlag = false;
private Lazy(){
synchronized (Lazy.class) {
if (!mySecretFlag) {
mySecretFlag = true;
} else {
throw new RuntimeException("不允许通过反射调用构造方法创建对象");
}
}
}
private static volatile Lazy LAZY;
public static Lazy getInstance(){
if (LAZY == null) {
synchronized (Lazy.class) {
if (LAZY == null) {
LAZY = new Lazy();
}
}
}
return LAZY;
}
}
Exception in thread "main" java.lang.reflect.InvocationTargetException
...
Caused by: java.lang.RuntimeException: 不允许通过反射调用构造方法创建对象
at singleton.Lazy.<init>(Demo.java:37)
... 5 more

问题:这个用来做标志的变量也可以通过反射获取并进行修改

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
constructor.setAccessible(true); // 无视私有
Field mySecretFlag = Lazy.class.getDeclaredField("mySecretFlag");
mySecretFlag.setAccessible(true);
Lazy lazy1 = constructor.newInstance();
mySecretFlag.set(lazy1, false);
Lazy lazy2 = constructor.newInstance();
System.out.println(lazy1.hashCode());
System.out.println(lazy2.hashCode());
}
644117698
1872034366

可以看到又成功破坏了单例


通过枚举保证单例

JDK 1.5

  • 枚举本身也是一个类
  • 枚举自带单例模式

在使用反射调用枚举类的无参构造方法时,会报错说不存在无参构造方法

  • 通过jad工具查看源码,发现确实没有无参构造方法
  • 有一个有参构造方法,参数类型为String, int
  • 于是在反射调用构造方法时,调用这个有参构造方法
enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle enumSingle1 = EnumSingle.getInstance();
Constructor<EnumSingle> declaredConstructor =
EnumSingle.class.getDeclaredConstructor(String.class, int.class);
EnumSingle enumSingle2 = declaredConstructor.newInstance();
System.out.println(enumSingle1.hashCode());
System.out.println(enumSingle2.hashCode());
}
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at singleton.EnumSingle.main(Demo.java:89)

确实无法通过反射创建多个对象了