看看线程特有对象ThreadLocal
阅读原文时间:2023年07月08日阅读:4

作用:设计线程安全的一种技术。 在使用多线程的时候,如果多个线程要共享一个非线程安全的对象,常用的手段是借助锁来实现线程的安全。线程安全隐患的前提是多线程共享一个不安全的对象 ,那么有没有办法让线程之间不共享这个对象,就像你和我,每个人都有自己的一个苹果,你吃你的,我吃我的,你我互不干涉,来达到线程的安全?有 !在java.lang包下有一个类叫ThreadLocal,让线程之间各自持有自己的对象T。

来看看 来自多线程编程指南的一个案列,的使用方式和其工作原理

/*
授权声明:
本源码系《Java多线程编程实战指南(核心篇)》一书(ISBN:978-7-121-31065-2,以下称之为“原书”)的配套源码,
欲了解本代码的更多细节,请参考原书。
本代码仅为原书的配套说明之用,并不附带任何承诺(如质量保证和收益)。
以任何形式将本代码之部分或者全部用于营利性用途需经版权人书面同意。
将本代码之部分或者全部用于非营利性用途需要在代码中保留本声明。
任何对本代码的修改需在代码中以注释的形式注明修改人、修改时间以及修改内容。
本代码可以从以下网址下载:
https://github.com/Viscent/javamtia
http://www.broadview.com.cn/31065
*/
package io.github.viscent.mtia.ch6;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 该类可能导致内存泄漏!
* @author Viscent Huang
*/
@WebServlet("/memoryLeak")
public class ThreadLocalMemoryLeak extends HttpServlet {
private static final long serialVersionUID = 4364376277297114653L;
final static ThreadLocal counterHolder = new ThreadLocal() {
@Override
protected Counter initialValue() {
Counter tsoCounter = new Counter();
return tsoCounter;
}
};

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doProcess(req, resp);
try (PrintWriter pwr = resp.getWriter()) {
pwr.printf("Thread %s,counter:%d",
Thread.currentThread().getName(),
counterHolder.get().getAndIncrement());
}
}

private void doProcess(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
IOException {
counterHolder.get().getAndIncrement();
// 省略其他代码
}
}

// 非线程安全
class Counter {
private int i = 0;
public int getAndIncrement() {
return i++;
}
}

ThreadLocalMemoryLeak 通过ThreadLocal counterHolder 来实现每个线程有各自的对象Counter,那么是只能用来实现每个线程都有自己Counter呢 ?先看看ThreaLocal的源码 ,ThreadLocal的内部成员结构:通过查看ThreadLocal的方法,我们可以往ThreadLocal中设置成员T,只有set方法,那么跟踪下set方法的源码先

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

ThreadLocal.ThreadLocalMap threadLocals = null; //原来threadLocals 就是ThreadLocal的内部类。

可以看到set方法内通过Thread获取到当前线程,然后操作当前的线程,获取到ThreadLocal的内部类ThreadLocalMap,然后往这个Map中把值Value set进去 ,所以现在知道了。设置的值Value 就是往当前线程的ThreadLocalMap中设置值,这样的话当然只有跟当前线程有关和别的线程无关了 ,所有达到了各自的线程有各自的对象T了。看看ThreadLocalMap的set方法又做了什么 :

private void set(ThreadLocal key, Object value) {

        Entry\[\] tab = table;  
        int len = tab.length;  
        int i = key.threadLocalHashCode & (len-1);

        for (Entry e = tab\[i\];  
             e != null;  
             e = tab\[i = nextIndex(i, len)\]) {  
            ThreadLocal<?> k = e.get();

            if (k == key) {  
                e.value = value;  
                return;  
            }

            if (k == null) {  
                replaceStaleEntry(key, value, i);  
                return;  
            }  
        }

        tab\[i\] = new Entry(key, value);  
        int sz = ++size;  
        if (!cleanSomeSlots(i, sz) && sz >= threshold)  
            rehash();  
    }

一看原来value 值放在了Entry中 。而每一个Entry都是ThreadLocalMap成员变量 tab数组的一个值。Entry又是ThreadLocalMap的内部类。,内部类Entry继承了垃圾回收机制的引用类,用来判断是否实例对象可回收。

static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;

        Entry(ThreadLocal<?> k, Object v) {  
            super(k);  
            value = v;  
        }  
    }

所以现在可以看明白ThreadLocal持有对象 T,最后变成当前线程 currentThread持有Entry对象实例,Entry持有对象T。

但是案列中似乎并没有看到对ThreadLocal 对象set方法的操作 ,只看到 创建对象重写了方法initialValue。 看方法名意思是初始化持有对象T。但是代码并没有对ThreadLocalMap去赋值,而是返回持有对象T,通过查看引用原来在setInittialValue 方法中有调用initialVlaue,setInitialValue又在get方法中调用 ,看看get方法的源码

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

原来在get获取对象T的时候 ,如果原先没有set值进去 ,执行initialValu方法,并把值设置到当前线程持有对象ThreadLocalMap中。

使用ThreadLocal的优劣势:

  1.因为没有使用到线程锁,可以避免线程上下文 的切换 。

  2.造成内存的泄露

  3.退化与数据错乱 。

  更多的详细参考数据《java多线程实战指南》