单例模式详解

单例模式详解

单例模式就是只能有一个实例的模式;最大的特点是构造器私有。建议看视频

单例模式分为两种:

饿汉式:直接将类的实例初始化好,可能会存在资源浪费的情况;

懒汉式:用的时候再初始化实例,比较常用。

饿汉式

特点:

  1. 构造器私有
  2. 构建一个静态常量表示类的实例
  3. 构建一个静态getInstance()方法,外部可以获得类的实例
  4. 多线程下也可以保证实例的唯一性
package com.example.juc;

public class TestHungryMan {
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];
    private byte[] data5 = new byte[1024 * 1024];

    private TestHungryMan() {
        System.out.println(Thread.currentThread().getName());
    }

    private final static TestHungryMan HUNGRY_MAN = new TestHungryMan();

    public static TestHungryMan getInstance() {
        return HUNGRY_MAN;
    }

}

class Test {
    public static void main(String[] args) {
        // 多线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                TestHungryMan testHungryMan = TestHungryMan.getInstance();
                System.out.println(testHungryMan);
            }).start();

        }
    }
}

Thread-0
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd
com.example.juc.TestHungryMan@6b7011dd

懒汉式

普通懒汉式

特点:

  1. 构造器私有
  2. 创建一个静态变量代表实例
  3. 构建一个公开的静态方法getInstance()来获取实例,;里面的逻辑为实例为空时才创建
  4. 多线程时单例失效
package com.example.juc;

public class TestLazyMan {
    private TestLazyMan() {
        System.out.println(Thread.currentThread().getName());
    }

    private static TestLazyMan lazyMan;

    public static TestLazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new TestLazyMan();
        }
        return lazyMan;
    }
}

class Test1 {
    public static void main(String[] args) {
        // 正常单线程获取实例,没问题
//        TestLazyMan lazyMan = TestLazyMan.getInstance();

        // 多线程 无法保证实例唯一
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                TestLazyMan testLazyMan = TestLazyMan.getInstance();
                System.out.println(testLazyMan);
            }).start();

        }

    }
}

result:

Thread-0
com.example.juc.TestLazyMan@22e7a307
Thread-2
Thread-3
com.example.juc.TestLazyMan@1acc1ca0
Thread-4
Thread-1
com.example.juc.TestLazyMan@39ae8a9b
Thread-7
Thread-6
com.example.juc.TestLazyMan@6bfcab86
Thread-9
com.example.juc.TestLazyMan@384e8869
Thread-8
Thread-5
com.example.juc.TestLazyMan@d166de5
com.example.juc.TestLazyMan@3261ea9b
com.example.juc.TestLazyMan@2131513b
com.example.juc.TestLazyMan@46a00ba1
com.example.juc.TestLazyMan@72bb9f6e

DCL懒汉式(双层检查锁模式)

特点:

  1. 其他同普通懒汉式

  2. getInstance()里面的逻辑采用双层检查锁模式:先做一次非同步的检查,将类锁住,再做一次同步的检查,保证多线程单例

  3. 由于类的初始化是非原子性操作,可能会导致代码出错

    类的初始化:

    1.分配内存空间

    2.执行构造方法,初始化对象

    3.把这个对象指向这个空间

    由于指令重排,执行顺序可能是132, 就会出问题

    所以要使用双层检查锁+原子性操作(volatile)

package com.example.juc;

public class TestLazyMan {
    private TestLazyMan() {
        System.out.println(Thread.currentThread().getName());
    }

    private volatile static TestLazyMan lazyMan;

    // 双重检查锁模式(Double-Check-Lock) DCL懒汉式
    public static TestLazyMan getInstance() {
        // 一次检查 非同步
        if (lazyMan == null) {
            synchronized (TestLazyMan.class) {
                // 二次检查 同步
                if (lazyMan == null) {
                    lazyMan = new TestLazyMan();
                }
            }
        }
        return lazyMan;
    }
}

class Test1 {
    public static void main(String[] args) {
        // 正常单线程获取实例,没问题
//        TestLazyMan lazyMan = TestLazyMan.getInstance();

        // 多线程 无法保证实例唯一
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                TestLazyMan testLazyMan = TestLazyMan.getInstance();
                System.out.println(testLazyMan);
            }).start();

        }

    }
}

Thread-0
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307
com.example.juc.TestLazyMan@22e7a307

通过反射获取

package com.example.juc;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class TestLazyMan {
    private TestLazyMan() {
        System.out.println(Thread.currentThread().getName());
    }

    private static TestLazyMan lazyMan;

    // 双重检查锁模式(Double-Check-Lock) DCL懒汉式
    public static TestLazyMan getInstance() {
        // 一次检查 非同步
        if (lazyMan == null) {
            synchronized (TestLazyMan.class) {
                // 二次检查 同步
                if (lazyMan == null) {
                    lazyMan = new TestLazyMan();
                }
            }
        }
        return lazyMan;
    }
}

class Test1 {
    public static void main(String[] args) throws Exception {
        // 多线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                TestHungryMan testHungryMan = TestHungryMan.getInstance();
                System.out.println(testHungryMan);
            }).start();

        }

    }
}

result

main
com.example.juc.TestLazyMan@681a9515... 
main
com.example.juc.TestLazyMan@3af49f1c
com.example.juc.TestLazyMan@681a9515

静态内部类

