31讲答疑课堂:模块五思考题集锦 - 图131讲答疑课堂:模块五思考题集锦

你好,我是刘超。

31讲答疑课堂:模块五思考题集锦 - 图2模块五我们都在讨论设计模式,在我看来,设计模式不仅可以优化我们的代码结构,使代码可扩展性、可读性强,同时也起到了优化系统性能的作⽤,这是我设置这个模块的初衷。特别是在⼀些⾼并发场景中,线程协作相关的设计模式可以⼤⼤提⾼程序的运⾏性能。

那么截⾄本周,有关设计模式的内容就结束了,不知你有没有发现这个模块的思考题都⽐较发散,很多同学也在留⾔区中写出了很多硬核信息,促进了技术交流。这⼀讲的答疑课堂我就来为你总结下课后思考题,希望我的答案能让你有新的收获。

第 26 讲

除了以上那些实现单例的⽅式,你还知道其它实现⽅式吗?

第9讲中,我曾提到过⼀个单例序列化问题,其答案就是使⽤枚举来实现单例,这样可以避免Java序列化破坏⼀个类的单例。

枚举⽣来就是单例,枚举类的域(field)其实是相应的enum类型的⼀个实例对象,因为在Java中枚举是⼀种语法糖,所以在编译后,枚举类中的枚举域会被声明为static属性。

第26讲中,我已经详细解释了JVM是如何保证static成员变量只被实例化⼀次的,我们不妨再来回顾下。使⽤了static修饰的成员变量,会在类初始化的过程中被收集进类构造器即⽅法中,在多线程场景下,JVM会保证只有⼀个线程能执⾏该 类的⽅法,其它线程将会被阻塞等待。等到唯⼀的⼀次⽅法执⾏完成,其它线程将不会再执⾏⽅法,转
⽽执⾏⾃⼰的代码。也就是说,static修饰了成员变量,在多线程的情况下能保证只实例化⼀次。我们可以通过代码简单了解下使⽤枚举实现的饿汉单例模式:

//饿汉模式 枚举实现
public enum Singleton {
INSTANCE;//不实例化
public List list = null;// list属性

private Singleton() {//构造函数list = new ArrayList();
}
public static Singleton getInstance(){ return INSTANCE;//返回已存在的对象
}
}

该⽅式实现的单例没有实现懒加载功能,那如果我们要使⽤到懒加载功能呢?此时,我们就可以基于内部类来实现:

//懒汉模式 枚举实现
public class Singleton {
INSTANCE;//不实例化
public List list = null;// list属性

private Singleton(){//构造函数list = new ArrayList();
}
//使⽤枚举作为内部类
private enum EnumSingleton {
INSTANCE;//不实例化
private Singleton instance = null;

private EnumSingleton(){//构造函数instance = new Singleton();
}
public static Singleton getSingleton(){ return instance;//返回已存在的对象
}
}

public static Singleton getInstance(){
return EnumSingleton.INSTANCE.getSingleton();//返回已存在的对象
}
}

第27讲

上⼀讲的单例模式和这⼀讲的享元模式都是为了避免重复创建对象,你知道这两者的区别在哪⼉吗?

⾸先,这两种设计模式的实现⽅式是不同的。我们使⽤单例模式是避免每次调⽤⼀个类实例时,都要重复实例化该实例,⽬的是在类本身获取实例化对象的唯⼀性;⽽享元模式则是通过⼀个共享容器来实现⼀系列对象的共享。

其次,两者在使⽤场景上也是有区别的。单例模式更多的是强调减少实例化提升性能,因此它⼀般是使⽤在⼀些需要频繁创建和销毁实例化对象,或创建和销毁实例化对象⾮常消耗资源的类中。

例如,连接池和线程池中的连接就是使⽤单例模式实现的,数据库操作是⾮常频繁的,每次操作都需要创建和销毁连接,如果使⽤单例,可以节省不断新建和关闭数据库连接所引起的性能消耗。⽽享元模式更多的是强调共享相同对象或对象属性,以此节约内存使⽤空间。

除了区别,这两种设计模式也有共性,单例模式可以避免重复创建对象,节约内存空间,享元模式也可以避免⼀个类的重复实例化。总之,两者很相似,但侧重点不⼀样,假如碰到⼀些要在两种设计模式中做选择的场景,我们就可以根据侧重点来选 择。

第28讲

除了以上这些多线程的设计模式(线程上下⽂设计模式、Thread-Per-Message设计模式、Worker-Thread设计模式),平时你还使⽤过其它的设计模式来优化多线程业务吗?

