在说我们的主人公ThreadLocal前,我先给大家说一个场景,大家想想应该怎么做。
场景:
路由器、交换机在运行的过程中有可能出现问题,现在我们想收集各个设备的日志信息,收集到的日志我们要存到本地文件当中的,每台设备要有专门一个文件夹与之对应,文件夹的名称就以设备的IP为名,在文件夹内记录是哪天的日志,每天建一个文件夹,如果某设备当天的日志太多,一个文件存放的话不利于阅读,那么就当文件大小达到我们设定的值时生成新的日志文件,后续的日志写到新的日志文件当中。
直接说可能不太好懂,我截几张图,第一张图如下,可以看到我把日志都放到了本地E盘的deviceLog目录下了,这里有6台设备的日志。
接着我们看第二张图,我们进入192.168.156.10这个文件夹,如下图所示,可以看到这里面存了4天的日志。
接着我们看第三张图,我们再进入20170612这个文件夹,看看12号的日志文件,如下图所示,可以看到共有两个日志文件,第一个日志文件达到了设定的最大值1M,因此另起了一个日志文件。
看了这三张图,相信大家知道在干什么了吧。针对这个场景,大家有什么好的实现思路?这里我把我的思路说一下,不当之处还请见谅,不过可以学习下我们今天的主人公ThreadLocal。
实际生产环境中有可能有成千上万台设备在运行,我们要搜集这些设备的日志信息,需要用Socket来接收日志信息,由于同一时刻可能有很多日志发过来,直接处理的话,有可能无法处理所有请求,从而导致有些日志无法被记录下来。解决方法是我们可以先把接收到的日志写到一个队列当中,然后专门用一个线程不断去队列中尝试拿日志,拿出日志后判断这条日志属于哪台设备(日志中有设备IP的信息),每台设备的日志我们专门起一个线程去处理,我们用一个Map来存放设备IP与线程的关系,以设备IP为key,以线程为value,这样拿到一个日志后通过IP就可以得到对应的线程。我们知道,要将日志写到本地的log文件当中的话,是需要用到IO流的,如FileOutputStream或FileWriter等,每个IO流专门处理一台设备的日志比较合适。
那么,问题来了,怎么将IO流与线程绑定到一起?
这时我们的主人公ThreadLocal便要上场了,如下所示,定义一个ThreadLocal,里面存放FileWriter,每创建一个线程就在ThreadLoal当中存放一个FileWriter。
private static final ThreadLocal<FileWriter> threadLocal = new ThreadLocal<>();
public Thread getWriteThread(String filepath){
try {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
FileWriter fileWriter;
try {
fileWriter = new FileWriter(new File(filepath));
threadLocal.set(fileWriter);
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread.start();
return thread;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
这样做有什么好处呢?好处就是线程在处理日志的时候,直接使用threadLocal.get()方法便可以获取该线程的FileWriter,该FileWriter只属于该线程,这样FileWriter很明确应该把日志写到哪个目录的哪个文件中。线程与线程之间毫无关系,各干各的,我们不用费劲的维护线程与FileWriter之间的关系,threadLocal帮我们自动完成了。是不是很棒呢?
//只是举个例子
public void write(){
FileWriter fileWriter = threadLocal.get();
try {
fileWriter.write("xxxxxxxxxxxxx");
} catch (IOException e) {
e.printStackTrace();
}
}
ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。
ThreadLocal的应用场合,我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
ThreadLocal与同步没有什么关系,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化我们的程序,使程序更加易读、简洁。ThreadLocal类为各线程提供了存放局部变量的场所。
JDK中ThreadLocal的实现:
并非在ThreadLocal中有一个Map,而是在每个Thread中存在这样一个Map,具体是ThreadLocal.ThreadLocalMap。当用set时候,往当前线程里面的Map里 put 的key是当前的ThreadLocal对象。而不是把当前Thread作为Key值put到ThreadLocal中的Map里。
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode = 0;
private static final int HASH_INCREMENT = 0x61c88647;
private static synchronized int nextHashCode() {
int h = nextHashCode;
nextHashCode = h + HASH_INCREMENT;
return h;
}
public ThreadLocal() {
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
return (T)map.get(this);
// Maps are constructed lazily. if the map for this thread
// doesn't exist, create it, with this ThreadLocal and its
// initial value as its only entry.
T value = initialValue();
createMap(t, value);
return value;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//this代表当前ThreadLocal实例,如果map中已经有该实例了,那么就是覆盖value操作
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);//this代表当前ThreadLocal实例
}
.......
static class ThreadLocalMap {
........
}
}
ThreadLocal内存泄漏:
每个Thread实例都具备一个ThreadLocal的map,以ThreadLocal Instance为key,以绑定的Object为Value。而这个map不是普通的map,它是在ThreadLocal中定义的,它和普通map的最大区别就是它的Entry是针对ThreadLocal弱引用的,即当外部ThreadLocal引用为空时,map就可以把ThreadLocal交给GC回收,从而得到一个null的key。
这个threadlocal内部的map在Thread实例内部维护了ThreadLocal Instance和bind value之间的关系,这个map有threshold,当超过threshold时,map会首先检查内部的ThreadLocal(前文说过,map是弱引用可以释放)是否为null,如果存在null,那么释放引用给gc,这样保留了位置给新的线程。如果不存在slate threadlocal,那么double threshold。除此之外,还有两个机会释放掉已经废弃的threadlocal占用的内存,一是当hash算法得到的table index刚好是一个null key的threadlocal时,直接用新的threadlocal替换掉已经废弃的。另外每次在map中新建一个entry时(即没有和用过的或未清理的entry命中时),会调用cleanSomeSlots来遍历清理空间。此外,当Thread本身销毁时,这个map也一定被销毁了(map在Thread之内),这样内部所有绑定到该线程的ThreadLocal的Object Value因为没有引用继续保持,所以被销毁。
从上可以看出Java已经充分考虑了时间和空间的权衡,但是因为置为null的threadlocal对应的Object Value无法及时回收。map只有到达threshold时或添加entry时才做检查,不似gc是定时检查,不过我们可以手工轮询检查,显式调用map的remove方法,及时的清理废弃的threadlocal内存。需要说明的是,只要不往不用的threadlocal中放入大量数据,问题不大,毕竟还有回收的机制。
综上,废弃threadlocal占用的内存会在3中情况下清理:
1 thread结束,那么与之相关的threadlocal value会被清理
2 GC后,thread.threadlocals(map) threshold超过最大值时,会清理
3 GC后,thread.threadlocals(map) 添加新的Entry时,hash算法没有命中既有Entry时,会清理
那么何时会“内存泄露”?当Thread长时间不结束,存在大量废弃的ThreadLocal,而又不再添加新的ThreadLocal(或新添加的ThreadLocal恰好和一个废弃ThreadLocal在map中命中)时。