概念:
单例模式重点在于在整个系统中共享一些创建时需要耗费较多资源的对象,在整个系统应用中只存在一个对象,被其他组件共享。
单例创建的步骤:
- 编写一个类
- 构造方法私有
- 编写一个方法用于获取唯一实例
优点:
- 在内存中只有一个实例,减少内存的开销,尤其是需要频繁创建和销毁的对象
- 减少资源的多重占用(比如写文件操作)
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 当你想控制类的实例,节省系统资源的时候。
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
实现:
1.懒汉式,线程不安全
- 是否Lazy初始化:是
- 是否多线程安全:否
- 实现难度:易
//懒汉式-线程不安全
public class LazyAndThreadUnsafe {
//构造函数私有
private LazyAndThreadUnsafe(){
System.out.println(Thread.currentThread().getName());
}
//单一实例
private static LazyAndThreadUnsafe lazyAndThreadUnsafe;
//唯一访问实例的入口
public static LazyAndThreadUnsafe getInstance(){
if(lazyAndThreadUnsafe == null){
lazyAndThreadUnsafe = new LazyAndThreadUnsafe();
}
return lazyAndThreadUnsafe;
}
}
class Test{
public static void main(String[] args) {
//LazyAndThreadUnsafe instance1 = LazyAndThreadUnsafe.getInstance();
//LazyAndThreadUnsafe instance2 = LazyAndThreadUnsafe.getInstance();
//System.out.println(instance1 == instance2); //true
//10个线程获取单例,发现构造函数被执行了多次,说明不是直接返回的对象,而是创建的
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyAndThreadUnsafe.getInstance();
}).start();
}
}
}
2.懒汉式,线程安全
- 是否Lazy初始化:是
- 是否多线程安全:是
- 实现难度:易
- 描述:加锁,会影响效率
//懒汉式,线程安全
public class LazyAndThreadSafe {
private LazyAndThreadSafe(){
System.out.println(Thread.currentThread().getName());
}
private static LazyAndThreadSafe lazyAndThreadSafe;
public static synchronized LazyAndThreadSafe getInstance(){
if(lazyAndThreadSafe == null){
lazyAndThreadSafe = new LazyAndThreadSafe();
}
return lazyAndThreadSafe;
}
}
class TestLazyAndThreadSafe{
public static void main(String[] args) {
//加了锁之后,只有一个线程会进入构造函数初始化对象
//但是效率低下
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyAndThreadSafe.getInstance();
}).start();
}
}
}
3.饿汉式
- 是否Lazy初始化:否
- 是否多线程安全:是
- 实现难度:易
- 描述:这种方式常用,没有加锁,执行效率会提高,但是类加载时就初始化,浪费内存, 不过是线程安全的。
//饿汉式
public class HungryAndThreadSafe {
private HungryAndThreadSafe(){
System.out.println(Thread.currentThread().getName());
}
private static HungryAndThreadSafe hungryAndThreadSafe = new HungryAndThreadSafe();
public static HungryAndThreadSafe getInstance(){
return hungryAndThreadSafe;
}
}
class TestHungryAndThreadSafe{
public static void main(String[] args) {
//类加载时就创建对象,线程安全
for (int i = 0; i < 10; i++) {
new Thread(()->{
HungryAndThreadSafe.getInstance();
}).start();
}
}
}
4.双检锁/双从校验锁(DCL,即double-checked locking)
- 是否Lazy初始化:是
- 是否多线程安全:是
- 实现难度:较复杂
- 描述:采用双锁机制,安全且在多线程下能保持高性能。
- volatile关键字 在jdk1.5出现
//双重检验锁
public class DCLAndThreadSafe {
private DCLAndThreadSafe(){
System.out.println(Thread.currentThread().getName());
}
private static volatile DCLAndThreadSafe dclAndThreadSafe;
public static DCLAndThreadSafe getInstance(){
if(dclAndThreadSafe == null){
synchronized (DCLAndThreadSafe.class){
if(dclAndThreadSafe == null){
dclAndThreadSafe = new DCLAndThreadSafe(); //不是原子性操作
/**
* 创建对象不是原子性操作,它被分解为几个指令:
* 创建对象的步骤:
* 1.为对象分配内存空间
* 2.执行构造方法,初始化对象
* 3.将实例(引用)指向内存空间
*
* 我们期望的执行顺序:123
* 而实际可能的执行顺序:132,当执行完13后,对象的引用不为null,但是对象还没有初始化
* 如果这时有线程访问getInstance()方法,将直接返回‘半个实例‘,取到的不是完整的对象,程序就会错处。
*
* 这是由于jvm为了优化指令,所以会对象指令进行重排序,提高程序的运行效率
* 所以为了防止指令重排序,在实例属性上加上volatile关键字
*/
}
}
}
return dclAndThreadSafe;
}
}
class TestDCLAndThreadSafe{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
DCLAndThreadSafe.getInstance();
}).start();
}
}
}
5.登记式/静态内部类
- 是否Lazy初始化:是
- 是否多线程安全:是
- 实现难度:一般
- 描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,所以线程安全。
//登记式/静态内部类
public class StaticAndThreadSafe {
private StaticAndThreadSafe(){
System.out.println(Thread.currentThread().getName());
}
//静态内部类,静态内部类的加载不需要依附于外部类,在使用时才加载,不过内部类的加载会导致外部类的加载
private static class InnerClass{
private static final StaticAndThreadSafe INSTANCE = new StaticAndThreadSafe();
}
//获取实例,调用才实例化
public static StaticAndThreadSafe getInstance(){
return InnerClass.INSTANCE;
}
}
class TestStaticAndThreadSafe{
public static void main(String[] args) {
//安全
for (int i = 0; i < 10; i++) {
new Thread(()->{
StaticAndThreadSafe.getInstance();
}).start();
}
}
}
6.枚举
- 是否Lazy初始化:否
- 是否多线程安全:是
- 实现难度:易
- 描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。 - 版本:jdk1.5之后
//非抽象的枚举类默认使用final修饰,不能被继承
public enum EnumAndThreadSafe {
INSTANCE; //实例
//枚举类的构造函数必须是私有的
//实例中如果有参数,构造函数也要有参数,且构造函数不能省略
private EnumAndThreadSafe(){
//观察有几个线程进入了构造方法,判断是否是线程安全的
System.out.println(Thread.currentThread().getName());
}
public EnumAndThreadSafe getInstance(){
return INSTANCE;
}
}
class TestEnumAndThreadSafe{
public static void main(String[] args) {
// EnumAndThreadSafe instance1 = EnumAndThreadSafe.INSTANCE;
// EnumAndThreadSafe instance2 = EnumAndThreadSafe.INSTANCE;
// System.out.println(instance1 == instance2);
//线程安全
for (int i = 0; i < 10; i++) {
new Thread(()->{
EnumAndThreadSafe instance = EnumAndThreadSafe.INSTANCE;
}).start();
}
}
}
总结:
一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。