This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
<audio id="audio" title="27 | 单例模式:如何创建单一对象优化系统性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/4f/750e7fa27d5a8d063589336d9447f34f.mp3"></audio>
你好,我是刘超。
从这一讲开始我们将一起探讨设计模式的性能调优。在《Design Patterns: Elements of Reusable Object-Oriented Software》一书中有23种设计模式的描述其中单例设计模式是最常用的设计模式之一。无论是在开源框架还是在我们的日常开发中单例模式几乎无处不在。
## 什么是单例模式?
它的核心在于,单例模式可以保证一个类仅创建一个实例,并提供一个访问它的全局访问点。
该模式有三个基本要点:一是这个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
结合这三点,我们来实现一个简单的单例:
```
//饿汉模式
public final class Singleton {
private static Singleton instance=new Singleton();//自行创建实例
private Singleton(){}//构造函数
public static Singleton getInstance(){//通过该函数向整个系统提供实例
return instance;
}
}
```
由于在一个系统中,一个类经常会被使用在不同的地方,通过单例模式,我们可以避免多次创建多个实例,从而节约系统资源。
## 饿汉模式
我们可以发现以上第一种实现单例的代码中使用了static修饰了成员变量instance所以该变量会在类初始化的过程中被收集进类构造器即&lt;clinit&gt;方法中。在多线程场景下JVM会保证只有一个线程能执行该类的&lt;clinit&gt;方法,其它线程将会被阻塞等待。
等到唯一的一次&lt;clinit&gt;方法执行完成,其它线程将不会再执行&lt;clinit&gt;方法转而执行自己的代码。也就是说static修饰了成员变量instance在多线程的情况下能保证只实例化一次。
这种方式实现的单例模式,在类初始化阶段就已经在堆内存中开辟了一块内存,用于存放实例化对象,所以也称为饿汉模式。
饿汉模式实现的单例的优点是可以保证多线程情况下实例的唯一性而且getInstance直接返回唯一实例性能非常高。
然而,在类成员变量比较多,或变量比较大的情况下,这种模式可能会在没有使用类对象的情况下,一直占用堆内存。试想下,如果一个第三方开源框架中的类都是基于饿汉模式实现的单例,这将会初始化所有单例类,无疑是灾难性的。
## 懒汉模式
懒汉模式就是为了避免直接加载类对象时提前创建对象的一种单例设计模式。该模式使用懒加载方式,只有当系统使用到类对象时,才会将实例加载到堆内存中。通过以下代码,我们可以简单地了解下懒加载的实现方式:
```
//懒汉模式
public final class Singleton {
private static Singleton instance= null;//不实例化
private Singleton(){}//构造函数
public static Singleton getInstance(){//通过该函数向整个系统提供实例
if(null == instance){//当instance为null时则实例化对象否则直接返回对象
instance = new Singleton();//实例化对象
}
return instance;//返回已存在的对象
}
}
```
以上代码在单线程下运行是没有问题的,但要运行在多线程下,就会出现实例化多个类对象的情况。这是怎么回事呢?
当线程A进入到if判断条件后开始实例化对象此时instance依然为null又有线程B进入到if判断条件中之后也会通过条件判断进入到方法里面创建一个实例对象。
所以我们需要对该方法进行加锁保证多线程情况下仅创建一个实例。这里我们使用Synchronized同步锁来修饰getInstance方法
```
//懒汉模式 + synchronized同步锁
public final class Singleton {
private static Singleton instance= null;//不实例化
private Singleton(){}//构造函数
public static synchronized Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//当instance为null时则实例化对象否则直接返回对象
instance = new Singleton();//实例化对象
}
return instance;//返回已存在的对象
}
}
```
但我们前面讲过,同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能。
还有每次请求获取类对象时都会通过getInstance()方法获取除了第一次为null其它每次请求基本都是不为null的。在没有加同步锁之前是因为if判断条件为null时才导致创建了多个实例。基于以上两点我们可以考虑将同步锁放在if条件里面这样就可以减少同步锁资源竞争。
```
//懒汉模式 + synchronized同步锁
public final class Singleton {
private static Singleton instance= null;//不实例化
private Singleton(){}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//当instance为null时则实例化对象否则直接返回对象
synchronized (Singleton.class){
instance = new Singleton();//实例化对象
}
}
return instance;//返回已存在的对象
}
}
```
看到这里你是不是觉得这样就可以了呢答案是依然会创建多个实例。这是因为当多个线程进入到if判断条件里虽然有同步锁但是进入到判断条件里面的线程依然会依次获取到锁创建对象然后再释放同步锁。所以我们还需要在同步锁里面再加一个判断条件
```
//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton {
private static Singleton instance= null;//不实例化
private Singleton(){}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第一次判断当instance为null时则实例化对象否则直接返回对象
synchronized (Singleton.class){//同步锁
if(null == instance){//第二次判断
instance = new Singleton();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}
```
以上这种方式通常被称为Double-Check它可以大大提高支持多线程的懒汉模式的运行性能。那这样做是不是就能保证万无一失了呢还会有什么问题吗
其实这里又跟Happens-Before规则和重排序扯上关系了这里我们先来简单了解下Happens-Before规则和重排序。
我们在第二期[加餐](https://time.geekbang.org/column/article/105756)中分享过编译器为了尽可能地减少寄存器的读取、存储次数会充分复用寄存器的存储值比如以下代码如果没有进行重排序优化正常的执行顺序是步骤1/2/3而在编译期间进行了重排序优化之后执行的步骤有可能就变成了步骤1/3/2这样就能减少一次寄存器的存取次数。
```
int a = 1;//步骤1加载a变量的内存地址到寄存器中加载1到寄存器中CPU通过mov指令把1写入到寄存器指定的内存中
int b = 2;//步骤2 加载b变量的内存地址到寄存器中加载2到寄存器中CPU通过mov指令把2写入到寄存器指定的内存中
a = a + 1;//步骤3 重新加载a变量的内存地址到寄存器中加载1到寄存器中CPU通过mov指令把1写入到寄存器指定的内存中
```
在 JMM 中重排序是十分重要的一环特别是在并发编程中。如果JVM可以对它们进行任意排序以提高程序性能也可能会给并发编程带来一系列的问题。例如我上面讲到的Double-Check的单例问题假设类中有其它的属性也需要实例化这个时候除了要实例化单例类本身还需要对其它属性也进行实例化
```
//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton {
private static Singleton instance= null;//不实例化
public List&lt;String&gt; list = null;//list属性
private Singleton(){
list = new ArrayList&lt;String&gt;();
}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第一次判断当instance为null时则实例化对象否则直接返回对象
synchronized (Singleton.class){//同步锁
if(null == instance){//第二次判断
instance = new Singleton();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}
```
在执行instance = new Singleton();代码时,正常情况下,实例过程这样的:
- 给 Singleton 分配内存;
- 调用 Singleton 的构造函数来初始化成员变量;
- 将 Singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)。
如果虚拟机发生了重排序优化这个时候步骤3可能发生在步骤2之前。如果初始化线程刚好完成步骤3而步骤2没有进行时则刚好有另一个线程到了第一次判断这个时候判断为非null并返回对象使用这个时候实际没有完成其它属性的构造因此使用这个属性就很可能会导致异常。在这里Synchronized只能保证可见性、原子性无法保证执行的顺序。
这个时候就体现出Happens-Before规则的重要性了。通过字面意思你可能会误以为是前一个操作发生在后一个操作之前。然而真正的意思是前一个操作的结果可以被后续的操作获取。这条规则规范了编译器对程序的重排序优化。
我们知道volatile关键字可以保证线程间变量的可见性简单地说就是当线程A对变量X进行修改后在线程A后面执行的其它线程就能看到变量X的变动。除此之外volatile在JDK1.5之后还有一个作用就是阻止局部重排序的发生也就是说volatile变量的操作指令都不会被重排序。所以使用volatile修饰instance之后Double-Check懒汉单例模式就万无一失了。
```
//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton {
private volatile static Singleton instance= null;//不实例化
public List&lt;String&gt; list = null;//list属性
private Singleton(){
list = new ArrayList&lt;String&gt;();
}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第一次判断当instance为null时则实例化对象否则直接返回对象
synchronized (Singleton.class){//同步锁
if(null == instance){//第二次判断
instance = new Singleton();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}
```
## 通过内部类实现
以上这种同步锁+Double-Check的实现方式相对来说复杂且加了同步锁那有没有稍微简单一点儿的可以实现线程安全的懒加载方式呢
我们知道在饿汉模式中我们使用了static修饰了成员变量instance所以该变量会在类初始化的过程中被收集进类构造器即&lt;clinit&gt;方法中。在多线程场景下JVM会保证只有一个线程能执行该类的&lt;clinit&gt;方法,其它线程将会被阻塞等待。这种方式可以保证内存的可见性、顺序性以及原子性。
如果我们在Singleton类中创建一个内部类来实现成员变量的初始化则可以避免多线程下重复创建对象的情况发生。这种方式只有在第一次调用getInstance()方法时才会加载InnerSingleton类而只有在加载InnerSingleton类之后才会实例化创建对象。具体实现如下
```
//懒汉模式 内部类实现
public final class Singleton {
public List&lt;String&gt; list = null;// list属性
private Singleton() {//构造函数
list = new ArrayList&lt;String&gt;();
}
// 内部类实现
public static class InnerSingleton {
private static Singleton instance=new Singleton();//自行创建实例
}
public static Singleton getInstance() {
return InnerSingleton.instance;// 返回内部类中的静态变量
}
}
```
## 总结
单例的实现方式其实有很多,但总结起来就两种:饿汉模式和懒汉模式,我们可以根据自己的需求来做选择。
如果我们在程序启动后一定会加载到类那么用饿汉模式实现的单例简单又实用如果我们是写一些工具类则优先考虑使用懒汉模式因为很多项目可能会引用到jar包但未必会使用到这个工具类懒汉模式实现的单例可以避免提前被加载到内存中占用系统资源。
## 思考题
除了以上那些实现单例的方式,你还知道其它实现方式吗?
期待在留言区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,414 @@
<audio id="audio" title="28 | 原型模式与享元模式:提升系统性能的利器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e0/81/e0f143c5dabddfccbd50e185ae004181.mp3"></audio>
你好,我是刘超。
原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。这么看,你会不会觉得两个模式有点相互矛盾呢?
其实不然,它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循环体中赋值一个对象,此时我们就可以采用原型模式来优化对象的创建过程;而在有些场景下,我们则可以避免重复创建多个实例,在内存中共享对象就好了。
今天我们就来看看这两种模式的适用场景,了解了这些你就可以更高效地使用它们提升系统性能了。
## 原型模式
我们先来了解下原型模式的实现。原型模式是通过给出一个原型对象来指明所创建的对象的类型,然后使用自身实现的克隆接口来复制这个原型对象,该模式就是用这种方式来创建出更多同类型的对象。
使用这种方式创建新的对象的话就无需再通过new实例化来创建对象了。这是因为Object类的clone方法是一个本地方法它可以直接操作内存中的二进制流所以性能相对new实例化来说更佳。
### 实现原型模式
我们现在通过一个简单的例子来实现一个原型模式:
```
//实现Cloneable 接口的原型抽象类Prototype
class Prototype implements Cloneable {
//重写clone方法
public Prototype clone(){
Prototype prototype = null;
try{
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return prototype;
}
}
//实现原型类
class ConcretePrototype extends Prototype{
public void show(){
System.out.println(&quot;原型模式实现类&quot;);
}
}
public class Client {
public static void main(String[] args){
ConcretePrototype cp = new ConcretePrototype();
for(int i=0; i&lt; 10; i++){
ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
clonecp.show();
}
}
}
```
**要实现一个原型类,需要具备三个条件:**
- 实现Cloneable接口Cloneable接口与序列化接口的作用类似它只是告诉虚拟机可以安全地在实现了这个接口的类上使用clone方法。在JVM中只有实现了Cloneable接口的类才可以被拷贝否则会抛出CloneNotSupportedException异常。
- 重写Object类中的clone方法在Java中所有类的父类都是Object类而Object类中有一个clone方法作用是返回对象的一个拷贝。
- 在重写的clone方法中调用super.clone()默认情况下类不具备复制对象的能力需要调用super.clone()来实现。
从上面我们可以看出原型模式的主要特征就是使用clone方法复制一个对象。通常有些人会误以为 Object a=new Object();Object b=a; 这种形式就是一种对象复制的过程然而这种复制只是对象引用的复制也就是a和b对象指向了同一个内存地址如果b修改了a的值也就跟着被修改了。
我们可以通过一个简单的例子来看看普通的对象复制问题:
```
class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setName(&quot;test1&quot;);
Student stu2 = stu1;
stu2.setName(&quot;test2&quot;);
System.out.println(&quot;学生1:&quot; + stu1.getName());
System.out.println(&quot;学生2:&quot; + stu2.getName());
}
}
```
如果是复制对象,此时打印的日志应该为:
```
学生1:test1
学生2:test2
```
然而,实际上是:
```
学生1:test2
学生2:test2
```
通过clone方法复制的对象才是真正的对象复制clone方法赋值的对象完全是一个独立的对象。刚刚讲过了Object类的clone方法是一个本地方法它直接操作内存中的二进制流特别是复制大对象时性能的差别非常明显。我们可以用 clone 方法再实现一遍以上例子。
```
//学生类实现Cloneable接口
class Student implements Cloneable{
private String name; //姓名
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
//重写clone方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student(); //创建学生1
stu1.setName(&quot;test1&quot;);
Student stu2 = stu1.clone(); //通过克隆创建学生2
stu2.setName(&quot;test2&quot;);
System.out.println(&quot;学生1:&quot; + stu1.getName());
System.out.println(&quot;学生2:&quot; + stu2.getName());
}
}
```
运行结果:
```
学生1:test1
学生2:test2
```
### 深拷贝和浅拷贝
在调用super.clone()方法之后首先会检查当前对象所属的类是否支持clone也就是看该类是否实现了Cloneable接口。
如果支持则创建当前对象所属类的一个新对象并对该对象进行初始化使得新对象的成员变量的值与当前对象的成员变量的值一模一样但对于其它对象的引用以及List等类型的成员属性则只能复制这些对象的引用了。所以简单调用super.clone()这种克隆对象方式,就是一种浅拷贝。
所以当我们在使用clone()方法实现对象的克隆时,就需要注意浅拷贝带来的问题。我们再通过一个例子来看看浅拷贝。
```
//定义学生类
class Student implements Cloneable{
private String name; //学生姓名
private Teacher teacher; //定义老师类
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
//重写克隆方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
//定义老师类
class Teacher implements Cloneable{
private String name; //老师姓名
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
//重写克隆方法,堆老师类进行克隆
public Teacher clone() {
Teacher teacher= null;
try {
teacher= (Teacher) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
public class Test {
public static void main(String args[]) {
Teacher teacher = new Teacher (); //定义老师1
teacher.setName(&quot;刘老师&quot;);
Student stu1 = new Student(); //定义学生1
stu1.setName(&quot;test1&quot;);
stu1.setTeacher(teacher);
Student stu2 = stu1.clone(); //定义学生2
stu2.setName(&quot;test2&quot;);
stu2.getTeacher().setName(&quot;王老师&quot;);//修改老师
System.out.println(&quot;学生&quot; + stu1.getName + &quot;的老师是:&quot; + stu1.getTeacher().getName);
System.out.println(&quot;学生&quot; + stu1.getName + &quot;的老师是:&quot; + stu2.getTeacher().getName);
}
}
```
运行结果:
```
学生test1的老师是王老师
学生test2的老师是王老师
```
观察以上运行结果我们可以发现在我们给学生2修改老师的时候学生1的老师也跟着被修改了。这就是浅拷贝带来的问题。
我们可以通过深拷贝来解决这种问题,其实深拷贝就是基于浅拷贝来递归实现具体的每个对象,代码如下:
```
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
Teacher teacher = this.teacher.clone();//克隆teacher对象
student.setTeacher(teacher);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
```
### 适用场景
前面我详述了原型模式的实现原理,那到底什么时候我们要用它呢?
在一些重复创建对象的场景下我们就可以使用原型模式来提高对象的创建性能。例如我在开头提到的循环体内创建对象时我们就可以考虑用clone的方式来实现。
例如:
```
for(int i=0; i&lt;list.size(); i++){
Student stu = new Student();
...
}
```
我们可以优化为:
```
Student stu = new Student();
for(int i=0; i&lt;list.size(); i++){
Student stu1 = (Student)stu.clone();
...
}
```
除此之外原型模式在开源框架中的应用也非常广泛。例如Spring中@Service默认都是单例的。用了私有全局变量若不想影响下次注入或每次上下文获取bean就需要用到原型模式我们可以通过以下注解来实现@Scope(“prototype”)。
## 享元模式
享元模式是运用共享技术有效地最大限度地复用细粒度对象的一种模式。该模式中,以对象的信息状态划分,可以分为内部数据和外部数据。内部数据是对象可以共享出来的信息,这些信息不会随着系统的运行而改变;外部数据则是在不同运行时被标记了不同的值。
享元模式一般可以分为三个角色,分别为 Flyweight抽象享元类、ConcreteFlyweight具体享元类和 FlyweightFactory享元工厂类。抽象享元类通常是一个接口或抽象类向外界提供享元对象的内部数据或外部数据具体享元类是指具体实现内部数据共享的类享元工厂类则是主要用于创建和管理享元对象的工厂类。
### 实现享元模式
我们还是通过一个简单的例子来实现一个享元模式:
```
//抽象享元类
interface Flyweight {
//对外状态对象
void operation(String name);
//对内对象
String getType();
}
```
```
//具体享元类
class ConcreteFlyweight implements Flyweight {
private String type;
public ConcreteFlyweight(String type) {
this.type = type;
}
@Override
public void operation(String name) {
System.out.printf(&quot;[类型(内在状态)] - [%s] - [名字(外在状态)] - [%s]\n&quot;, type, name);
}
@Override
public String getType() {
return type;
}
}
```
```
//享元工厂类
class FlyweightFactory {
private static final Map&lt;String, Flyweight&gt; FLYWEIGHT_MAP = new HashMap&lt;&gt;();//享元池,用来存储享元对象
public static Flyweight getFlyweight(String type) {
if (FLYWEIGHT_MAP.containsKey(type)) {//如果在享元池中存在对象,则直接获取
return FLYWEIGHT_MAP.get(type);
} else {//在响应池不存在,则新创建对象,并放入到享元池
ConcreteFlyweight flyweight = new ConcreteFlyweight(type);
FLYWEIGHT_MAP.put(type, flyweight);
return flyweight;
}
}
}
```
```
public class Client {
public static void main(String[] args) {
Flyweight fw0 = FlyweightFactory.getFlyweight(&quot;a&quot;);
Flyweight fw1 = FlyweightFactory.getFlyweight(&quot;b&quot;);
Flyweight fw2 = FlyweightFactory.getFlyweight(&quot;a&quot;);
Flyweight fw3 = FlyweightFactory.getFlyweight(&quot;b&quot;);
fw1.operation(&quot;abc&quot;);
System.out.printf(&quot;[结果(对象对比)] - [%s]\n&quot;, fw0 == fw2);
System.out.printf(&quot;[结果(内在状态)] - [%s]\n&quot;, fw1.getType());
}
}
```
输出结果:
```
[类型(内在状态)] - [b] - [名字(外在状态)] - [abc]
[结果(对象对比)] - [true]
[结果(内在状态)] - [b]
```
观察以上代码运行结果,我们可以发现:如果对象已经存在于享元池中,则不会再创建该对象了,而是共用享元池中内部数据一致的对象。这样就减少了对象的创建,同时也节省了同样内部数据的对象所占用的内存空间。
### 适用场景
享元模式在实际开发中的应用也非常广泛。例如Java的String字符串在一些字符串常量中会共享常量池中字符串对象从而减少重复创建相同值对象占用内存空间。代码如下
```
String s1 = &quot;hello&quot;;
String s2 = &quot;hello&quot;;
System.out.println(s1==s2);//true
```
还有在日常开发中的应用。例如线程池就是享元模式的一种实现将商品存储在应用服务的缓存中那么每当用户获取商品信息时则不需要每次都从redis缓存或者数据库中获取商品信息并在内存中重复创建商品信息了。
## 总结
通过以上讲解,相信你对原型模式和享元模式已经有了更清楚的了解了。两种模式无论是在开源框架,还是在实际开发中,应用都十分广泛。
在不得已需要重复创建大量同一对象时我们可以使用原型模式通过clone方法复制对象这种方式比用new和序列化创建对象的效率要高在创建对象时如果我们可以共用对象的内部数据那么通过享元模式共享相同的内部数据的对象就可以减少对象的创建实现系统调优。
## 思考题
上一讲的单例模式和这一讲的享元模式都是为了避免重复创建对象,你知道这两者的区别在哪儿吗?
期待在留言区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。
<img src="https://static001.geekbang.org/resource/image/bb/67/bbe343640d6b708832c4133ec53ed967.jpg" alt="unpreview">