特点:

  1. 能有效避免懒汉式的多线程单例失效问题
  2. 但是不能避免饿汉式的资源浪费问题
  3. 感觉没啥用
package com.example.juc;

public class TestStaticInner {
    private TestStaticInner() {

    }

    public static TestStaticInner getInstance() {
        return Inner.staticInner;
    }

    public static class Inner {
        private static final TestStaticInner staticInner = new TestStaticInner();
    }
}

安全问题

以上3种方法均为不安全的方法

以DCL懒汉式为例

通过反射创建实例会使单例模式失效

package com.example.juc;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class TestLazyMan {
    private TestLazyMan() {
        System.out.println(Thread.currentThread().getName());
    }

    private volatile static TestLazyMan lazyMan;

    // 双重检查锁模式(Double-Check-Lock) DCL懒汉式
    public static TestLazyMan getInstance() {
        // 一次检查 非同步
        if (lazyMan == null) {
            synchronized (TestLazyMan.class) {
                // 二次检查 同步
                if (lazyMan == null) {
                    lazyMan = new TestLazyMan();
                }
            }
        }
        return lazyMan;
    }
}

class Test1 {
    public static void main(String[] args) throws Exception {
        // 正常单线程获取实例,没问题
        TestLazyMan lazyMan = TestLazyMan.getInstance();
        System.out.println(lazyMan + "... ");
        // 通过反射创建新实例获取
        Constructor<TestLazyMan> declaredConstructor = TestLazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        TestLazyMan lazyMan1 = declaredConstructor.newInstance();
        System.out.println(lazyMan1);

        // 通过反射方法获取
        Method getInstance = TestLazyMan.class.getMethod("getInstance", null);
        TestLazyMan invoke = (TestLazyMan) getInstance.invoke(lazyMan1);
        System.out.println(invoke);

    }
}
main
com.example.juc.TestLazyMan2@681a9515
main
com.example.juc.TestLazyMan2@3af49f1c

解决:构造函数里面进行判断,但是多个反射创建实例又会失效

package com.example.juc;

import java.lang.reflect.Constructor;

public class TestLazyMan2 {
    private TestLazyMan2() {
        synchronized (TestLazyMan.class) {
            // 二次检查 同步
            if (lazyMan != null) {
                throw new RuntimeException("不能再次创建实例");
            }
        }

        System.out.println(Thread.currentThread().getName());
    }

    private volatile static TestLazyMan2 lazyMan;

    // 双重检查锁模式(Double-Check-Lock) DCL懒汉式
    public static TestLazyMan2 getInstance() {
        // 一次检查 非同步
        if (lazyMan == null) {
            synchronized (TestLazyMan.class) {
                // 二次检查 同步
                if (lazyMan == null) {
                    lazyMan = new TestLazyMan2();
                }
            }
        }
        return lazyMan;
    }
}

class Test2 {
    public static void main(String[] args) throws Exception {
        // 正常单线程获取实例,没问题
//        TestLazyMan2 lazyMan = TestLazyMan2.getInstance();
//        System.out.println(lazyMan + "... ");
        // 通过反射创建新实例获取
        Constructor<TestLazyMan2> declaredConstructor = TestLazyMan2.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        TestLazyMan2 lazyMan1 = declaredConstructor.newInstance();
        TestLazyMan2 lazyMan2 = declaredConstructor.newInstance();
        System.out.println(lazyMan1);
        System.out.println(lazyMan2);

    }
}

道高一尺,魔高一丈

枚举类破解了反射的不安全

package com.example.juc;

import java.lang.reflect.Constructor;

public enum TestEnumSingle {
    INSTANCE;

    public TestEnumSingle getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws Exception {
        TestEnumSingle instance = TestEnumSingle.INSTANCE;
        Constructor<TestEnumSingle> declaredConstructor = TestEnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        TestEnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);

    }

}

反编译源码

使用jad反编译工具 到\target\classes\com\example\juc目录下,运行命令

jad -sjava TestEnumSingle.class

可以看到TestEnumSingle类的构造器是有参构造器

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   TestEnumSingle.java

package com.example.juc;

import java.io.PrintStream;
import java.lang.reflect.Constructor;

public final class TestEnumSingle extends Enum
{

    public static TestEnumSingle[] values()
    {
        return (TestEnumSingle[])$VALUES.clone();
    }

    public static TestEnumSingle valueOf(String name)
    {
        return (TestEnumSingle)Enum.valueOf(com/example/juc/TestEnumSingle, name);
    }

    private TestEnumSingle(String s, int i)
    {
        super(s, i);
    }

    public TestEnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static void main(String args[])
        throws Exception
    {
        TestEnumSingle instance = INSTANCE;
        Constructor declaredConstructor = com/example/juc/TestEnumSingle.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        TestEnumSingle instance2 = (TestEnumSingle)declaredConstructor.newInstance(new Object[0]);
        System.out.println(instance);
        System.out.println(instance2);
    }

    public static final TestEnumSingle INSTANCE;
    private static final TestEnumSingle $VALUES[];

    static 
    {
        INSTANCE = new TestEnumSingle("INSTANCE", 0);
        $VALUES = (new TestEnumSingle[] {
            INSTANCE
        });
    }
}
posted @ 2021-12-22 16:35  Oh,mydream!  阅读(65)  评论(0编辑  收藏  举报