【图解Java多线程设计模式】Thread-Specific Storage模式

即使只有一个入口,也会在内部为每个线程分配特有的存储空间。
在Java标准类库中,java.lang.ThreadLocal类实现了该模式。

关于java.lang.ThreadLocal类

ThreadLocal的实例会管理多个对象

由于一个ThreadLocal的实例可以管理多个对象,所以ThreadLocal定义了可以“存储”(set)和“获取”(get)对象的方法。

set方法

ThreadLocal类的set方法用于将通过参数接收的实例与调用该方法的线程(当前线程)对应并存储起来。这里存储的对象可以通过get方法获取。set方法中没有表示线程的参数。set方法会先查询当前线程(即表达式Thread.currentThread()的值),然后以它作为键来存储实例。

get方法

ThreadLocal类的get方法用于获取与调用get方法的线程(当前线程)对应的实例。该线程之前通过set方法存储的实例就是get方法的返回值。如果之前一次都还没有调用过set方法,则get方法的返回值为null。

与set方法一样,get方法中也没有表示线程的参数。这是因为,get方法也会去查询当前线程。即get方法会以当前线程自身作为健去获取对象。

java.lang.ThreadLocal与泛型

java.lang.ThreadLocal是一个泛型类,可以通过参数的类型来指定要存储的对象的类型。ThreadLocal类的声明大致如下:

1
2
3
4
5
6
7
8
9
public class ThreadLocal<T> {
public void set(T value) {
...
}

public T get() {
...
}
}

即,通过ThreadLocal的T指定的类型就是set方法的参数的类型以及get方法的返回值的类型。

示例1:不使用Thread-Specific Storage模式

使用Log类的静态方法将日志记录在文件中。

类图

k0hTkd.png

Log.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class Log {
private static PrintWriter writer = null;

static {
try {
writer = new PrintWriter(new FileWriter("log.txt"));
} catch (IOException e) {
e.printStackTrace();
}
}

public static void println(String s) {
writer.println(s);
}

public static void close() {
writer.println("==== End of log ====");
writer.close();
}
}

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {

public static void main(String[] args) {
System.out.println("BEGIN");

for (int i = 0; i < 10; i++) {
Log.println("main: i = " + i);

try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}

Log.close();
System.out.println("END");
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BEGIN
END

# log.txt
main: i = 0
main: i = 1
main: i = 2
main: i = 3
main: i = 4
main: i = 5
main: i = 6
main: i = 7
main: i = 8
main: i = 9
==== End of log ====

Timethreads图

k046Hg.png

示例2:使用了Thread-Specific Storage模式

制作一种程序结构,在其中编写一个继承了Thread的类,但是并不在它的字段中保存PrintWriter的实例,而只是机械地在方法中调用Log.println。仅仅这样就可以实现自动地将字符串分配到各个线程的日志文件中。

类图

k04R4s.png

TSLog.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class TSLog {
private PrintWriter writer = null;

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() {
writer.println("==== End of log ====");
writer.close();
}
}

Log.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Log {
private static final ThreadLocal<TSLog> tsLogCollection = new ThreadLocal<TSLog>();

public static void println(String s) {
getTSLog().println(s);
}

public static void close() {
getTSLog().close();
}

private static TSLog getTSLog() {
TSLog tsLog = tsLogCollection.get();

if (tsLog == null) {
tsLog = new TSLog(Thread.currentThread().getName() + "-log.txt");
tsLogCollection.set(tsLog);
}

return tsLog;
}
}

ClientThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
}
}

Log.close();
System.out.println(getName() + " END");
}
}

Main.java

1
2
3
4
5
6
7
8
public class Main {

public static void main(String[] args) {
new ClientThread("Alice").start();
new ClientThread("Bobby").start();
new ClientThread("Chris").start();
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Bobby BEGIN
Chris BEGIN
Alice BEGIN
Bobby END
Chris END
Alice END

# Alice-log.txt、Bobby-log.txt、Chris-log.txt
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
==== End of log ====

Timethreads图

k0qOcd.png

登场角色

Client(委托者)

Client角色将处理委托给TSObjectProxy角色。一个TSObjectProxy角色会被多个Client角色使用。
在示例程序中,由ClientThread类扮演此角色。

TSObjectProxy(线程特有的对象的代理人)

TSObjectProxy角色会执行多个Client角色委托给它的处理。
首先,TSObjectProxy角色使用TSObjectCollection角色获取与Client角色对应的TSObject角色。接着,它将处理委托给TSObject角色。
在示例程序中,由Log类扮演此角色。

TSObjectCollection(线程特有的对象的集合)

TSObjectCollection角色有一张Client角色与TSObject角色之间的对应表。
当getTSObject方法被调用后,它会去查看对应表,返回与Client角色相对应的TSObject角色。另外,当setTSObject方法被调用后,它会将Client角色与TSObject角色之间的键值对应关系设置到对应表中。
在示例程序中,由java.lang.ThreadLocal类扮演此角色。

TSObject(线程特有的对象)

TSObject角色中保存着线程特有的信息。
TSObject角色由TSObjectCollection角色管理。TSObject角色的方法只会被单线程调用。
在示例程序中,由TSLog类扮演此角色。

类图

kBSlon.png

时序图

新创建TSObject角色

kBStQU.png

多个Client角色访问各自的TSObject角色

kBS3iq.png