View File

@@ -0,0 +1,536 @@
<audio id="audio" title="29 | 如何使用设计模式优化并发编程?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a5/3f/a577e2871067b9b8d5ec54fbf241483f.mp3"></audio>
你好,我是刘超。
在我们使用多线程编程时,很多时候需要根据业务场景设计一套业务功能。其实,在多线程编程中,本身就存在很多成熟的功能设计模式,学好它们,用好它们,那就是如虎添翼了。今天我就带你了解几种并发编程中常用的设计模式。
## 线程上下文设计模式
线程上下文是指贯穿线程整个生命周期的对象中的一些全局信息。例如我们比较熟悉的Spring中的ApplicationContext就是一个关于上下文的类它在整个系统的生命周期中保存了配置信息、用户信息以及注册的bean等上下文信息。
这样的解释可能有点抽象,我们不妨通过一个具体的案例,来看看到底在什么的场景下才需要上下文呢?
在执行一个比较长的请求任务时,这个请求可能会经历很多层的方法调用,假设我们需要将最开始的方法的中间结果传递到末尾的方法中进行计算,一个简单的实现方式就是在每个函数中新增这个中间结果的参数,依次传递下去。代码如下:
```
public class ContextTest {
// 上下文类
public class Context {
private String name;
private long id
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
// 设置上下文名字
public class QueryNameAction {
public void execute(Context context) {
try {
Thread.sleep(1000L);
String name = Thread.currentThread().getName();
context.setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 设置上下文ID
public class QueryIdAction {
public void execute(Context context) {
try {
Thread.sleep(1000L);
long id = Thread.currentThread().getId();
context.setId(id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 执行方法
public class ExecutionTask implements Runnable {
private QueryNameAction queryNameAction = new QueryNameAction();
private QueryIdAction queryIdAction = new QueryIdAction();
@Override
public void run() {
final Context context = new Context();
queryNameAction.execute(context);
System.out.println(&quot;The name query successful&quot;);
queryIdAction.execute(context);
System.out.println(&quot;The id query successful&quot;);
System.out.println(&quot;The Name is &quot; + context.getName() + &quot; and id &quot; + context.getId());
}
}
public static void main(String[] args) {
IntStream.range(1, 5).forEach(i -&gt; new Thread(new ContextTest().new ExecutionTask()).start());
}
}
```
执行结果:
```
The name query successful
The name query successful
The name query successful
The name query successful
The id query successful
The id query successful
The id query successful
The id query successful
The Name is Thread-1 and id 11
The Name is Thread-2 and id 12
The Name is Thread-3 and id 13
The Name is Thread-0 and id 10
```
然而这种方式太笨拙了每次调用方法时都需要传入Context作为参数而且影响一些中间公共方法的封装。
那能不能设置一个全局变量呢?如果是在多线程情况下,需要考虑线程安全,这样的话就又涉及到了锁竞争。
除了以上这些方法其实我们还可以使用ThreadLocal实现上下文。ThreadLocal是线程本地变量可以实现多线程的数据隔离。ThreadLocal为每一个使用该变量的线程都提供一份独立的副本线程间的数据是隔离的每一个线程只能访问各自内部的副本变量。
ThreadLocal中有三个常用的方法set、get、initialValue我们可以通过以下一个简单的例子来看看ThreadLocal的使用
```
private void testThreadLocal() {
Thread t = new Thread() {
ThreadLocal&lt;String&gt; mStringThreadLocal = new ThreadLocal&lt;String&gt;();
@Override
public void run() {
super.run();
mStringThreadLocal.set(&quot;test&quot;);
mStringThreadLocal.get();
}
};
t.start();
}
```
接下来我们使用ThreadLocal来重新实现最开始的上下文设计。你会发现我们在两个方法中并没有通过变量来传递上下文只是通过ThreadLocal获取了当前线程的上下文信息
```
public class ContextTest {
// 上下文类
public static class Context {
private String name;
private long id;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
// 复制上下文到ThreadLocal中
public final static class ActionContext {
private static final ThreadLocal&lt;Context&gt; threadLocal = new ThreadLocal&lt;Context&gt;() {
@Override
protected Context initialValue() {
return new Context();
}
};
public static ActionContext getActionContext() {
return ContextHolder.actionContext;
}
public Context getContext() {
return threadLocal.get();
}
// 获取ActionContext单例
public static class ContextHolder {
private final static ActionContext actionContext = new ActionContext();
}
}
// 设置上下文名字
public class QueryNameAction {
public void execute() {
try {
Thread.sleep(1000L);
String name = Thread.currentThread().getName();
ActionContext.getActionContext().getContext().setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 设置上下文ID
public class QueryIdAction {
public void execute() {
try {
Thread.sleep(1000L);
long id = Thread.currentThread().getId();
ActionContext.getActionContext().getContext().setId(id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 执行方法
public class ExecutionTask implements Runnable {
private QueryNameAction queryNameAction = new QueryNameAction();
private QueryIdAction queryIdAction = new QueryIdAction();
@Override
public void run() {
queryNameAction.execute();//设置线程名
System.out.println(&quot;The name query successful&quot;);
queryIdAction.execute();//设置线程ID
System.out.println(&quot;The id query successful&quot;);
System.out.println(&quot;The Name is &quot; + ActionContext.getActionContext().getContext().getName() + &quot; and id &quot; + ActionContext.getActionContext().getContext().getId())
}
}
public static void main(String[] args) {
IntStream.range(1, 5).forEach(i -&gt; new Thread(new ContextTest().new ExecutionTask()).start());
}
}
```
运行结果:
```
The name query successful
The name query successful
The name query successful
The name query successful
The id query successful
The id query successful
The id query successful
The id query successful
The Name is Thread-2 and id 12
The Name is Thread-0 and id 10
The Name is Thread-1 and id 11
The Name is Thread-3 and id 13
```
## Thread-Per-Message设计模式
Thread-Per-Message设计模式翻译过来的意思就是每个消息一个线程的意思。例如我们在处理Socket通信的时候通常是一个线程处理事件监听以及I/O读写如果I/O读写操作非常耗时这个时候便会影响到事件监听处理事件。
这个时候Thread-Per-Message模式就可以很好地解决这个问题一个线程监听I/O事件每当监听到一个I/O事件则交给另一个处理线程执行I/O操作。下面我们还是通过一个例子来学习下该设计模式的实现。
```
//IO处理
public class ServerHandler implements Runnable{
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader in = null;
PrintWriter out = null;
String msg = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
while ((msg = in.readLine()) != null &amp;&amp; msg.length()!=0) {//当连接成功后在此等待接收消息(挂起,进入阻塞状态)
System.out.println(&quot;server received : &quot; + msg);
out.print(&quot;received~\n&quot;);
out.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
```
```
//Socket启动服务
public class Server {
private static int DEFAULT_PORT = 12345;
private static ServerSocket server;
public static void start() throws IOException {
start(DEFAULT_PORT);
}
public static void start(int port) throws IOException {
if (server != null) {
return;
}
try {
//启动服务
server = new ServerSocket(port);
// 通过无线循环监听客户端连接
while (true) {
Socket socket = server.accept();
// 当有新的客户端接入时,会执行下面的代码
long start = System.currentTimeMillis();
new Thread(new ServerHandler(socket)).start();
long end = System.currentTimeMillis();
System.out.println(&quot;Spend time is &quot; + (end - start));
}
} finally {
if (server != null) {
System.out.println(&quot;服务器已关闭。&quot;);
server.close();
}
}
}
public static void main(String[] args) throws InterruptedException{
// 运行服务端
new Thread(new Runnable() {
public void run() {
try {
Server.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
```
以上我们是完成了一个使用Thread-Per-Message设计模式实现的Socket服务端的代码。但这里是有一个问题的你发现了吗
使用这种设计模式如果遇到大的高并发就会出现严重的性能问题。如果针对每个I/O请求都创建一个线程来处理在有大量请求同时进来时就会创建大量线程而此时JVM有可能会因为无法处理这么多线程而出现内存溢出的问题。
退一步讲,即使是不会有大量线程的场景,每次请求过来也都需要创建和销毁线程,这对系统来说,也是一笔不小的性能开销。
面对这种情况,我们可以使用线程池来代替线程的创建和销毁,这样就可以避免创建大量线程而带来的性能问题,是一种很好的调优方法。
## Worker-Thread设计模式
这里的Worker是工人的意思代表在Worker Thread设计模式中会有一些工人线程不断轮流处理过来的工作当没有工作时工人则会处于等待状态直到有新的工作进来。除了工人角色Worker Thread设计模式中还包括了流水线和产品。
这种设计模式相比Thread-Per-Message设计模式可以减少频繁创建、销毁线程所带来的性能开销还有无限制地创建线程所带来的内存溢出风险。
我们可以假设一个场景来看下该模式的实现通过Worker Thread设计模式来完成一个物流分拣的作业。
假设一个物流仓库的物流分拣流水线上有8个机器人它们不断从流水线上获取包裹并对其进行包装送其上车。当仓库中的商品被打包好后会投放到物流分拣流水线上而不是直接交给机器人机器人会再从流水线中随机分拣包裹。代码如下
```
//包裹类
public class Package {
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void execute() {
System.out.println(Thread.currentThread().getName()+&quot; executed &quot;+this);
}
}
```
```
//流水线
public class PackageChannel {
private final static int MAX_PACKAGE_NUM = 100;
private final Package[] packageQueue;
private final Worker[] workerPool;
private int head;
private int tail;
private int count;
public PackageChannel(int workers) {
this.packageQueue = new Package[MAX_PACKAGE_NUM];
this.head = 0;
this.tail = 0;
this.count = 0;
this.workerPool = new Worker[workers];
this.init();
}
private void init() {
for (int i = 0; i &lt; workerPool.length; i++) {
workerPool[i] = new Worker(&quot;Worker-&quot; + i, this);
}
}
/**
* push switch to start all of worker to work
*/
public void startWorker() {
Arrays.asList(workerPool).forEach(Worker::start);
}
public synchronized void put(Package packagereq) {
while (count &gt;= packageQueue.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.packageQueue[tail] = packagereq;
this.tail = (tail + 1) % packageQueue.length;
this.count++;
this.notifyAll();
}
public synchronized Package take() {
while (count &lt;= 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Package request = this.packageQueue[head];
this.head = (this.head + 1) % this.packageQueue.length;
this.count--;
this.notifyAll();
return request;
}
}
```
```
//机器人
public class Worker extends Thread{
private static final Random random = new Random(System.currentTimeMillis());
private final PackageChannel channel;
public Worker(String name, PackageChannel channel) {
super(name);
this.channel = channel;
}
@Override
public void run() {
while (true) {
channel.take().execute();
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
```
```
public class Test {
public static void main(String[] args) {
//新建8个工人
final PackageChannel channel = new PackageChannel(8);
//开始工作
channel.startWorker();
//为流水线添加包裹
for(int i=0; i&lt;100; i++) {
Package packagereq = new Package();
packagereq.setAddress(&quot;test&quot;);
packagereq.setName(&quot;test&quot;);
channel.put(packagereq);
}
}
}
```
我们可以看到这里有8个工人在不断地分拣仓库中已经包装好的商品。
## 总结
平时如果需要传递或隔离一些线程变量时我们可以考虑使用上下文设计模式。在数据库读写分离的业务场景中则经常会用到ThreadLocal实现动态切换数据源操作。但在使用ThreadLocal时我们需要注意内存泄漏问题在之前的[第25讲](https://time.geekbang.org/column/article/109201)中,我们已经讨论过这个问题了。
当主线程处理每次请求都非常耗时时,就可能出现阻塞问题,这时候我们可以考虑将主线程业务分工到新的业务线程中,从而提高系统的并行处理能力。而 Thread-Per-Message 设计模式以及 Worker-Thread 设计模式则都是通过多线程分工来提高系统并行处理能力的设计模式。
## 思考题
除了以上这些多线程的设计模式,平时你还使用过其它的设计模式来优化多线程业务吗?
期待在留言区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,387 @@
<audio id="audio" title="30 | 生产者消费者模式:电商库存设计优化" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/46/cb/4626e35f79a15ac12587ea4213dfb0cb.mp3"></audio>
你好,我是刘超。
生产者消费者模式,在之前的一些案例中,我们是有使用过的,相信你有一定的了解。这个模式是一个十分经典的多线程并发协作模式,生产者与消费者是通过一个中间容器来解决强耦合关系,并以此来实现不同的生产与消费速度,从而达到缓冲的效果。
使用生产者消费者模式,可以提高系统的性能和吞吐量,今天我们就来看看该模式的几种实现方式,还有其在电商库存中的应用。
## Object的wait/notify/notifyAll实现生产者消费者
在[第16讲](https://time.geekbang.org/column/article/102974)中我就曾介绍过使用Object的wait/notify/notifyAll实现生产者消费者模式这种方式是基于Object的wait/notify/notifyAll与对象监视器Monitor实现线程间的等待和通知。
还有,在[第12讲](https://time.geekbang.org/column/article/101244)中我也详细讲解过Monitor的工作原理借此我们可以得知这种方式实现的生产者消费者模式是基于内核来实现的有可能会导致大量的上下文切换所以性能并不是最理想的。
## Lock中Condition的await/signal/signalAll实现生产者消费者
相对Object类提供的wait/notify/notifyAll方法实现的生产者消费者模式我更推荐使用java.util.concurrent包提供的Lock &amp;&amp; Condition实现的生产者消费者模式。
在接口Condition类中定义了await/signal/signalAll 方法其作用与Object的wait/notify/notifyAll方法类似该接口类与显示锁Lock配合实现对线程的阻塞和唤醒操作。
我在[第13讲](https://time.geekbang.org/column/article/101651)中详细讲到了显示锁显示锁ReentrantLock或ReentrantReadWriteLock都是基于AQS实现的而在AQS中有一个内部类ConditionObject实现了Condition接口。
我们知道AQS中存在一个同步队列CLH队列当一个线程没有获取到锁时就会进入到同步队列中进行阻塞如果被唤醒后获取到锁则移除同步队列。
除此之外AQS中还存在一个条件队列通过addWaiter方法可以将await()方法调用的线程放入到条件队列中线程进入等待状态。当调用signal以及signalAll 方法后线程将会被唤醒并从条件队列中删除之后进入到同步队列中。条件队列是通过一个单向链表实现的所以Condition支持多个等待队列。
由上可知Lock中Condition的await/signal/signalAll实现的生产者消费者模式是基于Java代码层实现的所以在性能和扩展性方面都更有优势。
下面来看一个案例,我们通过一段代码来实现一个商品库存的生产和消费。
```
public class LockConditionTest {
private LinkedList&lt;String&gt; product = new LinkedList&lt;String&gt;();
private int maxInventory = 10; // 最大库存
private Lock lock = new ReentrantLock();// 资源锁
private Condition condition = lock.newCondition();// 库存非满和非空条件
/**
* 新增商品库存
* @param e
*/
public void produce(String e) {
lock.lock();
try {
while (product.size() == maxInventory) {
condition.await();
}
product.add(e);
System.out.println(&quot;放入一个商品库存,总库存为:&quot; + product.size());
condition.signalAll();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 消费商品
* @return
*/
public String consume() {
String result = null;
lock.lock();
try {
while (product.size() == 0) {
condition.await();
}
result = product.removeLast();
System.out.println(&quot;消费一个商品,总库存为:&quot; + product.size());
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return result;
}
/**
* 生产者
* @author admin
*
*/
private class Producer implements Runnable {
public void run() {
for (int i = 0; i &lt; 20; i++) {
produce(&quot;商品&quot; + i);
}
}
}
/**
* 消费者
* @author admin
*
*/
private class Customer implements Runnable {
public void run() {
for (int i = 0; i &lt; 20; i++) {
consume();
}
}
}
public static void main(String[] args) {
LockConditionTest lc = new LockConditionTest();
new Thread(lc.new Producer()).start();
new Thread(lc.new Customer()).start();
new Thread(lc.new Producer()).start();
new Thread(lc.new Customer()).start();
}
}
```
看完案例,请你思考下,我们对此还有优化的空间吗?
从代码中应该不难发现生产者和消费者都在竞争同一把锁而实际上两者没有同步关系由于Condition能够支持多个等待队列以及不响应中断 所以我们可以将生产者和消费者的等待条件和锁资源分离,从而进一步优化系统并发性能,代码如下:
```
private LinkedList&lt;String&gt; product = new LinkedList&lt;String&gt;();
private AtomicInteger inventory = new AtomicInteger(0);//实时库存
private int maxInventory = 10; // 最大库存
private Lock consumerLock = new ReentrantLock();// 资源锁
private Lock productLock = new ReentrantLock();// 资源锁
private Condition notEmptyCondition = consumerLock.newCondition();// 库存满和空条件
private Condition notFullCondition = productLock.newCondition();// 库存满和空条件
/**
* 新增商品库存
* @param e
*/
public void produce(String e) {
productLock.lock();
try {
while (inventory.get() == maxInventory) {
notFullCondition.await();
}
product.add(e);
System.out.println(&quot;放入一个商品库存,总库存为:&quot; + inventory.incrementAndGet());
if(inventory.get()&lt;maxInventory) {
notFullCondition.signalAll();
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
productLock.unlock();
}
if(inventory.get()&gt;0) {
try {
consumerLock.lockInterruptibly();
notEmptyCondition.signalAll();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}finally {
consumerLock.unlock();
}
}
}
/**
* 消费商品
* @return
*/
public String consume() {
String result = null;
consumerLock.lock();
try {
while (inventory.get() == 0) {
notEmptyCondition.await();
}
result = product.removeLast();
System.out.println(&quot;消费一个商品,总库存为:&quot; + inventory.decrementAndGet());
if(inventory.get()&gt;0) {
notEmptyCondition.signalAll();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
consumerLock.unlock();
}
if(inventory.get()&lt;maxInventory) {
try {
productLock.lockInterruptibly();
notFullCondition.signalAll();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}finally {
productLock.unlock();
}
}
return result;
}
/**
* 生产者
* @author admin
*
*/
private class Producer implements Runnable {
public void run() {
for (int i = 0; i &lt; 20; i++) {
produce(&quot;商品&quot; + i);
}
}
}
/**
* 消费者
* @author admin
*
*/
private class Customer implements Runnable {
public void run() {
for (int i = 0; i &lt; 20; i++) {
consume();
}
}
}
public static void main(String[] args) {
LockConditionTest2 lc = new LockConditionTest2();
new Thread(lc.new Producer()).start();
new Thread(lc.new Customer()).start();
}
}
```
我们分别创建 productLock 以及 consumerLock 两个锁资源前者控制生产者线程并行操作后者控制消费者线程并发运行同时也设置两个条件变量一个是notEmptyCondition负责控制消费者线程状态一个是notFullCondition负责控制生产者线程状态。这样优化后可以减少消费者与生产者的竞争实现两者并发执行。
我们这里是基于LinkedList来存取库存的虽然LinkedList是非线程安全但我们新增是操作头部而消费是操作队列的尾部理论上来说没有线程安全问题。而库存的实际数量inventory是基于AtomicIntegerCAS锁线程安全类实现的既可以保证原子性也可以保证消费者和生产者之间是可见的。
## BlockingQueue实现生产者消费者
相对前两种实现方式BlockingQueue实现是最简单明了的也是最容易理解的。
因为BlockingQueue是线程安全的且从队列中获取或者移除元素时如果队列为空获取或移除操作则需要等待直到队列不为空同时如果向队列中添加元素假设此时队列无可用空间添加操作也需要等待。所以BlockingQueue非常适合用来实现生产者消费者模式。还是以一个案例来看下它的优化代码如下
```
public class BlockingQueueTest {
private int maxInventory = 10; // 最大库存
private BlockingQueue&lt;String&gt; product = new LinkedBlockingQueue&lt;&gt;(maxInventory);//缓存队列
/**
* 新增商品库存
* @param e
*/
public void produce(String e) {
try {
product.put(e);
System.out.println(&quot;放入一个商品库存,总库存为:&quot; + product.size());
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
/**
* 消费商品
* @return
*/
public String consume() {
String result = null;
try {
result = product.take();
System.out.println(&quot;消费一个商品,总库存为:&quot; + product.size());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
/**
* 生产者
* @author admin
*
*/
private class Producer implements Runnable {
public void run() {
for (int i = 0; i &lt; 20; i++) {
produce(&quot;商品&quot; + i);
}
}
}
/**
* 消费者
* @author admin
*
*/
private class Customer implements Runnable {
public void run() {
for (int i = 0; i &lt; 20; i++) {
consume();
}
}
}
public static void main(String[] args) {
BlockingQueueTest lc = new BlockingQueueTest();
new Thread(lc.new Producer()).start();
new Thread(lc.new Customer()).start();
new Thread(lc.new Producer()).start();
new Thread(lc.new Customer()).start();
}
}
```
在这个案例中我们创建了一个LinkedBlockingQueue并设置队列大小。之后我们创建一个消费方法consume()方法里面调用LinkedBlockingQueue中的take()方法消费者通过该方法获取商品当队列中商品数量为零时消费者将进入等待状态我们再创建一个生产方法produce()方法里面调用LinkedBlockingQueue中的put()方法,生产方通过该方法往队列中放商品,如果队列满了,生产者就将进入等待状态。
## 生产者消费者优化电商库存设计
了解完生产者消费者模式的几种常见实现方式,接下来我们就具体看看该模式是如何优化电商库存设计的。
电商系统中经常会有抢购活动,在这类促销活动中,抢购商品的库存实际是存在库存表中的。为了提高抢购性能,我们通常会将库存存放在缓存中,通过缓存中的库存来实现库存的精确扣减。在提交订单并付款之后,我们还需要再去扣除数据库中的库存。如果遇到瞬时高并发,我们还都去操作数据库的话,那么在单表单库的情况下,数据库就很可能会出现性能瓶颈。
而我们库存表如果要实现分库分表,势必会增加业务的复杂度。试想一个商品的库存分别在不同库的表中,我们在扣除库存时,又该如何判断去哪个库中扣除呢?
如果随意扣除表中库存,那么就会出现有些表已经扣完了,有些表中还有库存的情况,这样的操作显然是不合理的,此时就需要额外增加逻辑判断来解决问题。
在不分库分表的情况下,为了提高订单中扣除库存业务的性能以及吞吐量,我们就可以采用生产者消费者模式来实现系统的性能优化。
创建订单等于生产者,存放订单的队列则是缓冲容器,而从队列中消费订单则是数据库扣除库存操作。其中存放订单的队列可以极大限度地缓冲高并发给数据库带来的压力。
我们还可以基于消息队列来实现生产者消费者模式如今RabbitMQ、RocketMQ都实现了事务我们只需要将订单通过事务提交到MQ中扣除库存的消费方只需要通过消费MQ来逐步操作数据库即可。
## 总结
使用生产者消费者模式来缓冲高并发数据库扣除库存压力,类似这样的例子其实还有很多。
例如,我们平时使用消息队列来做高并发流量削峰,也是基于这个原理。抢购商品时,如果所有的抢购请求都直接进入判断是否有库存和冻结缓存库存等逻辑业务中,由于这些逻辑业务操作会增加资源消耗,就可能会压垮应用服务。此时,为了保证系统资源使用的合理性,我们可以通过一个消息队列来缓冲瞬时的高并发请求。
生产者消费者模式除了可以做缓冲优化系统性能之外,它还可以应用在处理一些执行任务时间比较长的场景中。
例如导出报表业务,用户在导出一种比较大的报表时,通常需要等待很长时间,这样的用户体验是非常差的。通常我们可以固定一些报表内容,比如用户经常需要在今天导出昨天的销量报表,或者在月初导出上个月的报表,我们就可以提前将报表导出到本地或内存中,这样用户就可以在很短的时间内直接下载报表了。
## 思考题
我们可以用生产者消费者模式来实现瞬时高并发的流量削峰,然而这样做虽然缓解了消费方的压力,但生产方则会因为瞬时高并发,而发生大量线程阻塞。面对这样的情况,你知道有什么方式可以优化线程阻塞所带来的性能问题吗?
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,461 @@
<audio id="audio" title="31 | 装饰器模式:如何优化电商系统中复杂的商品价格策略?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/7e/fb48248bfb9c075ce52914582bcba07e.mp3"></audio>
你好,我是刘超。
开始今天的学习之前,我想先请你思考一个问题。假设现在有这样一个需求,让你设计一个装修功能,用户可以动态选择不同的装修功能来装饰自己的房子。例如,水电装修、天花板以及粉刷墙等属于基本功能,而设计窗帘装饰窗户、设计吊顶装饰房顶等未必是所有用户都需要的,这些功能则需要实现动态添加。还有就是一旦有新的装修功能,我们也可以实现动态添加。如果要你来负责,你会怎么设计呢?
此时你可能会想了,通常给一个对象添加功能,要么直接修改代码,在对象中添加相应的功能,要么派生对应的子类来扩展。然而,前者每次都需要修改对象的代码,这显然不是理想的面向对象设计,即便后者是通过派生对应的子类来扩展,也很难满足复杂的随意组合功能需求。
面对这种情况,使用装饰器模式应该再合适不过了。它的优势我想你多少知道一点,我在这里总结一下。
装饰器模式能够实现为对象动态添加装修功能,它是从一个对象的外部来给对象添加功能,所以有非常灵活的扩展性,我们可以在对原来的代码毫无修改的前提下,为对象添加新功能。除此之外,装饰器模式还能够实现对象的动态组合,借此我们可以很灵活地给动态组合的对象,匹配所需要的功能。
下面我们就通过实践,具体看看该模式的优势。
## 什么是装饰器模式?
在这之前,我先简单介绍下什么是装饰器模式。装饰器模式包括了以下几个角色:接口、具体对象、装饰类、具体装饰类。
接口定义了具体对象的一些实现方法;具体对象定义了一些初始化操作,比如开头设计装修功能的案例中,水电装修、天花板以及粉刷墙等都是初始化操作;装饰类则是一个抽象类,主要用来初始化具体对象的一个类;其它的具体装饰类都继承了该抽象类。
下面我们就通过装饰器模式来实现下装修功能,代码如下:
```
/**
* 定义一个基本装修接口
* @author admin
*
*/
public interface IDecorator {
/**
* 装修方法
*/
void decorate();
}
```
```
/**
* 装修基本类
* @author admin
*
*/
public class Decorator implements IDecorator{
/**
* 基本实现方法
*/
public void decorate() {
System.out.println(&quot;水电装修、天花板以及粉刷墙。。。&quot;);
}
}
```
```
/**
* 基本装饰类
* @author admin
*
*/
public abstract class BaseDecorator implements IDecorator{
private IDecorator decorator;
public BaseDecorator(IDecorator decorator) {
this.decorator = decorator;
}
/**
* 调用装饰方法
*/
public void decorate() {
if(decorator != null) {
decorator.decorate();
}
}
}
```
```
/**
* 窗帘装饰类
* @author admin
*
*/
public class CurtainDecorator extends BaseDecorator{
public CurtainDecorator(IDecorator decorator) {
super(decorator);
}
/**
* 窗帘具体装饰方法
*/
@Override
public void decorate() {
System.out.println(&quot;窗帘装饰。。。&quot;);
super.decorate();
}
}
```
```
public static void main( String[] args )
{
IDecorator decorator = new Decorator();
IDecorator curtainDecorator = new CurtainDecorator(decorator);
curtainDecorator.decorate();
}
```
运行结果:
```
窗帘装饰。。。
水电装修、天花板以及粉刷墙。。。
```
通过这个案例我们可以了解到如果我们想要在基础类上添加新的装修功能只需要基于抽象类BaseDecorator去实现继承类通过构造函数调用父类以及重写装修方法实现装修窗帘的功能即可。在main函数中我们通过实例化装饰类调用装修方法即可在基础装修的前提下获得窗帘装修功能。
基于装饰器模式实现的装修功能的代码结构简洁易读,业务逻辑也非常清晰,并且如果我们需要扩展新的装修功能,只需要新增一个继承了抽象装饰类的子类即可。
在这个案例中,我们仅实现了业务扩展功能,接下来,我将通过装饰器模式优化电商系统中的商品价格策略,实现不同促销活动的灵活组合。
## 优化电商系统中的商品价格策略
相信你一定不陌生,购买商品时经常会用到的限时折扣、红包、抵扣券以及特殊抵扣金等,种类很多,如果换到开发视角,实现起来就更复杂了。
例如,每逢双十一,为了加大商城的优惠力度,开发往往要设计红包+限时折扣或红包+抵扣券等组合来实现多重优惠。而在平时,由于某些特殊原因,商家还会赠送特殊抵扣券给购买用户,而特殊抵扣券+各种优惠又是另一种组合方式。
要实现以上这类组合优惠的功能最快、最普遍的实现方式就是通过大量if-else的方式来实现。但这种方式包含了大量的逻辑判断致使其他开发人员很难读懂业务 并且一旦有新的优惠策略或者价格组合策略出现,就需要修改代码逻辑。
这时刚刚介绍的装饰器模式就很适合用在这里其相互独立、自由组合以及方便动态扩展功能的特性可以很好地解决if-else方式的弊端。下面我们就用装饰器模式动手实现一套商品价格策略的优化方案。
首先,我们先建立订单和商品的属性类,在本次案例中,为了保证简洁性,我只建立了几个关键字段。以下几个重要属性关系为,主订单包含若干详细订单,详细订单中记录了商品信息,商品信息中包含了促销类型信息,一个商品可以包含多个促销类型(本案例只讨论单个促销和组合促销):
```
/**
* 主订单
* @author admin
*
*/
public class Order {
private int id; //订单ID
private String orderNo; //订单号
private BigDecimal totalPayMoney; //总支付金额
private List&lt;OrderDetail&gt; list; //详细订单列表
}
```
```
/**
* 详细订单
* @author admin
*
*/
public class OrderDetail {
private int id; //详细订单ID
private int orderId;//主订单ID
private Merchandise merchandise; //商品详情
private BigDecimal payMoney; //支付单价
}
```
```
/**
* 商品
* @author admin
*
*/
public class Merchandise {
private String sku;//商品SKU
private String name; //商品名称
private BigDecimal price; //商品单价
private Map&lt;PromotionType, SupportPromotions&gt; supportPromotions; //支持促销类型
}
```
```
/**
* 促销类型
* @author admin
*
*/
public class SupportPromotions implements Cloneable{
private int id;//该商品促销的ID
private PromotionType promotionType;//促销类型 1\优惠券 2\红包
private int priority; //优先级
private UserCoupon userCoupon; //用户领取该商品的优惠券
private UserRedPacket userRedPacket; //用户领取该商品的红包
//重写clone方法
public SupportPromotions clone(){
SupportPromotions supportPromotions = null;
try{
supportPromotions = (SupportPromotions)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return supportPromotions;
}
}
```
```
/**
* 优惠券
* @author admin
*
*/
public class UserCoupon {
private int id; //优惠券ID
private int userId; //领取优惠券用户ID
private String sku; //商品SKU
private BigDecimal coupon; //优惠金额
}
```
```
/**
* 红包
* @author admin
*
*/
public class UserRedPacket {
private int id; //红包ID
private int userId; //领取用户ID
private String sku; //商品SKU
private BigDecimal redPacket; //领取红包金额
}
```
接下来,我们再建立一个计算支付金额的接口类以及基本类:
```
/**
* 计算支付金额接口类
* @author admin
*
*/
public interface IBaseCount {
BigDecimal countPayMoney(OrderDetail orderDetail);
}
```
```
/**
* 支付基本类
* @author admin
*
*/
public class BaseCount implements IBaseCount{
public BigDecimal countPayMoney(OrderDetail orderDetail) {
orderDetail.setPayMoney(orderDetail.getMerchandise().getPrice());
System.out.println(&quot;商品原单价金额为:&quot; + orderDetail.getPayMoney());
return orderDetail.getPayMoney();
}
}
```
然后,我们再建立一个计算支付金额的抽象类,由抽象类调用基本类:
```
/**
* 计算支付金额的抽象类
* @author admin
*
*/
public abstract class BaseCountDecorator implements IBaseCount{
private IBaseCount count;
public BaseCountDecorator(IBaseCount count) {
this.count = count;
}
public BigDecimal countPayMoney(OrderDetail orderDetail) {
BigDecimal payTotalMoney = new BigDecimal(0);
if(count!=null) {
payTotalMoney = count.countPayMoney(orderDetail);
}
return payTotalMoney;
}
}
```
然后,我们再通过继承抽象类来实现我们所需要的修饰类(优惠券计算类、红包计算类):
```
/**
* 计算使用优惠券后的金额
* @author admin
*
*/
public class CouponDecorator extends BaseCountDecorator{
public CouponDecorator(IBaseCount count) {
super(count);
}
public BigDecimal countPayMoney(OrderDetail orderDetail) {
BigDecimal payTotalMoney = new BigDecimal(0);
payTotalMoney = super.countPayMoney(orderDetail);
payTotalMoney = countCouponPayMoney(orderDetail);
return payTotalMoney;
}
private BigDecimal countCouponPayMoney(OrderDetail orderDetail) {
BigDecimal coupon = orderDetail.getMerchandise().getSupportPromotions().get(PromotionType.COUPON).getUserCoupon().getCoupon();
System.out.println(&quot;优惠券金额:&quot; + coupon);
orderDetail.setPayMoney(orderDetail.getPayMoney().subtract(coupon));
return orderDetail.getPayMoney();
}
}
```
```
/**
* 计算使用红包后的金额
* @author admin
*
*/
public class RedPacketDecorator extends BaseCountDecorator{
public RedPacketDecorator(IBaseCount count) {
super(count);
}
public BigDecimal countPayMoney(OrderDetail orderDetail) {
BigDecimal payTotalMoney = new BigDecimal(0);
payTotalMoney = super.countPayMoney(orderDetail);
payTotalMoney = countCouponPayMoney(orderDetail);
return payTotalMoney;
}
private BigDecimal countCouponPayMoney(OrderDetail orderDetail) {
BigDecimal redPacket = orderDetail.getMerchandise().getSupportPromotions().get(PromotionType.REDPACKED).getUserRedPacket().getRedPacket();
System.out.println(&quot;红包优惠金额:&quot; + redPacket);
orderDetail.setPayMoney(orderDetail.getPayMoney().subtract(redPacket));
return orderDetail.getPayMoney();
}
}
```
最后,我们通过一个工厂类来组合商品的促销类型:
```
/**
* 计算促销后的支付价格
* @author admin
*
*/
public class PromotionFactory {
public static BigDecimal getPayMoney(OrderDetail orderDetail) {
//获取给商品设定的促销类型
Map&lt;PromotionType, SupportPromotions&gt; supportPromotionslist = orderDetail.getMerchandise().getSupportPromotions();
//初始化计算类
IBaseCount baseCount = new BaseCount();
if(supportPromotionslist!=null &amp;&amp; supportPromotionslist.size()&gt;0) {
for(PromotionType promotionType: supportPromotionslist.keySet()) {//遍历设置的促销类型,通过装饰器组合促销类型
baseCount = protmotion(supportPromotionslist.get(promotionType), baseCount);
}
}
return baseCount.countPayMoney(orderDetail);
}
/**
* 组合促销类型
* @param supportPromotions
* @param baseCount
* @return
*/
private static IBaseCount protmotion(SupportPromotions supportPromotions, IBaseCount baseCount) {
if(supportPromotions.getPromotionType()==PromotionType.COUPON) {
baseCount = new CouponDecorator(baseCount);
}else if(supportPromotions.getPromotionType()==PromotionType.REDPACKED) {
baseCount = new RedPacketDecorator(baseCount);
}
return baseCount;
}
}
```
```
public static void main( String[] args ) throws InterruptedException, IOException
{
Order order = new Order();
init(order);
for(OrderDetail orderDetail: order.getList()) {
BigDecimal payMoney = PromotionFactory.getPayMoney(orderDetail);
orderDetail.setPayMoney(payMoney);
System.out.println(&quot;最终支付金额:&quot; + orderDetail.getPayMoney());
}
}
```
运行结果:
```
商品原单价金额为20
优惠券金额3
红包优惠金额10
最终支付金额7
```
以上源码可以通过 [Github](https://github.com/nickliuchao/decorator.git) 下载运行。通过以上案例可知:使用装饰器模式设计的价格优惠策略,实现各个促销类型的计算功能都是相互独立的类,并且可以通过工厂类自由组合各种促销类型。
## 总结
这讲介绍的装饰器模式主要用来优化业务的复杂度,它不仅简化了我们的业务代码,还优化了业务代码的结构设计,使得整个业务逻辑清晰、易读易懂。
通常,装饰器模式用于扩展一个类的功能,且支持动态添加和删除类的功能。在装饰器模式中,装饰类和被装饰类都只关心自身的业务,不相互干扰,真正实现了解耦。
## 思考题
责任链模式、策略模式与装饰器模式有很多相似之处。平时,这些设计模式除了在业务中被用到以外,在架构设计中也经常被用到,你是否在源码中见过这几种设计模式的使用场景呢?欢迎你与大家分享。

View File

@@ -0,0 +1,282 @@
<audio id="audio" title="32 | 答疑课堂:模块五思考题集锦" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/09/38/096196416b79ff659515d387528e5938.mp3"></audio>
你好,我是刘超。
模块五我们都在讨论设计模式,在我看来,设计模式不仅可以优化我们的代码结构,使代码可扩展性、可读性强,同时也起到了优化系统性能的作用,这是我设置这个模块的初衷。特别是在一些高并发场景中,线程协作相关的设计模式可以大大提高程序的运行性能。
那么截至本周,有关设计模式的内容就结束了,不知你有没有发现这个模块的思考题都比较发散,很多同学也在留言区中写出了很多硬核信息,促进了技术交流。这一讲的答疑课堂我就来为你总结下课后思考题,希望我的答案能让你有新的收获。
[第 26 讲](https://time.geekbang.org/column/article/109564)
**除了以上那些实现单例的方式,你还知道其它实现方式吗?**
在[第9讲](https://time.geekbang.org/column/article/99774)中我曾提到过一个单例序列化问题其答案就是使用枚举来实现单例这样可以避免Java序列化破坏一个类的单例。
枚举生来就是单例枚举类的域field其实是相应的enum类型的一个实例对象因为在Java中枚举是一种语法糖所以在编译后枚举类中的枚举域会被声明为static属性。
在[第26讲](https://time.geekbang.org/column/article/109564)中我已经详细解释了JVM是如何保证static成员变量只被实例化一次的我们不妨再来回顾下。使用了static修饰的成员变量会在类初始化的过程中被收集进类构造器即&lt;clinit&gt;方法中在多线程场景下JVM会保证只有一个线程能执行该类的&lt;clinit&gt;方法,其它线程将会被阻塞等待。等到唯一的一次&lt;clinit&gt;方法执行完成,其它线程将不会再执行&lt;clinit&gt;方法转而执行自己的代码。也就是说static修饰了成员变量在多线程的情况下能保证只实例化一次。
我们可以通过代码简单了解下使用枚举实现的饿汉单例模式:
```
//饿汉模式 枚举实现
public enum Singleton {
INSTANCE;//不实例化
public List&lt;String&gt; list = null;// list属性
private Singleton() {//构造函数
list = new ArrayList&lt;String&gt;();
}
public static Singleton getInstance(){
return INSTANCE;//返回已存在的对象
}
}
```
该方式实现的单例没有实现懒加载功能,那如果我们要使用到懒加载功能呢?此时,我们就可以基于内部类来实现:
```
//懒汉模式 枚举实现
public class Singleton {
//不实例化
public List&lt;String&gt; list = null;// list属性
private Singleton(){//构造函数
list = new ArrayList&lt;String&gt;();
}
//使用枚举作为内部类
private enum EnumSingleton {
INSTANCE;//不实例化
private Singleton instance = null;
private EnumSingleton(){//构造函数
instance = new Singleton();
}
public Singleton getSingleton(){
return instance;//返回已存在的对象
}
}
public static Singleton getInstance(){
return EnumSingleton.INSTANCE.getSingleton();//返回已存在的对象
}
}
```
[第27讲](https://time.geekbang.org/column/article/109980)
**上一讲的单例模式和这一讲的享元模式都是为了避免重复创建对象,你知道这两者的区别在哪儿吗?**
首先,这两种设计模式的实现方式是不同的。我们使用单例模式是避免每次调用一个类实例时,都要重复实例化该实例,目的是在类本身获取实例化对象的唯一性;而享元模式则是通过一个共享容器来实现一系列对象的共享。
其次,两者在使用场景上也是有区别的。单例模式更多的是强调减少实例化提升性能,因此它一般是使用在一些需要频繁创建和销毁实例化对象,或创建和销毁实例化对象非常消耗资源的类中。
例如,连接池和线程池中的连接就是使用单例模式实现的,数据库操作是非常频繁的,每次操作都需要创建和销毁连接,如果使用单例,可以节省不断新建和关闭数据库连接所引起的性能消耗。而享元模式更多的是强调共享相同对象或对象属性,以此节约内存使用空间。
除了区别,这两种设计模式也有共性,单例模式可以避免重复创建对象,节约内存空间,享元模式也可以避免一个类的重复实例化。总之,两者很相似,但侧重点不一样,假如碰到一些要在两种设计模式中做选择的场景,我们就可以根据侧重点来选择。
[第28讲](https://time.geekbang.org/column/article/110862)
**除了以上这些多线程的设计模式线程上下文设计模式、Thread-Per-Message设计模式、Worker-Thread设计模式平时你还使用过其它的设计模式来优化多线程业务吗**
在这一讲的留言区undifined同学问到了如果我们使用Worker-Thread设计模式worker线程如果是异步请求处理当我们监听到有请求进来之后将任务交给工作线程怎么拿到返回结果并返回给主线程呢
回答这个问题的过程中就会用到一些别的设计模式,可以一起看看。
如果要获取到异步线程的执行结果我们可以使用Future设计模式来解决这个问题。假设我们有一个任务需要一台机器执行但是该任务需要一个工人分配给机器执行当机器执行完成之后需要通知工人任务的具体完成结果。这个时候我们就可以设计一个Future模式来实现这个业务。
首先,我们申明一个任务接口,主要提供给任务设计:
```
public interface Task&lt;T, P&gt; {
T doTask(P param);//完成任务
}
```
其次我们申明一个提交任务接口类TaskService主要用于提交任务提交任务可以分为需要返回结果和不需要返回结果两种
```
public interface TaskService&lt;T, P&gt; {
Future&lt;?&gt; submit(Runnable runnable);//提交任务,不返回结果
Future&lt;?&gt; submit(Task&lt;T,P&gt; task, P param);//提交任务,并返回结果
}
```
接着,我们再申明一个查询执行结果的接口类,用于提交任务之后,在主线程中查询执行结果:
```
public interface Future&lt;T&gt; {
T get(); //获取返回结果
boolean done(); //判断是否完成
}
```
然后我们先实现这个任务接口类当需要返回结果时我们通过调用获取结果类的finish方法将结果传回给查询执行结果类
```
public class TaskServiceImpl&lt;T, P&gt; implements TaskService&lt;T, P&gt; {
/**
* 提交任务实现方法,不需要返回执行结果
*/
@Override
public Future&lt;?&gt; submit(Runnable runnable) {
final FutureTask&lt;Void&gt; future = new FutureTask&lt;Void&gt;();
new Thread(() -&gt; {
runnable.run();
}, Thread.currentThread().getName()).start();
return future;
}
/**
* 提交任务实现方法,需要返回执行结果
*/
@Override
public Future&lt;?&gt; submit(Task&lt;T, P&gt; task, P param) {
final FutureTask&lt;T&gt; future = new FutureTask&lt;T&gt;();
new Thread(() -&gt; {
T result = task.doTask(param);
future.finish(result);
}, Thread.currentThread().getName()).start();
return future;
}
}
```
最后我们再实现这个查询执行结果接口类FutureTask中get 和 finish 方法利用了线程间的通信wait和notifyAll实现了线程的阻塞和唤醒。当任务没有完成之前通过get方法获取结果主线程将会进入阻塞状态直到任务完成再由任务线程调用finish方法将结果传回给主线程并唤醒该阻塞线程
```
public class FutureTask&lt;T&gt; implements Future&lt;T&gt; {
private T result;
private boolean isDone = false;
private final Object LOCK = new Object();
@Override
public T get() {
synchronized (LOCK) {
while (!isDone) {
try {
LOCK.wait();//阻塞等待
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return result;
}
/**
* 获取到结果,并唤醒阻塞线程
* @param result
*/
public void finish(T result) {
synchronized (LOCK) {
if (isDone) {
return;
}
this.result = result;
this.isDone = true;
LOCK.notifyAll();
}
}
@Override
public boolean done() {
return isDone;
}
}
```
我们可以实现一个造车任务,然后用任务提交类提交该造车任务:
```
public class MakeCarTask&lt;T, P&gt; implements Task&lt;T, P&gt; {
@SuppressWarnings(&quot;unchecked&quot;)
@Override
public T doTask(P param) {
String car = param + &quot; is created success&quot;;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return (T) car;
}
}
```
最后运行该任务:
```
public class App {
public static void main(String[] args) {
// TODO Auto-generated method stub
TaskServiceImpl&lt;String, String&gt; taskService = new TaskServiceImpl&lt;String, String&gt;();//创建任务提交类
MakeCarTask&lt;String, String&gt; task = new MakeCarTask&lt;String, String&gt;();//创建任务
Future&lt;?&gt; future = taskService.submit(task, &quot;car1&quot;);//提交任务
String result = (String) future.get();//获取结果
System.out.print(result);
}
}
```
运行结果:
```
car1 is created success
```
从JDK1.5起Java就提供了一个Future类它可以通过get()方法阻塞等待获取异步执行的返回结果然而这种方式在性能方面会比较糟糕。在JDK1.8中Java提供了CompletableFuture类它是基于异步函数式编程。相对阻塞式等待返回结果CompletableFuture可以通过回调的方式来处理计算结果所以实现了异步非阻塞从性能上来说它更加优越了。
在Dubbo2.7.0版本中Dubbo也是基于CompletableFuture实现了异步通信基于回调方式实现了异步非阻塞通信操作非常简单方便。
[第29讲](https://time.geekbang.org/column/article/111288)
**我们可以用生产者消费者模式来实现瞬时高并发的流量削峰,然而这样做虽然缓解了消费方的压力,但生产方则会因为瞬时高并发,而发生大量线程阻塞。面对这样的情况,你知道有什么方式可以优化线程阻塞所带来的性能问题吗?**
无论我们的程序优化得有多么出色,只要并发上来,依然会出现瓶颈。虽然生产者消费者模式可以帮我们实现流量削峰,但是当并发量上来之后,依然有可能导致生产方大量线程阻塞等待,引起上下文切换,增加系统性能开销。这时,我们可以考虑在接入层做限流。
限流的实现方式有很多例如使用线程池、使用Guava的RateLimiter等。但归根结底它们都是基于这两种限流算法来实现的漏桶算法和令牌桶算法。
漏桶算法是基于一个漏桶来实现的,我们的请求如果要进入到业务层,必须经过漏桶,漏桶出口的请求速率是均衡的,当入口的请求量比较大的时候,如果漏桶已经满了,请求将会溢出(被拒绝),这样我们就可以保证从漏桶出来的请求量永远是均衡的,不会因为入口的请求量突然增大,致使进入业务层的并发量过大而导致系统崩溃。
令牌桶算法是指系统会以一个恒定的速度在一个桶中放入令牌一个请求如果要进来它需要拿到一个令牌才能进入到业务层当桶里没有令牌可以取时则请求会被拒绝。Google的Guava包中的RateLimiter就是基于令牌桶算法实现的。
我们可以发现,漏桶算法可以通过限制容量池大小来控制流量,而令牌算法则可以通过限制发放令牌的速率来控制流量。
[第30讲](https://time.geekbang.org/column/article/111600)
**责任链模式、策略模式与装饰器模式有很多相似之处。在平时,这些设计模式除了在业务中被用到之外,在架构设计中也经常被用到,你是否在源码中见过这几种设计模式的使用场景呢?欢迎你与大家分享。**
责任链模式经常被用在一个处理需要经历多个事件处理的场景。为了避免一个处理跟多个事件耦合在一起,该模式会将多个事件连成一条链,通过这条链路将每个事件的处理结果传递给下一个处理事件。责任链模式由两个主要实现类组成:抽象处理类和具体处理类。
另外很多开源框架也用到了责任链模式例如Dubbo中的Filter就是基于该模式实现的。而Dubbo的许多功能都是通过Filter扩展实现的比如缓存、日志、监控、安全、telnet以及RPC本身责任链中的每个节点实现了Filter接口然后由ProtocolFilterWrapper将所有的Filter串连起来。
策略模式与装饰器模式则更为相似,策略模式主要由一个策略基类、具体策略类以及一个工厂环境类组成,与装饰器模式不同的是,策略模式是指某个对象在不同的场景中,选择的实现策略不一样。例如,同样是价格策略,在一些场景中,我们就可以使用策略模式实现。基于红包的促销活动商品,只能使用红包策略,而基于折扣券的促销活动商品,也只能使用折扣券。
以上就是模块五所有思考题的答案,现在不妨和你的答案结合一下,看看是否有新的收获呢?如果你还有其它问题,请在留言区中提出,我会一一解答。最后欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他加入讨论。