单例模式的几种写法

52 阅读3分钟

1. 饿汉式(线程安全)

容易产生垃圾对象。

public class Singleton1 {
    private static final Singleton1 instance = new Singleton1();
    
    private Singleton1() {}
    
    public static Singleton1 getInstance() {
        return instance;
    }
}

**2. 懒汉式(线程不安全) **

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {}

    public static SingleTon getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new SingleTon();
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> System.out.println(SingleTon.getInstance().hashCode())).start();
        }
    }
}
/** hashCode不一致,不是一个单例
1236783213
1849980617
2137033978
270921682
1902102109
919115337
1482438336
587167875
932094492
2016912823
*/

3. 懒汉式(线程安全)

但是,对整个方法加锁,锁的粒度太大,效率比较低

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {}

    public static synchronized SingleTon getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new SingleTon();
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> System.out.println(SingleTon.getInstance().hashCode())).start();
        }
    }
}

/** hashCode一致
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
*/

4. 懒汉式(优化加锁,但不可行)

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {}

    public static SingleTon getInstance() {
        if (instance == null) {
            // 对里面进行加锁,虽然可以提高效率,但是不可行,不是单例模式
            /**
            * 当两个线程同时运行到此处时
            * 第一个线程假设进入同步代码块获得了实例
            * 第二个线程这时也可以进入同步代码块获取实例
            * 所以,可以在synchronized中再次判断instance是否为空
            */
            synchronized(SingleTon.class) {	
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new SingleTon();
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> System.out.println(SingleTon.getInstance().hashCode())).start();
        }
    }
}

/** hashCode不一致
1236783213
1482438336
919115337
2113902944
1849980617
270921682
932094492
2016912823
587167875
1902102109
*/

5. 懒汉式(双重校验锁,线程安全)

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {}

    public static SingleTon getInstance() {
        if (instance == null) {	// 第一次判空:可以排除大部分instance不为空的情况,可以提高效率
            synchronized(SingleTon.class) {
                if (instance == null) {	// 防止两个线程同时运行到此处
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> System.out.println(SingleTon.getInstance().hashCode())).start();
        }
    }
}

/** hsahCode一致
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
1482438336
*/

在第2行代码处要不要加volatile呢?

需要注意 instance 采⽤ volatile 关键字修饰也是很有必要。

instance = new Singleton();

这段代码其实是分为三步执⾏:

  1. instance 分配内存空间
  2. 初始化 instance
  3. instance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getInstance() 后发现 instance 不为空,因此返回instance,但此时 instance 还未被初始化。

使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。