在这⼀讲的留⾔区,undifined同学问到了,如果我们使⽤Worker-Thread设计模式,worker线程如果是异步请求处理,当我们监听到有请求进来之后,将任务交给⼯作线程,怎么拿到返回结果,并返回给主线程呢?

回答这个问题的过程中就会⽤到⼀些别的设计模式,可以⼀起看看。

如果要获取到异步线程的执⾏结果,我们可以使⽤Future设计模式来解决这个问题。假设我们有⼀个任务,需要⼀台机器执
⾏,但是该任务需要⼀个⼯⼈分配给机器执⾏,当机器执⾏完成之后,需要通知⼯⼈任务的具体完成结果。这个时候我们就可以设计⼀个Future模式来实现这个业务。

⾸先,我们申明⼀个任务接⼝,主要提供给任务设计:

public interface Task { T doTask(P param);//完成任务
}
其次,我们申明⼀个提交任务接⼝类,TaskService主要⽤于提交任务,提交任务可以分为需要返回结果和不需要返回结果两种:

public interface TaskService {
Future<?> submit(Runnable runnable);//提交任务,不返回结果Future<?> submit(Task task, P param);//提交任务,并返回结果
}
接着,我们再申明⼀个查询执⾏结果的接⼝类,⽤于提交任务之后,在主线程中查询执⾏结果:

public interface Future {

T get(); //获取返回结果boolean done(); //判断是否完成
}

然后,我们先实现这个任务接⼝类,当需要返回结果时,我们通过调⽤获取结果类的finish⽅法将结果传回给查询执⾏结果
类:

public class TaskServiceImpl implements TaskService {

/*
提交任务实现⽅法,不需要返回执⾏结果
*/
@Override
public Future<?> submit(Runnable runnable) {
final FutureTask future = new FutureTask(); new Thread(() -> {
runnable.run();
}, Thread.currentThread().getName()).start(); return future;
}

/*
提交任务实现⽅法,需要返回执⾏结果
*/
@Override
public Future<?> submit(Task task, P param) { final FutureTask future = new FutureTask(); new Thread(() -> {
T result = task.doTask(param); future.finish(result);
}, Thread.currentThread().getName()).start(); return future;
}
}
最后,我们再实现这个查询执⾏结果接⼝类,FutureTask中,get 和 finish ⽅法利⽤了线程间的通信wait和notifyAll实现了线程的阻塞和唤醒。当任务没有完成之前通过get⽅法获取结果,主线程将会进⼊阻塞状态,直到任务完成,再由任务线程调⽤
finish⽅法将结果传回给主线程,并唤醒该阻塞线程:

