java线程实现方式(线程间的变量访问)
基本概念和用法线程本地变量是说,每个线程都有同一个变量的独有拷贝,这个概念听上去比较难以理解,我们先直接来看类TheadLocal的用法 。
ThreadLocal是一个泛型类,接受一个类型参数T,它只有一个空的构造方法,有两个主要的public方法:
public T get()public void set(T value)set就是设置值,get就是获取值,如果没有值,返回null,看上去,ThreadLocal就是一个单一对象的容器,比如:
public static void main(String[] args) {ThreadLocal<Integer> local = new ThreadLocal<>();local.set(100);System.out.println(local.get());}输出为100 。
那ThreadLocal有什么特殊的呢?特殊发生在有多个线程的时候,看个例子:
public class ThreadLocalBasic {static ThreadLocal<Integer> local = new ThreadLocal<>();public static void main(String[] args) throws InterruptedException {Thread child = new Thread() {@Overridepublic void run() {System.out.println("child thread initial: " + local.get());local.set(200);System.out.println("child thread final: " + local.get());}};local.set(100);child.start();child.join();System.out.println("main thread final: " + local.get());}}local是一个静态变量,main方法创建了一个子线程child,main和child都访问了local,程序的输出为:
child thread initial: nullchild thread final: 200main thread final: 100这说明,main线程对local变量的设置对child线程不起作用,child线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local,但每个线程都有自己的独立的值,这就是线程本地变量的含义 。
除了get/set,ThreadLocal还有两个方法:
protected T initialValue()public void remove()initialValue用于提供初始值,它是一个受保护方法,可以通过匿名内部类的方式提供,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回null 。remove删掉当前线程对应的值,如果删掉后,再次调用get,会再调用initialValue获取初始值 。看个简单的例子:
public class ThreadLocalInit {static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {return 100;}};public static void main(String[] args) {System.out.println(local.get());local.set(200);local.remove();System.out.println(local.get());}}输出值都是100 。
使用场景ThreadLocal有什么用呢?我们来看几个例子 。
DateFormat/SimpleDateFormatThreadLocal是实现线程安全的一种方案,比如对于DateFormat/SimpleDateFormat,我们在32节介绍过日期和时间操作,提到它们是非线程安全的,实现安全的一种方式是使用锁,另一种方式是每次都创建一个新的对象,更好的方式就是使用ThreadLocal,每个线程使用自己的DateFormat,就不存在安全问题了,在线程的整个使用过程中,只需要创建一次,又避免了频繁创建的开销,示例代码如下:
public class ThreadLocalDateFormat {static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};public static String date2String(Date date) {return sdf.get().format(date);}public static Date string2Date(String str) throws ParseException {return sdf.get().parse(str);}}需要说明的是,ThreadLocal对象一般都定义为static,以便于引用 。
ThreadLocalRandom即使对象是线程安全的,使用ThreadLocal也可以减少竞争,比如,我们在34节介绍过Random类,Random是线程安全的,但如果并发访问竞争激烈的话,性能会下降,所以Java并发包提供了类ThreadLocalRandom,它是Random的子类,利用了ThreadLocal,它没有public的构造方法,通过静态方法current获取对象,比如:
public static void main(String[] args) {ThreadLocalRandom rnd = ThreadLocalRandom.current();System.out.println(rnd.nextInt());}current方法的实现为:
public static ThreadLocalRandom current() {return localRandom.get();}localRandom就是一个ThreadLocal变量:
private static final ThreadLocal<ThreadLocalRandom> localRandom =new ThreadLocal<ThreadLocalRandom>() {protected ThreadLocalRandom initialValue() {return new ThreadLocalRandom();}};复制代码上下文信息ThreadLocal的典型用途是提供上下文信息,比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很啰嗦,这时,使用ThreadLocal就很方便,所以它被用于各种框架如Spring中,我们看个简单的示例:
public class RequestContext {public static class Request { //...};private static ThreadLocal<String> localUserId = new ThreadLocal<>();private static ThreadLocal<Request> localRequest = new ThreadLocal<>();public static String getCurrentUserId() {return localUserId.get();}public static void setCurrentUserId(String userId) {localUserId.set(userId);}public static Request getCurrentRequest() {return localRequest.get();}public static void setCurrentRequest(Request request) {localRequest.set(request);}}在首次获取到信息时,调用set方法如setCurrentRequest/setCurrentUserId进行设置,然后就可以在代码的任意其他地方调用get相关方法进行获取了 。
基本实现原理ThreadLocal是怎么实现的呢?为什么对同一个对象的get/set,每个线程都能有自己独立的值呢?我们直接来看代码 。
set方法的代码为:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}它调用了getMap,getMap的代码为:
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}返回线程的实例变量threadLocals,它的初始值为null,在null时,set调用createMap初始化,代码为:
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}从以上代码可以看出,每个线程都有一个Map,类型为ThreadLocalMap,调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value 。ThreadLocalMap是一个内部类,它是专门用于ThreadLocal的,与一般的Map不同,它的键类型为WeakReference<ThreadLocal>,我们没有提过WeakReference,它与Java的垃圾回收机制有关,使用它,便于回收内存,具体我们就不探讨了 。
get方法的代码为:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}return setInitialValue();}通过线程访问到Map,以ThreadLocal对象为键从Map中获取到条目,取其value,如果Map中没有,调用setInitialValue,其代码为:
private T setInitialValue() {T value = https://www.hedan60.com/baike2/initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}initialValue()就是之前提到的提供初始值的方法,默认实现就是返回null 。
remove方法的代码也很直接,如下所示:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}简单总结下,每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键读写当前线程的Map,这样,就实现了每个线程都有自己的独立拷贝的效果 。
线程池与ThreadLocal我们知道,线程池中的线程是会重用的,如果异步任务使用了ThreadLocal,会出现什么情况呢?可能是意想不到的,我们看个简单的示例:
public class ThreadPoolProblem {static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() {@Overrideprotected AtomicInteger initialValue() {return new AtomicInteger(0);}};static class Task implements Runnable {@Overridepublic void run() {AtomicInteger s = sequencer.get();int initial = s.getAndIncrement();// 期望初始为0System.out.println(initial);}}public static void main(String[] args) {executorService executor = Executors.newFixedThreadPool(2);executor.execute(new Task());executor.execute(new Task());executor.execute(new Task());executor.shutdown();}}对于异步任务Task而言,它期望的初始值应该总是0,但运行程序,结果却为:
001第三次执行异步任务,结果就不对了,为什么呢?因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空,修改后的值带到了下一个异步任务 。那怎么办呢?有几种思路:
第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove使用完ThreadLocal对象后,总是调用其remove方法使用自定义的线程池我们分别来看下,对于第一种,在Task的run方法开始处,添加set或remove代码,如下所示:
static class Task implements Runnable {@Overridepublic void run() {sequencer.set(new AtomicInteger(0));//或者 sequencer.remove();AtomicInteger s = sequencer.get();//...}}对于第二种,将Task的run方法包裹在try/finally中,并在finally语句中调用remove,如下所示:
static class Task implements Runnable {@Overridepublic void run() {try{AtomicInteger s = sequencer.get();int initial = s.getAndIncrement();// 期望初始为0System.out.println(initial);}finally{sequencer.remove();}}}以上两种方法都比较麻烦,需要更改所有异步任务的代码,另一种方法是扩展线程池ThreadPoolExecutor,它有一个可以扩展的方法:
protected void beforeExecute(Thread t, Runnable r) { }在线程池将任务r交给线程t执行之前,会在线程t中先执行beforeExecure,可以在这个方法中重新初始化ThreadLocal 。如果知道所有需要初始化的ThreadLocal变量,可以显式初始化,如果不知道,也可以通过反射,重置所有ThreadLocal,反射的细节我们会在后续章节进一步介绍 。
我们创建一个自定义的线程池MyThreadPool,示例代码如下:
static class MyThreadPool extends ThreadPoolExecutor {public MyThreadPool(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue) {super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);}@Overrideprotected void beforeExecute(Thread t, Runnable r) {try {//使用反射清空所有ThreadLocalField f = t.getClass().getDeclaredField("threadLocals");f.setAccessible(true);f.set(t, null);} catch (Exception e) {e.printStackTrace();}super.beforeExecute(t, r);}}这里,使用反射,找到线程中存储ThreadLocal对象的Map变量threadLocals,重置为null 。使用MyThreadPool的示例代码如下:
public static void main(String[] args) {ExecutorService executor = new MyThreadPool(2, 2, 0,TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>());executor.execute(new Task());executor.execute(new Task());executor.execute(new Task());executor.shutdown();}使用以上介绍的任意一种解决方案,结果就符合期望了 。
小结本节介绍了ThreadLocal的基本概念、用法用途、实现原理、以及和线程池结合使用时的注意事项,简单总结来说:
ThreadLocal使得每个线程对同一个变量有自己的独立拷贝,是实现线程安全、减少竞争的一种方案 。ThreadLocal经常用于存储上下文信息,避免在不同代码间来回传递,简化代码 。每个线程都有一个Map,调用ThreadLocal对象的get/set实际就是以ThreadLocal对象为键读写当前线程的该Map 。在线程池中使用ThreadLocal,需要注意,确保初始值是符合期望的 。原文链接:https://juejin.cn/post/6844903475847888909【java线程实现方式(线程间的变量访问)】更多信息请关注@软件老王,关注不迷路,软件老王和他的IT朋友们,分享一些他们的技术见解和生活故事 。
- 学渣|北大教授吐槽学渣女儿,“我实现了开挂人生,闺女却让我认了命”
- 原生家庭|2020年底“婴儿潮”失约了?专家鼓励高学历人群生二胎,能实现吗
- 腰围|写下来的愿望更容易实现:腰围是少女和大妈的分水岭,要拼死保持
- 新闻记者|国家卫健委:到2025年实现每千人口拥有3岁以下婴幼儿托位数4.5个
- 如何实现高质量发展(高质量小说推荐)
- 实现夙愿(什么样的夙愿)
- 补钙|孩子梦想身高1米8参加篮球队,多吃补钙菜,个头蹭蹭长,愿望实现
- 小学阶段的教育培养目标(中小学为实现教育目标)
- 写作的价值何在(实现价值议论文)
- 中年人|中年人的兴趣班,在孩子双减之后实现了
