【设计模式】第一章:单例模式详解及应用案例

系列文章

【设计模式】七大设计原则
【设计模式】第一章:单例模式
【设计模式】第二章:工厂模式
【设计模式】第三章:建造者模式
【设计模式】第四章:原型模式
【设计模式】第五章:适配器模式
【设计模式】第六章:装饰器模式
【设计模式】第七章:代理模式
【设计模式】第八章:桥接模式
【设计模式】第九章:外观模式 / 门面模式
【设计模式】第十章:组合模式
【设计模式】第十一章:享元模式
【设计模式】第十二章:观察者模式
【设计模式】第十三章:模板方法模式
【设计模式】第十四章:策略模式
【设计模式】第十五章:责任链模式
【设计模式】第十六章:迭代器模式
【设计模式】第十七章:状态模式
【设计模式】第十八章:备忘录模式
【设计模式】第十九章:访问者模式
【设计模式】第二十章:解释器模式
【设计模式】第二十一章:命令模式
【设计模式】第二十二章:中介者模式


目录

  • 系列文章
  • 一、定义及注意事项
  • 二、实现方式
    • 1. 懒汉式(线程不安全)
    • 2. 懒汉式(线程安全)
      • (1) 方法加锁
      • (2) 代码块加锁
    • 3. 双检锁式
      • 双检锁式为什么要判空两次
    • 4. 饿汉式
  • 三、应用场景
  • 四、小结
  • 推荐


一、定义及注意事项

摘自百度百科:单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)

也就是说,一个类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

在我们使用单例模式的时候需要注意的是:

  • 单例类有且只能有一个实例
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须给所有其他对象提供这一实例。

二、实现方式

在Java中,一般有两种实现方式:懒汉式饿汉式,顾名思义:

  • 懒汉式:比较懒,什么时候使用,什么时候才进行实例化
  • 饿汉式:特别饥饿,因为饿怕了所以在创建对象的时候就进行实例化

1. 懒汉式(线程不安全)

public static class Lazy {
	private static Lazy instance = null;

	private Lazy() {
	}

	public static Lazy getInstance() {
		if (null == instance) {
			instance = new Lazy();
		}
		return instance;
	}
}

这是懒汉式最基础的一种实现方式,方法在第一次访问的时候才进行实例化,从而达到懒加载的效果。但是这种写法会有线程安全的问题,假如在对象还未实例化的时候被两个线程同时访问,它就有可能出现被实例化多次的情况。那么有没有更好的解决方案呢,自然是有的,给上面的代码加锁就可以了,也就是线程安全的懒汉式。

2. 懒汉式(线程安全)

线程安全的懒汉式有两种写法:一种是把锁加在方法上,另一种是加在代码块中。

(1) 方法加锁

public static class Lazy {
	private static Lazy instance = null;

	private Lazy() {
	}

	public static synchronized Lazy getInstance() {
		if (null == instance) {
			instance = new Lazy();
		}
		return instance;
	}
}

以上写法便是给懒汉式加了锁后的代码,但是还会存在另一个问题:synchronized修饰的是整个方法,也就是每次访问时都是同步的,但是这个方法只会实例化一次,这样就会导致效率变低。那么大家思考一下,有没有更适合的解决方案呢?其实我们只需要把对方法加锁换成给代码块加锁就可以了。

(2) 代码块加锁

public static class Lazy {
	private static Lazy instance = null;

	private Lazy() {
	}

	public static Lazy getInstance() {
		if (null == instance) {
			synchronized (Lazy.class){
				instance = new Lazy();
			}
		}
		return instance;
	}
}

上面的代码是将锁加在代码块中,那么这种写法是最优解吗?当然在单线程中是没有任何问题的,但是如果是多线程,假如有 线程A 和 线程B 一起访问了该方法,线程A 首先进入 if 拿到锁,此时线程B进入 if 后被同步,等待线程A释放锁,那么当线程A释放后,线程B进入锁又进行了一次实例化。也就是说这种写法也可能会导致多次实例化,此时我们可以看一下双检锁式。

3. 双检锁式

public static class Lazy {
	private static Lazy instance = null;

	private Lazy() {
	}

	public static Lazy getInstance() {
		if (null == instance) {
			synchronized (Lazy.class) {
				if (null == instance) {
					instance = new Lazy();
				}
			}
		}
		return instance;
	}
}

我们上面在代码中利用 if 进行了两次的判空,大家来思考一下我们为什么要进行两次判空呢?

双检锁式为什么要判空两次

我们可以仔细看一下代码中的两个 if 都有什么作用:

  • 首先是第一个 if :如果我们去掉第一个 if,那么每个进入方法的线程都会被同步,那就和把锁加在方法上一样了,也就是说会影响效率。
  • 第二个 if:我们已经判断过一次非空,如果我们将第二个 if 去掉的话,在多线程的情况下可能会导致多次实例化的情况,所以我们要在锁中再判空一次。

4. 饿汉式

public static class Hungry {
	private static Hungry instance = new Hungry();

	private Hungry() {
	}

	private static Hungry getInstance() {
		return instance;
	}
}

饿汉式也是比较常见的写法,在类加载的时候就已经完成了实例化,效率比较高。当然缺点也很明显,如果该实例没有被用到,那么给其分配的内存就浪费掉了。


三、应用场景

以下部分内容摘自菜鸟教程

**意图:**保证一个类仅有一个实例
**主要解决:**一个全局使用的类频繁的创建与销毁的场景
**何时使用:**当要控制实例数目,节省系统资源时
**如何解决:**判断系统是否已经有这个单例,如果有则直接返回,如果没有则创建
**关键代码:**构造函数是私有的
应用实例:
1、一个班级只有一个班主任。
2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
适用场景
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。


四、小结

单例模式是创建型模式之一,它只允许创建一个对象,节省了多次创建对象的内存和时间;经常被公共访问的对象适用单例模式。但它不适用于有变化的对象,如果一个对象在不同场景下会发生变化,单例模式会引起变化,不能适用所有的应用场景。


推荐

关注博客和公众号获取最新文章

Bummon’s Blog | Bummon’s Home | 公众号

你可能感兴趣的:(设计模式,单例模式,设计模式,java)