public class FutureTask implements Future {

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 implements Task {

@SuppressWarnings(“unchecked”)
@Override
public T doTask(P param) {

String car = param + “ is created success”;

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 taskService = new TaskServiceImpl();//创建任务提交类MakeCarTask task = new MakeCarTask();//创建任务

Future<?> future = taskService.submit(task, “car1”);//提交任务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讲

我们可以⽤⽣产者消费者模式来实现瞬时⾼并发的流量削峰,然⽽这样做虽然缓解了消费⽅的压⼒,但⽣产⽅则会因为瞬时⾼并发,⽽发⽣⼤量线程阻塞。⾯对这样的情况,你知道有什么⽅式可以优化线程阻塞所带来的性能问题吗?

⽆论我们的程序优化得有多么出⾊,只要并发上来,依然会出现瓶颈。虽然⽣产者消费者模式可以帮我们实现流量削峰,但是当并发量上来之后,依然有可能导致⽣产⽅⼤量线程阻塞等待,引起上下⽂切换,增加系统性能开销。这时,我们可以考虑在接⼊层做限流。

限流的实现⽅式有很多,例如,使⽤线程池、使⽤Guava的RateLimiter等。但归根结底,它们都是基于这两种限流算法来实现的:漏桶算法和令牌桶算法。

漏桶算法是基于⼀个漏桶来实现的,我们的请求如果要进⼊到业务层,必须经过漏桶,漏桶出⼝的请求速率是均衡的,当⼊⼝的请求量⽐较⼤的时候,如果漏桶已经满了,请求将会溢出(被拒绝),这样我们就可以保证从漏桶出来的请求量永远是均衡的,不会因为⼊⼝的请求量突然增⼤,致使进⼊业务层的并发量过⼤⽽导致系统崩溃。

令牌桶算法是指系统会以⼀个恒定的速度在⼀个桶中放⼊令牌,⼀个请求如果要进来,它需要拿到⼀个令牌才能进⼊到业务层,当桶⾥没有令牌可以取时,则请求会被拒绝。Google的Guava包中的RateLimiter就是基于令牌桶算法实现的。

我们可以发现,漏桶算法可以通过限制容量池⼤⼩来控制流量,⽽令牌算法则可以通过限制发放令牌的速率来控制流量。

第30讲

责任链模式、策略模式与装饰器模式有很多相似之处。在平时,这些设计模式除了在业务中被⽤到之外,在架构设计中也经常被⽤到,你是否在源码中⻅过这⼏种设计模式的使⽤场景呢?欢迎你与⼤家分享。

责任链模式经常被⽤在⼀个处理需要经历多个事件处理的场景。为了避免⼀个处理跟多个事件耦合在⼀起,该模式会将多个事件连成⼀条链,通过这条链路将每个事件的处理结果传递给下⼀个处理事件。责任链模式由两个主要实现类组成:抽象处理类和具体处理类。

另外,很多开源框架也⽤到了责任链模式,例如Dubbo中的Filter就是基于该模式实现的。⽽Dubbo的许多功能都是通过Filter 扩展实现的,⽐如缓存、⽇志、监控、安全、telnet以及RPC本身,责任链中的每个节点实现了Filter接⼝,然后由
ProtocolFilterWrapper将所有的Filter串连起来。

策略模式与装饰器模式则更为相似,策略模式主要由⼀个策略基类、具体策略类以及⼀个⼯⼚环境类组成,与装饰器模式不同的是,策略模式是指某个对象在不同的场景中,选择的实现策略不⼀样。例如,同样是价格策略,在⼀些场景中,我们就可以使⽤策略模式实现。基于红包的促销活动商品,只能使⽤红包策略,⽽基于折扣券的促销活动商品,也只能使⽤折扣券。

以上就是模块五所有思考题的答案,现在不妨和你的答案结合⼀下,看看是否有新的收获呢?如果你还有其它问题,请在留⾔区中提出,我会⼀⼀解答。最后欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他加⼊讨论。
31讲答疑课堂:模块五思考题集锦 - 图3

  1. 精选留⾔ <br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646315733217-54922428-d8a1-4b17-b090-cdc7cbd4c30b.png#)发条橙⼦ 。<br />⽼师, 对于单例模式那块还是有些不解,希望⽼师解答 :

在类初始化时 会将 static成员变量放到 ⽅法中 ,在类加载准备阶段负责为其创建内存,到了初始化阶段执⾏⽅法 进⾏赋值。

  1. 请问在类加载的时候也会存在多线程的场景么?这块不太好理解 。以及后⾯改成了创建⼀个内部类, 在类加载的时候内部类应该会⼀起被加载 ,这时候内部类中的static也会⼀起被赋值,和直接在外层的类中直接初始化有什么区别么 ?

  2. ⽼师,另外有个点想确认下, 类的字⾯量(字符串和final修饰的成员变量)会在类加载的时候被放到堆的永久代中(运⾏时常量池)。那 static修饰的成员变量还是放在元数据空间⾥是么

2019-08-03 07:15
作者回复
1、外部类初次加载,会初始化静态变量、静态代码块、静态⽅法,但不会加载内部类和静态内部类。

如果我们使⽤内部类实现,不会在初始化类时加载内部类,只有在我们第⼀次调⽤EnumSingleton.INSTANCE,才会加载 Enu
mSingleton 类,⽽只有在加载EnumSingleton类之后,才会实例化创建INSTANCE对象,由于INSTANCE是⼀个静态成员变量
,所以在初始化时由 ⽅法保证线程安全。

2、元空间存放的是类元素,⽽静态常量池存放的是类的字⾯量和字符引⽤,运⾏时常量池存放的是直接引⽤。这⾥的静态常量池也是很多⼈说的字符串常量池。
2019-08-03 10:22

31讲答疑课堂:模块五思考题集锦 - 图4疯狂咸⻥
⽼师会讲数据库调优么?
2019-08-03 02:17
作者回复
下⼀讲开始讲到数据库调优了,欢迎⼀起学习成⻓。
2019-08-03 10:23

31讲答疑课堂:模块五思考题集锦 - 图5Jxin
打卡,转眼已过⼤半,感谢⽼师⼀致以来的分享。棒棒的。
2019-08-03 20:22
作者回复
同样感谢你们⼀路以来的坚持和⽀持。
2019-08-06 10:45

31讲答疑课堂:模块五思考题集锦 - 图6路晓明
⽼师,请教⼀个问题。如果在服务器上部署两个应⽤。是不是JDK应该对应⼀个应⽤。单独对 jvm进⾏优化。我觉得这样的部署话 JVM参数配置相互不影响,性能⽐较⾼。不知到这样是否合理?请⽼师给点建议
2019-08-09 13:34
作者回复
这相当于两个JVM,设置的参数是互不影响的。但是还是会存在相互影响的情况,会存在相互竞争⼀些共享资源的情况,例如
CPU,IO等
2019-08-12 09:54

31讲答疑课堂:模块五思考题集锦 - 图7Liam
单例应该作为枚举类型的⼀个属性,在私有构造⽅法内创建并初始化,暴露⼀个获取⽅法,通过枚举实例去获取单例时会触发单例初始化,这⾥是否有必要区分懒汉恶汉?谈话关注点不应该在枚举实例上吧
2019-08-06 09:12
作者回复
枚举类的域,即代码中的INSTANCE就是我们已经实例化对象的属性了,相当于private static Singleton instance=new Singleto
n()。 正常的枚举实现是⼀个饿汉模式,没有懒加载的实现。枚举类的这个可以再仔细思考下。
2019-08-06 11:14

31讲答疑课堂:模块五思考题集锦 - 图8Liam
枚举单例这⾥没看懂,单例不应该是作为枚举类的⼀个属性,然后在枚举的私有构造⽅法内实⼒化
2019-08-06 09:08
作者回复
枚举是饿汉模式,也就是在属性中已经实例化了,相当于我们最开始讲的饿汉模式。
2019-08-06 11:10

31讲答疑课堂:模块五思考题集锦 - 图9晓杰
⽼师,枚举单例的懒汉模式写的有问题吧
2019-08-05 23:29
作者回复
具体哪⾥有问题呢?欢迎指出
2019-08-06 11:16

31讲答疑课堂:模块五思考题集锦 - 图10失⽕的夏天
⽼师我问⼀下,内部类和静态内部类的区别是什么,哪⾥有不同,我看jdk⾥map的node,还有list的node都是静态的。之前有看到设置成静态是为了防⽌内存泄露,但是没有想明⽩是为什么
2019-08-03 15:00
作者回复
我理解的是,静态内部类是⼀个独⽴类,只是借⽤外部类来隐藏⾃⼰。跟外部类没有实质性的关系,即内部静态类不需要有指向外部类的引⽤。 map的node以及list的node都是⼀个独⽴类,不属于map和list,但node⼜想借外部类来存放⾃⼰。这种⽅式也可以防⽌内存泄漏。

⽽内部类则不⼀样,内部类只能在外部类中实例化被使⽤,属于真正的内部类,属于外部类的⼀部分,它可以访问外部类的任何成员变量。⾮静态内部类需要持有对外部类的引⽤。

2019-08-06 10:44

31讲答疑课堂:模块五思考题集锦 - 图11-W.LI-
31讲答疑课堂:模块五思考题集锦 - 图12⽼师好!感觉⾃⼰写代码很多时候都是⾯向过程的思维。纯纯的CRUD程序员平时99%的⼯作都是增删改查+处理业务逻辑。如
何培养⾃⼰的⾯向对象思维?⽼师有好的建议书籍推荐么?万分感谢,我感觉这个应该是共性的问题。
2019-08-03 12:18
作者回复
需要⾃⼰在⼯作中多学会思考,再加上多读⼀些源码,对着源码⾃⼰再重新⼿写这套设计模式的实现。我在之前推荐过《⼤话设计模式》这本书,写的⽐较⽣动有趣,建议阅读。
2019-08-06 10:18

31讲答疑课堂:模块五思考题集锦 - 图13-W.LI-
很⼲货谢谢⽼师。future那个感觉很不错,之前都是⽤的future+线程池+countdownLanch。实现回调的,调⽤get⽅法的时候确 实会阻塞等待最后⼀个任务完成为⽌,如果需要对⼀批任务做组合处理的化只能这样了吧。如果不需要聚合处理就可以使⽤Co
mpletableFuture进⾏优化,回头看下CompletableFuture。之前好像看过future类的源码,没记错的话和⽼师的代码实现⼀样, 依稀记得只是包了⼀层,future的run⽅法⾥⾯调⽤任务的callable⽅法,返回值存放在future的成员变量result⾥。future这个算代理模式么?
2019-08-03 12:14
作者回复
更像观察者模式
2019-08-06 10:14