第7章-ThreadPerMessage
7.1 模式简介
- 上司把文件递给下属:“能帮我传真一下这个文件吗?” 妻子告诉丈夫:“老公,帮忙倒一下垃圾”。像这样将工作委托给其他人的情况很常见。这个人把工作拜托给别人之后,就可以返回继续做自己的工作。
- 所谓Per,就是 “每~” 的意思。因此,Thread Per Message直译过来就是 “每个消息一个线程” 的意思。Message在这里可以理解为 “命令” 或 “请求” 。为每个命令或请求新分配一个线程,由这个线程来执行处理——这就是Thread-Per-Message模式。
- 在Thread-Per-Message模式中,消息的 “委托端” 和 “执行端” 是不同的线程。消息的委托端线程会告诉执行端线程 “这项工作就交给你了”。
7.3 Thread-Per-Message模式中的角色
7.3.1 Client(委托人)
- Client角色会向Host角色发出请求(request),但是并不知道Host角色是如何实现该请求的。
7.3.2 Host
- Host角色收到Client角色的请求(request)之后,会新创建并启动一个线程。新创建的线程将使用Helper角色来 “处理”(handle)请求。
7.3.3 Helper(助手)
- Helper角色为Host角色提供请求处理的功能。Host角色创建的新线程会利用Helper角色。
7.3.4 类图和Timethreads图
7.4 拓展思路的要点
7.4.1 提高响应性,缩短延迟时间
- Thread-Per-Message模式能够提高与Client角色对应的Host角色的响应性,降低延迟时间。尤其是当handle操作非常耗时,或者handle操作需要等待输入/输出时,效果非常明显。
- 在Thread-Per-Message模式下,Host角色会启动新的线程。由于启动线程也会花费时间,所以想要提高响应性时,是否使用Thread-Per-Message模式取决于 “handle操作花费的时间” 和 “线程启动花费的时间” 之间的均衡。
7.4.2 适用于操作顺序没有要求时
- 在Thread-Per-Message模式中,handle方法并不一定是按request方法的调用顺序来执行的。因此,当操作要按某种顺序执行时,Thread-Per-Message模式并不适用。
7.4.3 适用于不需要返回值时
- 在Thread-Per-Message模式中,request方法并不会等待handle方法执行结束。所以request得不到handle的运行结果。因此,Thread-Per-Message模式适用于不需要获取返回值的情况。例如通知某个事件时。
7.4.4 应用于服务器
- 为了使服务器可以处理多个请求,我们可以使用Thread-Per-Message模式。服务器本身的线程接收客户端的请求,而这些请求的实际处理则交由其他线程来执行,服务器本身的线程则返回,去等待客户端的其他请求。
7.6 进程与线程
7.6.1 线程之间共享内存
- 进程与线程之间最大的区别就是内存是否共享。
- 通常,每个进程都拥有彼此独立的内存空间。一个进程不可以擅自读取、写入其他进程的内存。由于进程的内存空间是彼此独立的,所以一个进程无须担心被其他进程破坏。
- 线程之间共享内存,我们经常让一个线程向内存中写入内容,来供其他线程读取。所谓 “共享内存”,在 Java中就是 “共享实例” 的意思。Java的实例分配在内存上,可由多个线程进行读写。
- 由于线程之间共享内存,所以线程之间的通信可以很自然、简单地实现。一个线程向实例中写入内容,其他线程就可以读取该实例的内容。而由于多个线程可以访问同一个实例,所以我们必须正确执行互斥处理。
7.6.2 线程的上下文切换快
- 进程和线程之间的另一个区别就是上下文切换的繁重程度。
- 当运行中的进程进行切换时,进程要暂时保存自身的当前状态(上下文信息)。而接着开始运行的进程需要恢复之前保存的自身的上下文信息。这种信息切换(context-switch)比较花费时间。
- 当运行中的线程进行切换时,与进程一样,也会进行上下文切换。但由于线程管理的上下文信息比进程少,所以一般来说,线程的上下文切换要比进程快。因此,当执行紧密关联的多项工作时,通常线程比进程更加适合。
7.7 java.util.concurrent 包和Thread-Per-Message模式
- Thread-Per-Message模式会为每个请求创建并启动线程。下面将介绍 “并启动线程” 的各种实现形式。
7.7.1 java.lang.Thread类
- 使用new来创建Thread类的实例,并调用 start 方法来启动线程。这是最最基本的内容。
7.7.2 java.lang.Runnable接口
- 使用new来创建Thread实例时,构造函数中将传入Runnable接口的实现类的实例。
7.7.3 java.util.concurrent.ThreadFactory接口
- java.util.concurrent.ThreadFactory 接口声明了如下所示的一个newThread方法:
java Thread newThread(Runnable r)
- ThreadFactory是将线程创建抽象化了的接口,参数中的Runnable对象表示线程执行的操作内容。
- 使用new创建Thread实例时,代码依赖于java.lang.Thread类。这时,我们无法控制创建线程的部分,可复用性较低。假如我们用字段threadFactory来保存ThreadFactory对象,用threadFactory.newThread(…)来替代new Thread(……)。这样一来,只要替换赋给threadFactory的ThreadFactory对象,我们便可以控制线程创建了。
7.7.4 java.util.concurrent.Executors 类获取的ThreadFactory
- java.util.concurrent.Executors 类提供了许多实用的静态方法。例如,Executors.defaultThreadFactory()表达式可以获取当前默认设置的ThreadFactory对象。
7.7.5 java.util.concurrent.Executor接口
- java.util.concurrent.Executor接口声明了如下所示的一个execute方法:
java void execute(Runnable r)
- Executor接口将某些“处理的执行”抽象化了,参数传入的Runnable对象表示 “执行的处理” 的内容。
- 前面介绍的ThreadFactory接口隐藏了线程创建的细节,但并未隐藏创建线程的操作。如果使用Executor接口,创建线程的操作也可以隐藏起来。
7.7.6 java.util.concurrent.ExecutorService接口
- 在前面的程序中,虽然我们使用Executor进行了抽象化,但最终还是需要自己手动执行new Thread(…)。不过仔细想想,并不一定每次都必须创建线程。只要遵循Executor接口,我们也可以使用某些 “会复用那些处理执行结束后空闲下来的线程” 的类。
- 这就是ava.util.concurrent.Executorservice 接口。Executorservice接口对可以反复 execute 的服务进行了抽象化。线程一直在后台运行着,每当调用execute方法时,线程就会执行Runnable对象。
- 通常情况下,在ExecutorService接口后面,线程是一直运行着的,所以Executorservice接口提供了shutdown方法来结束服务。
- 由于ExecutorService接口是Executor的子接口,所以接收Executor对象的Host类也可以接收Executorservice对象。
7.7.7 java.util.concurrent.ScheduledExecutorService类
- java.util.concurrent.ScheduledExecutorService 接口是ExecutorService的子接口,用于推迟操作的执行。
- schedule方法位于ScheduledExecutorService接口中,可以用于设置Runnable对象(r)和延迟时间(delay、unit)。
java schedule(Runnable r,long delay,TimeUnit unit)
- long 类型的delay表示的是延迟时间,TimeUnit类型的unit表示的则是指定延迟时间的单位(NANOSECONDS、MICROSECONDS、MILLISECONDS或SECONDS)。
7.7.8 总结
- java.lang.Thread类:最基本的创建、启动线程的类
- java.lang.Runnable接口:表示线程所执行的 “工作” 的接口
- java.util.concurrent.ThreadFactory接口:将线程创建抽象化了的接口
- java.util.concurrent.Executor接口:将线程执行抽象化了的接口
- java.util.concurrent.ExecutorService接口:将被复用的线程抽象化了的接口
- java.util.concurrent.ScheduledExecutorService接口:将被调度的线程的执行抽象化了的接口
- java.util.concurrent.Executors类:用于创建实例的工具类