本篇是《图解Java多线程设计模式》第十一章的读书笔记。
有一个储物室,里面有很多储物柜。每个人拿着自己的钥匙去开自己的储物柜,虽然进同一个储物室,但彼此互不干扰。这就是 Thread-Local Storage 线程中的局部存储空间。
来看一个例子:每个线程将信息打印到自己的日志文件中。
TSLog :打印类,每个线程都有自己的 TSLog 对象,向自己的日志文件中打印信息。
public class TSLog {
private PrintWriter writer;
public TSLog(String filename) {
try {
writer = new PrintWriter(new FileWriter(filename));
} catch (IOException e) {
e.printStackTrace();
}
}
public void println(String s) {
writer.println(s);
}
public void close() {
System.out.println("=====End of log======");
writer.close();
}
}
Log:利用 ThreadLocal 获取请求对应的 TSLog 对象。
public class Log {
private static final ThreadLocal<TSLog> tsLogCollection = new ThreadLocal<>();
private static TSLog getTSLog() {
TSLog tsLog = tsLogCollection.get();
if (tsLog == null) {
tsLog = new TSLog(Thread.currentThread().getName()+"-log.txt");
tsLogCollection.set(tsLog);
}
return tsLog;
}
public static void println(String s) {
getTSLog().println(s);
}
public static void close() {
getTSLog().close();
}
}
ClientThread:请求线程
public class ClientThread extends Thread {
public ClientThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(getName()+" BEGIN");
for (int i = 0; i < 10; i++) {
Log.println("i = "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Log.close();
System.out.println(getName()+" END");
}
}
Main:测试
public class Main {
public static void main(String[] args) {
new ClientThread("Zhao").start();
new ClientThread("Qian").start();
new ClientThread("Sun").start();
}
}
类的关系图:
ThreadLocal 是线程特有的存储空间的意思,也就是只属于该线程,各个线程各自专用。
虽然 Thread-local storage 模式没有使用到锁,但这并不意味着吞吐量的提高,因为 ThreadLocal 存储/获取操作会产生额外的性能开销,该模式的重点在于没有显示的执行互斥处理,导致编程时犯错的可能性减小。
由于 ThreadLocal 会自动判断当前的线程,这相当于在程序中引入了上下文,这有一定危险,因为我们看不到处理中所用到的信息。例如在找Bug时,我们关注信息的流向,但上下文的引入导致当前Bug的产生可能是程序之前的行为导致的,也就导致定位问题的难度。
基于角色与基于任务
关于线程与线程使用的信息之间的关系,由基于角色和基于任务两种思考方式。
主体与客体
要想组装塑料模型,以下二者缺一不可。
- 组装塑料模型的人
- 塑料模型套件
同理假设我们需要让线程去完成一项工作,那么以下二者缺一不可:
- 工作的线程
- 工作所需的信息
总结以下就是:
- 用于做某事的主体
- 用于做某事的客体
在设计多线程程序时,根据以 主体 为主还是以 客体 为主的不同产生了以下两种方式
- 基于角色:以主体为主
- 基于任务:以客体为主
基于角色的考虑方式
即”线程最伟大"的方式。线程的实例来保存进行工作所必须的信息(上下文,状态)。这样减少了线程之间的交互信息量。一个线程使用从其它线程接收到的信息来执行处理(指的是线程间的通信),改变自己的内部状态。我们将这种线程称为角色。用代码来表示大致如下:
class Actor extends Thread {
// 角色的内部状态
public void run() {
// 循环的从外部接收并执行任务,改变内部状态
}
}
该书的第12章介绍的 Active Object 模式就是一种角色。
基于任务的考虑方式
即“任务最伟大”的方式。信息保存在线程之间交互的实例中。不仅是数据还有请求的方法都在该实例中。我们称该实例为信息,请求或是命令。这里暂称为任务,任务储存了足够的信息,可以说这是一种 富任务 往来于 轻线程之间的方式。典型就是书的第8章介绍的 Worker Thread 模式。
用代码表示大致如下:
class Task implements Runnable {
// 进行工作所必须的信息
public void run() {
// 工作的处理内容
}
}
java.util.TimerTask
类就是一个基于任务的类。该类实现了 Runnable 接口,它会被 java.util.Timer
类调用。如果要定义一项在一定时间后进行的工作或是顶起进行的工作,可以使用该类。
java.util.concurrent.FutureTask
类也是一个基于任务的类。
实际上两种方式是综合在一起的
我们通常采用的是 角色之间通过任务交互 的形式。