
这两天看到了单例模式的面试题,打算把单例模式的知识点整理一下。
概念
单例模式(Singleton Pattern
)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
要注意的地方:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
啥子优缺点
优点
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
用途
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
JDK中哪些实现了单例模式
java.lang.Runtime
由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。
线程池
java.lang.reflect.Proxy类
GUI中
java.awt.Toolkit#getDefaultToolkit()
java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()
实现方式
方式 |
Lazy 初始化 |
多线程安全 |
实现难度 |
懒汉式 |
是 |
否/是 |
易 |
饿汉式 |
否 |
是 |
易 |
DLC |
是 |
是 |
较复杂 |
内部静态类 |
是 |
是 |
一般 |
枚举 |
否 |
是 |
易 |
懒汉式
分为多线程安全和多线程不安全两种。
多线程不安全
最基本的实现方式,但是由于没有添加synchronized
锁,所以不支持多线程,在多线程不能正常工作。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public class LazyMan {
private LazyMan(){}
private static LazyMan lazyMan;
public static LazyMan getInstance(){ if (lazyMan == null){ lazyMan = new LazyMan(); } return lazyMan; } }
|
多线程测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
public class LazyMan {
private LazyMan(){ System.out.println(Thread.currentThread().getName() + "OK"); }
private static LazyMan lazyMan;
public static LazyMan getInstance(){ if (lazyMan == null){ lazyMan = new LazyMan(); } return lazyMan; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ lazyMan.getInstance(); }).start(); } } }
|
测试结果
| Thread-2OK Thread-9OK Thread-0OK Thread-6OK Thread-8OK Thread-4OK Thread-3OK Thread-1OK Thread-5OK Thread-7OK
|
出现多个线程,且每次运行结果不同。
多线程安全
效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
| public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
|
饿汉式
比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
| public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
|
DCL(双检锁/双重校验锁)
采用双锁机制,安全且在多线程情况下能保持高性能。
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package single;
public class DCL {
private DCL(){}
private static DCL lazyMan;
public volatile static DCL getInstance(){ if (lazyMan == null){ synchronized (LazyMan.class){ if (lazyMan == null){ lazyMan = new DCL();
} };
} return lazyMan; } }
|
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| package single;
public class DCL {
private DCL(){ System.out.println(Thread.currentThread().getName() + "OK"); }
private static DCL lazyMan;
public volatile static DCL getInstance(){ if (lazyMan == null){ synchronized (LazyMan.class){ if (lazyMan == null){ lazyMan = new DCL();
} };
} return lazyMan; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ lazyMan.getInstance(); }).start(); } } }
|
测试结果
运行结果只有一个线程。
注意
不加volatile
时,由于不是原子性操作,可能出现指令重排现象使得线程不安全。
volatile:只保证可见性,不保证原子性,但是可以防止指令重排
静态内部类
这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
| public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
|
枚举
是实现单例模式的最佳方法。
更简洁,自动支持序列化机制,绝对防止多次实例化。
| public enum Singleton { INSTANCE; public void whateverMethod() { } }
|
参考
菜鸟教程
Hollis