菜鸟成长系列-概述
菜鸟成长系列-面向对象的四大基础特性
菜鸟成长系列-多态、接口和抽象类
菜鸟成长系列-面向对象的6种设计原则
前面已经将设计模式中的基本内容撸了一下,今天开始正式开始设计模式系列的内容,因为网上也有很多关于设计模式的技术博客,从不同的角度对设计模式都做了很详细的解读;本系列的模式除了基本的概念和模型之外,还会结合java自身使用的和Spring中使用的一些案例来进行学习分析。
水平有限,如果存在不当之处,希望大家多提意见,灰常感谢!
设计模式中总体分为三类:
一、创建型(5):
- 工厂方法[Factory Method]
- 抽象工厂[Abstract Factory]
- 原型[Prototype]
- 建造者[Builder]
- 单例[Singleton]
还有一个简单工厂[Simple Factory],目前有两种,有的把单例模式作为这5种之一,有的是将简单工厂作为这5种之一。这里不做讨论,原则上两个都是,只是划分规则不同。
二、结构型(7)
- 适配器[Adapter]
- 桥接[Bridge]
- 组合[Composite]
- 装饰[Decorator]
- 外观[Facade]
- 享元[Flyweight]
- 代理[Proxy]
三、行为型(11)
- 策略[Strategy]
- 模板方法[Template method]
- 职责链[Chain of Responsibility]
- 迭代器[Iterator]
- 状态[State]
- 访问者[Visitor]
- 命令[Command]
- 备忘录[Memento]
- 观察者[Observer]
- 中介者[Mediator]
- 解释器[Interpreter]
单例模式
首先它是一种创建型模式,与其他模式区别在于:单例模式确保被创建的类只有一个实例对象,而且自行实例化并向整个系统提供这个实例。一般情况下我们称当前这个类为单例类。
从上面这段话中我们可以了解到,单例模式具备以下三个要点:
- 某个类只能有一个实例
- 必须自行创建这个实例[具体的对象创建由类本身负责,其他类不负责当前类的创建]
- 必须向整个系统提供这个实例[也就是说,当前类需要对外提供一个获取当前实例的一个方法,且该方法不能是私有的]
OK,来看单例模式的几种实现方式。
方式一:饿汉式
package com.glmapper.design.singleton;
/**
* 单例模式-饿汉式
* @author glmapper
* @date 2017年12月17日下午10:30:38
*/
public class EagerSingleton {
/**
* 内部直接提供一个eagerSingletonInstance;
* 我们知道,一般情况下,如果一个变量被static final修饰了,那么该变量将会被视为常量。
* 满足要点:自行创建
*/
private static final EagerSingleton eagerSingletonInstance = new EagerSingleton();
/**
* 提供一个私有的构造函数,这样其他类就无法通过new
* EagerSingleton()来获取对象了,同样也保证了当前类不可以被继承
* 满足要点:某个类只能有一个实例
*/
private EagerSingleton(){}
/**
* 对外提供一个获取实例的方法
* 满足要点:向整个系统提供这个实例
*/
public static EagerSingleton getInstance(){
return eagerSingletonInstance;
}
}
方式二:懒汉式
package com.glmapper.design.singleton;
/**
* 单例模式-懒汉式
* @author glmapper
* @date 2017年12月17日下午10:45:54
*/
public class LazySingleton {
//提供一个私有静态变量,注意区别与饿汉式中的static final。
private static LazySingleton lazySingletonInstance = null ;
//同样需要提供一个私有的构造方法,其作用与饿汉式中的作用一样
private LazySingleton(){}
/**
* 1.使用synchronized来保证线程同步
* 2.实例的具体创建被延迟到第一次调用getInstance方法时来进行
* 3.如果当前实例已经存在,不再重复创建
*/
public synchronized static LazySingleton getInstance(){
if (lazySingletonInstance == null) {
lazySingletonInstance = new LazySingleton();
}
return lazySingletonInstance;
}
}
饿汉式单例类在自己被加载时就自己实例化了,即便加载器是静态的,在饿汉式单例类被加载时仍会将自己实例化。从资源利用角度来说,这个比懒汉式单例类稍微的差一些。如果从速度和响应时间来看,饿汉式就会比懒汉式好一些。懒汉式在单例类进行实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题。
方式三:登记式
package com.glmapper.design.singleton;
import java.util.HashMap;
/**
* 单例模式-登记式
* @author glmapper
* @date 2017年12月17日下午10:58:36
*/
public class RegisterSingleton {
//提供一个私有的HashMap类型的registerSingletonInstance存储该RegisterSingleton类型的单例
private static HashMap registerSingletonInstance = new HashMap<>();
//通过static静态代码块来进行初始化RegisterSingleton当前类的实例,并将当前实例存入registerSingletonInstance
static {
RegisterSingleton singleton = new RegisterSingleton();
registerSingletonInstance.put(singleton.getClass().getName(), singleton);
}
/**
* 注意区别,此处提供的是非private类型的,说明当前类可以被继承
*/
protected RegisterSingleton(){}
/**
* 获取实例的方法
*/
public static RegisterSingleton getInstance(String name){
//如果name为空,则那么默认为当前类的全限定名
if (name == null) {
name ="com.glmapper.design.singleton.RegisterSingleton";
}
//如果map中没有查询到指定的单例,则将通过Class.forName(name)来创建一个实例对象,并存入map中
if (registerSingletonInstance.get(name)==null) {
try {
registerSingletonInstance.put(name, Class.forName(name).newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
//返回实例
return (RegisterSingleton) registerSingletonInstance.get(name);
}
}
登记式单例是Gof为了克服饿汉式和懒汉式单例类均不可被继承的缺点而设计的。
package com.glmapper.design.singleton;
/**
* 登记式-单例-子类
* @author glmapper
* @date 2017年12月17日下午11:14:03
*
*/
public class ChildRegisterSingleton extends RegisterSingleton
{
/**
* 由于子类必须允许父类以构造方法调用产生实例,因此,子类的构造方法必须
* 是public类型的。但是这样一来,就等于说可以允许以new
* ChildRegisterSingleton()的方式产生实例,而不必在父类的登记中。
*/
public ChildRegisterSingleton(){}
//客户端测试获取实例
public static void main(String[] args) {
ChildRegisterSingleton crs1 = (ChildRegisterSingleton) getInstance(
"com.glmapper.design.singleton.ChildRegisterSingleton");
ChildRegisterSingleton crs2 = (ChildRegisterSingleton) getInstance(
"com.glmapper.design.singleton.ChildRegisterSingleton");
System.out.println(crs1 == crs2);
}
}
返回:true 这个同志们可以自行验证,肯定是一样的。但是不能使用new,
因为前提约束是,需在父类中登记的才是单例。
方式四:双重检测模式,双重检测方式在某些书上或者文献中说对于java语言来说是不成立的,但是目前确实是通过某种技巧完成了在java中使用双重检测机制的单例模式的实现,;这种技巧后面来说;关于为什么java语言对于双重检测成例不成立,大家可以在[BLOCH01]文献中看下具体情况。
先来看一个单线程模式下的情况:
package com.glmapper.design.singleton;
/**
* 一个错误的单例例子
* @author glmapper
* @date 2017年12月17日下午11:53:04
*/
public class DoubleCheckSingleton {
private static DoubleCheckSingleton instance=null;
public static DoubleCheckSingleton getDoubleCheckSingleton(){
if (instance == null) {
instance = new DoubleCheckSingleton();
}
return instance;
}
}
这个很明显是一个错误的例子,对于A/B两个线程,因为step 1并没有使用同步策略,因此线程A/B可能会同时进行// step 2,这样的话,就会可能创建两个对象。那么正确的方式如下:使用synchronized关键字来保证同步。
package com.glmapper.design.singleton;
/**
* 这是一个正确的打开方式哦。。。
* @author glmapper
* @date 2017年12月17日下午11:53:04
*/
public class DoubleCheckSingleton {
private static DoubleCheckSingleton instance=null;
//使用synchronized来保证getDoubleCheckSingleton同一时刻只能被一个线程访问
public synchronized static DoubleCheckSingleton getDoubleCheckSingleton(){
if (instance == null) {
instance = new DoubleCheckSingleton();
}
return instance;
}
}
这种方式虽然保证了线程安全性,但是也存在另外一种问题:同步化操作仅仅在instance首次初始化操作之前会起到作用,如果instance已经完成了初始化,对于getDoubleCheckSingleton每一次调用来说都会阻塞其他线程,造成一个不必要的瓶颈。那我们就通过使用更加细粒度化的锁,来适当的减小额外的开销。OK,下面再来一个错误的例子:
package com.glmapper.design.singleton;
/**
* 一个错误的单例例子
* @author glmapper
* @date 2017年12月17日下午11:53:04
*/
public class DoubleCheckSingleton {
private static DoubleCheckSingleton instance=null;
//使用synchronized来保证getDoubleCheckSingleton同一时刻只能被一个线程访问
public static DoubleCheckSingleton getDoubleCheckSingleton(){
if (instance == null) { //1
// B线程检测到uniqueInstance不为空
synchronized (DoubleCheckSingleton.class) { //2
if (instance == null) { //3
instance = new DoubleCheckSingleton();//4
// A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
}
}
}
// 后面B线程执行时将引发:对象尚未初始化错误。
return instance;//5
}
}
看起来没什么毛病呀?我们来分析,两个线程A和B,同时到达1,且都通过了1的检测。此时A到了4,B在2。此时B线程检测到instance不为空,A线程被指令重排了,刚好先赋值了;但还没执行完构造函数;再接下来B线程执行时将引发:对象尚未初始化错误(5)。
对于上面的问题,我们可以通过volatile关键字来修饰instance对象,来保证instance对象的内存可见性和防止指令重排序。这个也就是前面说到的“技巧”。
private static DoubleCheckSingleton instance=null;
改为:
private static volatile DoubleCheckSingleton instance=null;
本篇将单例模式的几种情况进行了分析。后面将会对将java中和Spring中所使用的单例场景进行具体的案例分析。
JAVA中的单例模式使用
JAVA中对于单例模式的使用最经典的就是RunTime这个类。
注释解读:每个Java应用程序都有一个Runtime类的单个实例,允许应用程序与运行应用程序的环境进行交互。 当前运行时可以从getRuntime方法获得。应用程序不能创建它自己的这个类的实例。
看过上篇文章的小伙伴可能比较清楚,这里RunTime使用的是懒汉式单例的方式来创建的。Runtime提供了一个静态工厂方法getRuntime方法用于获取Runtime实例。Runtime这个类的具体源码分析和只能此处不做分析。
Spring中的单例
Spring依赖注入Bean实例默认是单例的。Spring中bean的依赖注入都是依赖AbstractBeanFactory的getBean方法来完成的。那我们就来看看在getBean中都发生了什么。
org.springframework.beans.factory.suppor.AbstractBeanFactory
从上面这张图中我们啥也看不出,只知道在getBean中又调用了doGetBean方法(Spring中还有java源码中有很多类似的写法,好处在于我们可以通过子类继承,继而编写我们自己的处理逻辑)。OK,再来看看doGetBean方法。
来看下这个方法的注释:返回指定的bean可以共享或独立的实例 (谷歌+有道+百度)
- name:要检索的bean的名称
- requiredType:要检索的bean所需的类型
- args:如果使用静态工厂方法的显式参数创建原型,则使用参数。 在其他情况下使用非空args值是无效的。
- typeCheckOnly:获得实例是否是为了类型检查,而不是实际的使用
这个方法体内的代码非常的多,那么我们本文不是来学习Spring的,所以我们只看我们关心的部分,
为手工注册的singleton检查单例缓存。,从这个注释可以看出,此处就是我们获取实例的地方,再往下看。
此处和上面的getBean一样,也是通过模板方法的方式进行调用的。
OK,这里我们看到了获取单例实例的具体实现过程。
返回注册在给定名称下的(原始的)singleton对象。检查已经实例化的单例,并且还允许提前引用当前创建的单例(解析循环引用)。
这里使用的是饿汉式中的双重检测机制来实现的。
OK,至此单例模式的学习就结束了,下一篇文章将会介绍工厂模式(简单工厂,工厂方法,抽象工厂)。