Thread-Specific Storage Pattern

来源:互联网 发布:量子引力 知乎 编辑:程序博客网 时间:2024/06/05 04:51

什么是Thread-Specific Storage Pattern?

现在有一个保管箱,许多投币保管箱都排在一起,每个人拿着自己的要是进来了,退出的时候拿着自己的行李。又有一个人拿着自己的钥匙进来了,打开自己的保管箱,拿走自己的行李,而不会破坏其他人的行李。

这个模式名称翻译成中文就是“线程独有的储藏库”。这个模式只有一个入口,但是内部对每个线程提供特有的存储空间。


首先介绍以下java.lang.ThreadLocal类

java.lang.ThreadLocal的实例可以想象成一种集合架构(collection)。也就是说,ThreadLocal的实例只有一个,但是可以管理多个对象。因为ThreadLocal的实例管理了多个对象,所以ThreadLocal拥有存放(set)和取得(get)方法。

public void set()

ThreadLocal类的实例有一个set方法,可以将参数指定的实例存放到调用set方法的线程所对应的存储空间。这里存放的实例,可以调用get方法取得。这个set方法是没有用传入参数的,而是检查当前线程,也就是Thread.currentThread()的值,自动以这个值作为key存放实例,相当于把自己的行李放进保管箱一样。

public Object get()

ThreadLocal类的get方法,可以调用get方法的线程对应的实例(现在线程对应的实例)。之前set的实例,就是现在get的返回值。如果没有set过,就返回null。调用get时,相当于从自己的保管箱拿出行李一样。与set方法一样,get方法没有表示线程的参数,因为程序会在get里自己去检查现在的线程,也就是自己本身就是键值。


下面设想一种情况,我们设置3个线程,每个线程负责对一个不同的文件进行写入。利用ThreadLocal,我们给每个线程设置独立的文件名称属性,使得线程得以对不同文件进行写入。

下面我们的代码测试

首先是我们的TSLog(表示的意思是ThreadSpecificLog,线程特有Log类)类:

package justTest;import java.io.FileWriter;import java.io.IOException;import java.io.PrintWriter;public class TSLog {    private PrintWriter writer = null;    // 初始化writer字段    public TSLog(String fileName) {        try {            writer = new PrintWriter(new FileWriter(fileName));        } catch (IOException e) {            e.printStackTrace();        }    }    // 加入一条log    public void println(String s) {        writer.println(s);    }    // 关闭log    public void close() {        writer.println("====End of log====");        writer.close();    }}
接下来是我们的Log类,主要作用是获取当前线程的TSLog,然后调用其方法:

package justTest;public class Log {    private static final ThreadLocal<TSLog> tsLogCollection = new ThreadLocal<TSLog>();        //加入一条log    public static void println(String s){        getTSLog().println(s);    }        //关闭log    public static void close(){        getTSLog().close();    }        //取得线程特有的log    private static TSLog getTSLog(){        TSLog tsLog = (TSLog)tsLogCollection.get();                //如果线程第一次调用 就建立新文件并且注册log        if(tsLog == null){            tsLog = new TSLog(Thread.currentThread().getName()+"-log.txt");            tsLogCollection.set(tsLog);        }        return tsLog;    }}
下面是ClientThread类,继承自Thread类,使用Log.println和Log.close方法。
package justTest;public class ClientThread extends Thread {    public ClientThread(String name){        super(name);    }    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");    }}
最后是我们的Test类,启动ClientThread线程:

package justTest;public class Test {    public static void main(String[] args) {        new ClientThread("Alice").start();        new ClientThread("Bobby").start();        new ClientThread("Chris").start();    }}
现在回头再来看看我们的代码:

在TSLog类中,这个类负责建立线程特有的log记录,而Log类负责调用TSLog类下的方法以及获得线程特有的log,骑宠tsLogCollection字段就是用来储藏每个线程的TSLog实例的保管箱。其他的没什么可说的,前面几篇文章说太多了。


Thread-Specific Storage Pattern的所有参与者

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参与者的组合。比如示例中的ThreadLocal类。

TSObject(线程独有的对象)参与者

TSObject参与者存放有线程特有的信息,这个参与者由TSObjectCollection管理。TSObject参与者的方法只会由单线程调用。比如示例中的TSLog类。


扩展思考

局部变量与java.lang.ThreadLocal类

线程本来就有特有的区域,就是存放方法局部变量的堆栈。在方法里分配的局部变量都是线程独有的,无法被其他线程访问到。但是这些变量一旦推出方法就会消失。而ThreadLocal则是和方法调用无关,为线程分配特有空间的类。

放置线程特有信息的地方

1、线程外(thread-external)

java.lang.ThreadLocal的示例可以说是保管箱间。每个线程的保管箱都集中在保管箱间里。线程不会带着保管箱到处跑。像这样就被称为线程外的信息存放。这样的方式不需要修改表示线程的现有类,然而也存在线程类不容易被阅读的危险,毕竟无法得知其他类中存放有线程的信息。

2、线程内(thread-internal)

现在有一个Thread类的子类MyThread,MyThread的字段就是线程特有的信息,这时我们就称呼位线程内存放的信息。

将特有信息存放在线程外就像是自己所有,但是自己不一定带在身上;

而特有信息存放在线程的方式,就像拿在自己手上一样。

不必担心被其他线程访问

这个模式下线程独有的内存空间是不用担心被其他线程擅自乱动的。

这是很重要的性质,共享互斥是很重要的,但是实现共享互斥并不容易,而且要求共享实例不损坏,共享互斥执行性能偏低。然而这个模式保证了线程内部的属性绝对不会被其他线程碰到,而且没有出现进行共享互斥的操作(也许底层有,但是至少不用我们写),是很方便的架构。毕竟这个模式没有出现SharedResource。

throughput的提升取决于实现

虽然这个模式需要我们写的代码没有共享互斥,但是程序的throughput不见得一定比Single Threaded Execution Pattern来得高。因为互斥控制可能存在于TSObjectCollection参与者内,另外通过TSObjectProxy参与者调用方法也需要花费时间(毕竟要通过TSObject)。

然而这个模式的优点不是在于throughput上,而是复用性:

1、不需要改变程序的结构

2、互斥共享不在表面上,减少了犯错的机会

隐藏了context

这应该可以说是这个模式的一个缺点了,TSObjectCollection参与者(相当于ThreadLocal)会自己判断现在的线程而不需要程序员传入,虽然方便,但是也危险。