Java设计模式之单例模式
目录
简介
设计模式是软件开发中必须要掌握的知识,特别是为了开发出可重用,更可靠的代码,设计模式就更不可或缺。于是我准备写几篇文章来详细介绍一下常用的设计模式。第一篇就从最简单的单例模式讲起吧!
1.什么是设计模式?
设计模式就是过去人们在面对同样的软件设计问题时所总结出来的经验。是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。有了设计模式,你就可以学习过去人们在设计软件上的一些优秀经验从而开发出更稳定的代码出来。说的更容易理解一点就是学习过去人们遇到类似问题时的解决经验来帮助我们解决现有的问题。
2.单例模式
单例模式可以说是所有的设计模式中最简单的设计模式,所以说今天首先就来讲一下它。单例单例,顾名思义,就是该类只会有一个独一无二的实例。关于单例模式只需要记住一句话,那就是”确保一个类只有一个独一无二的实例,并且提供一个全局访问点”,单例常常用来管理共享的一些资源比如线程池连接或者数据库连接或者用作工具类。下面来看一个典型的单例模式的实例。如下:
public class SingleTon {
private static SingleTon uniqueInstance = null;
public static SingleTon getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new SingleTon();
}
return uniqueInstance;
}
}
以上就是一个典型的单例模式的实例,定义了一个静态变量uniqueInstance,定义了一个静态方法getInstance()得到这个变量,先判断该变量是否为null,如果为null,证明是第一次调用getInstance()方法,则先调用构造方法初始化;如果不为null则直接返回该对象。通过这种方式,保证调用getInstance()方法的时候返回的都是第一次构造方法初始化的那个uniqueInstance对象。不过上述的实例有一点小问题,后面会进行详细说明,你们可以先想一下到底哪里有点小问题呢。
下面通过一种实际的场景来体验一下单例模式的使用并且暴露一下我上面提到的可能存在的问题。
场景:假设一个巧克力工厂有一个计算机控制的锅炉,锅炉的作用是将牛奶和巧克力融在一起,送到下一阶段,制成巧克力棒。那么这个锅炉控制的代码就可以写成这样:
public class ChocolateBoiler {
private static ChocolateBoiler instance = null;
private boolean isEmpty;//锅炉是否为空
private boolean isBoiled;//锅炉是否正在加热
public static ChocolateBoiler getInstance() {
if(instance == null) {
instance = new ChocolateBoiler();
}
return instance;
}
public ChocolateBoiler() {//初始化时锅炉为空
isEmpty = true;
isBoiled = false;
}
public void fill() {//填充锅炉
if (isEmpty()) {
isEmpty = false;
isBoiled = false;
}
}
public void drain() {//清空锅炉 保证清空之前是满的并且已经煮过
if (!isEmpty() && isBoiled()) {
isEmpty = true;
isBoiled = false;
}
}
public void boil() {//开始煮 必须确保是满的而且没有煮过
if (!isEmpty()&&!isBoiled()) {
isBoiled = true;
}
}
public boolean isEmpty() {
return isEmpty;
}
public boolean isBoiled() {
return isBoiled;
}
}
上面这段代码就是单例模式的一个实际应用。我们通过ChocolateBoiler.getInstance()方法获取一个实例,然后调用某个方法就可以让这个锅炉工作起来。但是考虑一下某种特殊情况。如果有多个线程同时调用getInstance()方法呢?比如现在有线程A和线程B同时调用了getInstance()方法,那么很可能就会产生多个instance对象。因为CPU按时间切片,导致某个线程访问instance对象的时候不是最新的状态。所以我们就需要对getInstance()方法进行同步。那么熟悉多线程的人很可能就想到加个synchronized同步这个方法就好了。如下
public synchronized static ChocolateBoiler getInstance() {
if(instance == null) {
instance = new ChocolateBoiler();
}
return instance;
}
这样写当然没任何问题。但是有没有想过效率的问题?事实上这个方法只有第一个调用的时候才会需要同步,后面的调用只是返回instance,只有第一次才需要新建,所以才需要同步,使用双重枷锁机制进行一下优化如下:
public static ChocolateBoiler getInstance() {
if(instance == null) {
synchronized(this) {
if(instance == null) {
instance = new ChocolateBoiler();
}
}
}
return instance;
}
通过这种方式就确保了线程同步并且保证了每次返回的对象一定是同一个。
3.单例模式的实现方式
实现单例模式的方式有很多种方式。一般的教材都只说了”饿汉式” 和 “懒汉式”两种方式,其实并不只有这两种方式,下面对所有的方式以及优点和缺点进行一下介绍:
- 单例模式的八种写法
1.饿汉式(静态变量)
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
优点:无需做线程同步 每次返回的都是同一个对象
缺点:在类加载的时候就会初始化INSTANCE变量,不管后面是否用到,INSTANCE都会被实例化。如果没用过,造成浪费。
2.饿汉式(静态方法)
public class Singleton {
private final static Singleton INSTANCE;
static {
INSTANCE = new SingleTon();
}
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
优缺点和1类似,只不过是在静态方法中进行了初始化。
3、懒汉式(线程不安全)[不可用]
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
4、懒汉式(线程安全,同步方法)[不推荐用]
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。
5、懒汉式(线程安全,同步代码块)[不可用]
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
6、双重检查[推荐用]
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
优点:线程安全;延迟加载;效率较高。
7、静态内部类[推荐用]
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
以上就是实现单例模式的几种方式。推荐使用6.7两种方式。
4.总结
经过这么长的篇幅可算是把单例模式给讲完了。单例模式只要记住一点,那就是”独一无二的对象”就行了,好比你去球场打球,买了一张团队票,所有的人都用这张票进入球场。
第一次发文,可能有很多错误,如果有错误欢迎指正。下一篇打算讲解Java设计模式之观察者模式。