Java并发:concurrent 包,Java并发:ThreadLocal
21.7 新类库中的构件
Java SE5的 包中引入了大量设计用来解决并发问题的新类。学习这些“工具类”就可以专注于自己想要实现的功能而不是线程的同步、死锁等一堆令人头疼的细节。这一小节内容非常多,建议的学习方法是:java.util.concurrent
- 首先看目录,了解这一小节主要讲的是哪几种构件
- 通过构件的名字猜猜它们想实现的功能,然后通过查询文档总结一下每个构件的特点,适用的场景
- 尝试着去寻找项目中涉及到的点,然后具体学习这个构件的知识,之后用新构件重新实现这一块作为巩固
嗯,上面总结了一下学习这个小节的步骤(其实是因为太多了。。。。。我不想全看 T_T),那么我们就把目录摘出来看看吧。
一、前言
下面是21.7小节的目录。嗯,发现一共是7个构件,现在从文档出发,逐个浏览一下:
- 21.7 新类库中的构件
- 21.7.1 CountDownLatch
- 21.7.2 CyclicBarrier
- 21.7.3 DelayQueue
- 21.7.4 PriorityBlockingQueue
- 21.7.5 使用 ScheduledExecutor 的温室控制器
- 21.7.6 Semaphore
- 21.7.7 Exchanger
下面我们先简单的“望文生义”一下,然后再逐个击破:)
CountDownLatch:名字直译为——倒计时锁。官方文档的描述是 A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes. (一个线程同步辅助工具,可以让一个或多个线程等待直到其它线程的任务全部完成才会被唤醒。)CyclicBarrier:和上面那个功能相似,只是上面的倒计时数值不能被重置,只能递减到0停止;而 CyclicBarrier 可以在倒计时数减为0之后重用(还是原来的值)- DelayQueue:无界的 BlockingQueue(前面生产者-消费者讲过哦),用于放置实现了 Delayed interface 的对象,其中的对象只能在到期时才能在队列中取走。这种队列是有序的,即队头对象的延期到期的时间最长。
- PriorityBlockingQueue:优先队列的 BlockingQueue,具有可阻塞的读取操作。其实就是 BlockingQueue 的优先队列实现
- 使用 ScheduledExecutor 的温室控制器:
- Semaphore:正常的锁(concurrent.Lock 或者 synchronized)在任何时刻都只能允许一个任务访问资源,而 Semaphore (计数信号量)允许 N 个任务同时访问这个资源。(是不是有池子的感觉嘞??)
- Exchanger:两个任务之间交换对象的栅栏。意思是各自拥有对象,离开栅栏时,就拥有对方持有的对象了。典型就是一个任务生产对象,一个任务消费对象。(值得思考,为啥要交换?我直接用一个容器或者 BlockingQueue 完全可以解耦啊,这个到底用在哪里?)
二、代码来了
下面给每个构件都写个小例子,然后总结一下它们产生的原因和最佳使用场景。go go go!!
1. CountDownLatch
CountDownLatch 被用于:
主线程等待执行结束后多个子线程主线程再执行
因此,具体使用过程中:
- 主线程:定义 CountDownLatch 需要等待的子线程个数
- 子线程:调整 CountDownLatch 的剩余线程数
- 主线程: 阻塞等待子线程执行结束
countDownLatch.await()
从上面过程可以看出:主线程定义 CountDownLatch,子线程调整 CountDownLatch,主线程在 CountDownLatch 上保持等待状态。
文档也太详细了吧:
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
A CountDownLatch is initialized with a given count. The methods block until the current count reaches zero due to invocations of the method, after which all waiting threads are released and any subsequent invocations of return immediately. This is a one-shot phenomenon – the count cannot be reset. If you need a version that resets the count, consider using a CyclicBarrier【和 CyclicBarrier 的区别】.
awaitcountDown()await
A CountDownLatch is a versatile(多功能的) synchronization tool and can be used for a number of purposes. A CountDownLatch initialized with a count of one serves as a simple on/off latch, or gate: all threads invoking wait at the gate until it is opened by a thread invoking . A CountDownLatch initialized to N can be used to make one thread wait until N threads have completed some action, or some action has been completed N times.【这里是使用场景:count=1为开关;count=N 重复 N 次】
awaitcountDown()
A useful property of a CountDownLatch is that it doesn’t require that threads calling countDown wait for the count to reach zero before proceeding, it simply prevents any thread from proceeding past an await until all threads could pass.
同时文档提供了演示代码:
Here is a pair of classes in which a group of worker threads use two countdown latches:1.The first is a start signal that prevents any worker from proceeding until the driver is ready for them to proceed;2.The second is a completion signal that allows the driver to wait until all workers have completed.class Driver { // ...void main() throws InterruptedException {CountDownLatch startSignal = new CountDownLatch(1);CountDownLatch doneSignal = new CountDownLatch(N);for (int i = 0; i < N; ++i) // create and start threadsnew Thread(new Worker(startSignal, doneSignal)).start();doSomethingElse(); // don't let run yetstartSignal.countDown(); // let all threads proceeddoSomethingElse();doneSignal.await(); // wait for all to finish}}class Worker implements Runnable {private final CountDownLatch startSignal;private final CountDownLatch doneSignal;Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {this.startSignal = startSignal;this.doneSignal = doneSignal;}public void run() {try {startSignal.await();doWork();doneSignal.countDown();} catch (InterruptedException ex) {} // return;}void doWork() { ... }}
文档已经够清晰了,这里就不多废话了。
2. CyclicBarrier
使用 CyclicBarrier,多个之间,具体操作:子线程相互等待
- 主线程:定义 CyclicBarrier 需要等待的子线程个数
- 子线程:调用 等待其他线程
CyclicBarrier.await()
直译为循环栅栏,通过它可以让一组线程全部到达某个状态后再同时执行,也就是说假如有5个线程协作完成一个任务,那么只有当每个线程都完成了各自的任务(都到达终点),才能继续运行(开始领奖)。循环的意思是当所有等待线程都被释放(也就是所有线程完成各自的任务,整个程序开始继续执行)以后,CyclicBarrier 可以被重用。而上面的 CountDownLatch 只能用一次。
所有线程达到指定的状态后,一起继续执行:
package top.ningg.java.concurrent;import java.util.concurrent.BrokenBarrierException;import java.util.concurrent.CyclicBarrier;import java.util.concurrent.TimeUnit;public class TestOfCyclicBarrier {public static void main(String[] args) {// 说明:// 1. 创建 CyclicBarrier 时,可以指定一个 Runnable 的任务;// 2. 所有线程都到齐后,先执行这个任务,之后才会继续执行;CyclicBarrier cyclicBarrier = new CyclicBarrier(5);for (int index = 0; index < 5; index++) {Thread newThread = new Thread(new TaskOfCyclicBarrier(cyclicBarrier));newThread.start();}}}class TaskOfCyclicBarrier implements Runnable {private CyclicBarrier cyclicBarrier;public TaskOfCyclicBarrier(CyclicBarrier cyclicBarrier) {this.cyclicBarrier = cyclicBarrier;}@Overridepublic void run() {System.out.println("等待所有人到齐后,开始开会...");try {TimeUnit.SECONDS.sleep(3);cyclicBarrier.await();System.out.println("开始开会...");} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}}
CyclicBarrier 和 CountDownLatch 之间的差异:
- CyclicBarrier是 ,CountDownLatch是 ;
多个线程互相等待一个线程等待多个线程 - CyclicBarrier可以,而CountDownLatch
重复使用一次有效
3. DelayQueue
DelayQueue 就是一个无界队列,是用 PriorityQueue 实现的 BlockingQueue,如果要使用 DelayQueue,其中的元素必须实现 Delayed 接口,Delayed 接口有2个方法需要重写:compareTo()和 getDelay()方法。因为使用的是优先队列,所以需要确定元素之间的优先级,那么重写 compareTo()就很明显了,又为了满足 DelayQueue 的特性(每次队头是延期到期时间最长的元素),那么就需要知道元素的到期时间,而这个时间就是通过 getDelay()获取的。
延迟到期时间最长:这个刚看的时候还挺迷糊的,现在知道了。就是到期之后保存时间最长的元素。比如2个元素,在10:00:00这个时间点都到期了,但是 A 元素到期后保存时间为2分钟,B 元素到期后保存时间为1分钟,那么优先级最高的肯定是 A 元素了(本质来说,这个 order 是通过小顶堆维护的,所以获取延迟到期时间最长元素的时间复杂度为 O(lgN))。
写了一个例子,但是因为输出有点问题,就看了一下 DelayQueue 的源码,发现里面的实现是委托给 PriorityQueue 的,于是写了篇文章跟了下 PriorityQueue 的基本操作( 也是 DelayQueue 的基本操作),结合文档和源码和我给的例子,应该就非常 easy 了:PriorityQueue 源码剖析
4. PriorityBlockingQueue
- BlockingQueue:线程安全
- ArrayBlockingQueue:
- LinkedBlockingQueue:
- Queue:非线程安全
哈哈,前面刚看完 PriorityQueue 的源码,这里就遇到了 PriorityBlockingQueue,其实 PriorityBlockingQueue就是用 PriorityQueue 实现的 BlockingQueue,所以没啥可说的。写了个例子低空掠过:
package concurrency;import java.util.Random;import java.util.concurrent.PriorityBlockingQueue;class Leader implements Comparable {private String name;private int degree;public Leader(String name, int degree) {this.name = name;this.degree = degree;}@Overridepublic int compareTo(Object o) {Leader leader = (Leader) o;return leader.degree - this.degree;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getDegree() {return degree;}public void setDegree(int degree) {this.degree = degree;}}public class WhoGoFirst {// 通过随机数给领导分级别private static PriorityBlockingQueue<Leader> leaders = new PriorityBlockingQueue<Leader>();public static void watchFilm(Leader leader) {leaders.add(leader);}public static void goFirst(PriorityBlockingQueue<Leader> leaders) {try {while (!leaders.isEmpty()) {Leader leader = leaders.take();System.out.println("级别: " + leader.getDegree() + "的 " + leader.getName() + " 正在撤离...");}} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {Random random = new Random();for (int i = 1; i <= 10; i++) {watchFilm(new Leader("leader " + i, random.nextInt(10)));}System.out.println("所有领导已经就坐,开始播放电影:速度与激情7...");System.out.println("着火了!!!");goFirst(leaders);}}/*output:所有领导已经就坐,开始播放电影:速度与激情7...着火了!!!级别: 8的 leader 3 正在撤离...级别: 7的 leader 8 正在撤离...级别: 6的 leader 4 正在撤离...级别: 6的 leader 9 正在撤离...级别: 6的 leader 2 正在撤离...级别: 5的 leader 5 正在撤离...级别: 4的 leader 6 正在撤离...级别: 4的 leader 7 正在撤离...级别: 2的 leader 10 正在撤离...级别: 0的 leader 1 正在撤离...*/
5. ScheduledExcutor
这个小节讲的是定时触发任务,知道 crontab 的应该都不陌生。看完以后我 google 了一下,发现几个类似功能的类,先知道有这几个东西,用到了再具体看文档吧。
- Timer:单线程轮询任务列表,效率较低
- ScheduledExcutor:并行执行
- JCrontab:借鉴了 crontab 的语法,其区别在于 command 不再是 unix/linux 的命令,而是一个 Java 类。如果该类带参数,例如com.ibm.scheduler.JCronTask2#run,则定期执行 run 方法;如果该类不带参数,则默认执行 main 方法。此外,还可以传参数给 main 方法或者构造函数,例如com.ibm.scheduler.JCronTask2#run Hello World表示传两个参数 Hello 和 World 给构造函数
- Quartz:Spring 就是用的这个执行定时任务的
给个随便搜到的资料:几种任务调度的 Java 实现方法与比较
对于举例子的 ScheduledThreadPoolExecutor,大概看下源码,本质是使用 DelayWorkQueue 实现的 BlockingQueue。其中 DelayWorkQueue 和 DelayQueue 类似,不过没有复用 DelayQueue 中用到的 PriorityQueue,而是自己捯饬了一个新的小(大)顶堆。看来concurrent 也不是100%完美的代码呀,哈哈哈。
public class ScheduledThreadPoolExecutorTest {public static void main(String[] args) {ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1);BusinessTask task = new BusinessTask();//1秒后开始执行任务,以后每隔2秒执行一次executorService.scheduleWithFixedDelay(task, 1000, 2000,TimeUnit.MILLISECONDS);}private static class BusinessTask implements Runnable{@Overridepublic void run() {System.out.println("任务开始...");// doBusiness();System.out.println("任务结束...");}}}
嗯,这个例子虽然简单,但是我想说几点:
- ScheduleAtFixedRate 是基于固定时间间隔进行任务调度,ScheduleWithFixedDelay 取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度:
scheduleWithFixedDelay()方法:每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay, initialDelay+executeTime+delay, initialDelay+2executeTime+2delayscheduleWithFixedRate()方法:每次执行时间为上一次任务开始起向后推一个时间间隔,即每次执行时间为 :initialDelay, initialDelay+period, initialDelay+2*period, …
- 有可能上面的程序执行了一段时间后,会发现不再执行了,去查看日志,可能是doBusiness()方法中抛出了异常。
但是为什么 抛出异常就会中止定时任务的执行呢?看文档就知道了:doBusiness()
Creates and executes a periodic action that becomes enabled first after the given initial delay, and subsequently with the given delay between the termination of one execution and the commencement of the next. If any execution of the task encounters an exception, subsequent executions are suppressed. Otherwise, the task will only terminate via cancellation or termination of the executor.
简单翻译就是:
创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务的任一执行遇到异常,就会取消后续执行。否则,只能通过执行程序的取消或终止方法来终止该任务。
所以上面的例子应该改成下面这样:
public class ScheduledThreadPoolExecutorTest {public static void main(String[] args) {ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1);BusinessTask task = new BusinessTask();//1秒后开始执行任务,以后每隔2秒执行一次executorService.scheduleWithFixedDelay(task, 1000, 2000,TimeUnit.MILLISECONDS);}private static class BusinessTask implements Runnable{@Overridepublic void run() {//捕获所有的异常,保证定时任务能够继续执行try{System.out.println("任务开始...");// doBusiness();System.out.println("任务结束...");}catch (Throwable e) {// solve the exception problem}}}}
6. Semaphore
Semaphore 是一个计数信号量,平常的锁(来自 concurrent.locks 或者内建的 synchronized)再任何时刻都只能允许一个任务访问一项资源,但是 Semaphore 允许 N 个任务同时访问这个资源。你还可以将信号量看做是在向外分发使用资源的“许可证”,尽管实际上没有使用任何许可证对象。
总结来说,一般的锁是保证一个资源只能被一个任务访问;Semaphore 是保证一堆资源可以同时有多个任务访问。举个例子,现在有一个厕所,5个坑位,如果使用 synchronized 的话,同步厕所就只能让1个人进入,浪费了4个坑位;稍微往前一步是使用 BlockingQueue(如果你用 synchronized 来同步5个坑位就很复杂多了),再往前一步,concurrent 提供了 Semaphore ,它通过 acquire()和 release()来保证资源的分发使用。
7. Exchanger
终于来到21.7小节的最后一个构件了!!!!
这个构件很简单,是为了让两个任务交换对象,当两个任务进入 Exchanger 提供的“栅栏”时,他们各自拥有一个对象,当它们离开时,都拥有了之前由对方拥有的对象。为什么要有这么个东西呢?考虑下面的场景:
一个任务在创建对象,这些对象的生产/销毁代价都非常高。上面 Semaphore 的例子还算靠谱,因为我用完了资源并没有销毁,直接还给资源池了,然后立马可以被复用。但是如果两个线程需要知晓对方的工作状态信息,就可以用 Exchanger 交换各自的工作状态。
参考来源
Java并发:ThreadLocal
前言
看到《Java 编程思想》的21.3小节,只用了一个小节介绍 ThreadLocal 的使用,但是不是很懂,于是找了点资料学习一下,下面是这篇文章的主线:
- 什么是 ThreadLocal,为什么要提出这个概念?——共享资源的访问问题
- synchronized——串行访问
- volatile——主内存刷新,不存在线程副本
- ThreadLocal——线程空间内的全局变量
- 文档 & 源码分析
- 探究一下为什么这么设计?
- 实际中如何正确使用?
- 有什么缺点?有危险吗?
下面就来分析分析吧,gogogo~~
一、什么是 ThreadLocal,为什么要提出这个概念?
多线程问题最让人抓狂的就是对共享资源的并发访问。目前为止,我见到的对共享资源的使用方法有:
synchronized——串行访问volatile——主内存刷新,不存在线程副本ThreadLocal——线程空间内的全局变量
synchronized 很简单,就是每个线程访问共享资源时,会检查对象头中的锁状态,然后进行串行访问共享资源;volatile 也很简单,它在使用中保证对变量的访问均在主内存进行,不存在对象副本,所以每个线程要使用的时候,都必须强制从主内存刷新,但是如果操作不具有原子性,也会导致共享资源的污染,所以 volatile 的使用场景要非常小心,在《深入理解 Java 虚拟机》中有详细的分析,这里就不细谈了;然后 ThreadLocal,其实 ThreadLocal 跟共享资源没关系,因为都是线程内部的,所以根本不存在共享这一说法,那它是干嘛的?下面会详细说明。
二、文档 & 源码分析
public class ThreadLocal extends Object,泛型
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)【注:一般为 private static 修饰,比如一个 userid 或者事务 id】. Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
文档说的很简单,其实源码也很简单的,核心部分就是 ThreadLocalMap。然后对这个 Map 的 init,get,set 操作。我们抓住关键部分看一下:
首先是 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);}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
在这个方法内部我们看到,首先通过 getMap(Thread t)方法获取和当前线程相关的 ThreadLocalMap,然后将变量的值设置到 ThreadLocalMap 对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。而 getMap()也很简单,就是获取和设置 Thread 类中的 threadLocals 变量,而这个变量的类型就是 ThreadLocalMap,这样进一步验证了上文中的观点:每个线程都有自己独立的 ThreadLocalMap 对象。
打开java.lang.Thread类的源代码,我们能得到更直观的证明:
/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;
然后再看一下ThreadLocal类中的get()方法:
//get 方法public T get() {Thread t = Thread.currentThread(); //原来 Thread 中有 ThreadLocalMap 属性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();}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}
这两个方法的代码告诉我们,在获取和当前线程绑定的值时,ThreadLocalMap 对象是以 this 指向的 ThreadLocal 对象为键进行查找的,这当然和前面set()方法的代码是呼应的。
三、探究一下为什么这么设计?
ThreadLocal ,从名字就可以翻译为线程的本地存储。这是一种对共享资源安全使用的方法,但是和 synchronized 有区别,它为每个线程都分配一个变量的内存空间,根除了线程对共享变量的竞争。但是因为每个线程,所以这个变量在不同线程之间是“透明的”、“无法感知的”,这就意味着各个线程的这个变量不能有联系,它只和当前的线程相关联。
用一个例子来说明吧:
import java.util.Random;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;class Task implements Runnable {@Overridepublic void run() {while (!Thread.currentThread().isInterrupted()) {ThreadLocalTest.increment();System.out.println(this);}}public String toString() {return "thread is: " + Thread.currentThread() + ", value is: " + ThreadLocalTest.get();}}public class ThreadLocalTest {private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {private Random random = new Random(47);protected synchronized Integer initialValue() {return random.nextInt(10000);}};public static void increment() {value.set(value.get() + 1);}public static int get() {return value.get();}public static void main(String[] args) throws InterruptedException {ExecutorService exec = Executors.newCachedThreadPool();for (int i = 0; i < 3; i++) {exec.execute(new Task());}TimeUnit.SECONDS.sleep(3);exec.shutdownNow();}}
输出我就不贴了,运行一下就会发现。每个线程都会维护一个 value,而且相互不会影响。
但是仔细观察这个程序也许会发现一个问题:
为什么 ThreadLocal 是 static 的?既然是跟线程有关的,那么为啥要声明为静态变量?静态的意思是所有对象公用一个,而 ThreadLocal 刚刚才说是线程本地,每个线程各自都有单独的一份。这是为什么呢?
首先自然是文档,然后源码,然后去 stackoverflow 搜索,比如Why should Java ThreadLocal variables be static。 我个人试着总结一下吧。
首先,TLS (Thread Local Storage)的概念来源于 C 语言,详细的可以参考这篇文章——漫谈兼容内核 。其实多线程的提出,最需要解决的是可视性问题,也就是前面提到的对共享资源的正确使用。对于有些共享资源,我希望所有线程可见,于是我在进程级别声明即可(也就是常见的全局变量),这样所有线程都可以任意访问,这时为了防止污染,就需要加锁进行串行访问;对于有些资源,我仅仅希望在本线程可见,不能让其他线程污染,这样很容易想到在线程内部声明局部变量,但是这么做有个头疼的问题是局部变量有作用域,如果我的线程需要横跨多个函数,那么我需要一层一层传递。举个极端的例子,如果有个线程 A 吧,A 启动时会调用 a(),然后 a()中调b(),b()中调 c(),……共有100层方法调用,且只有第1层和第100层用到了一个想在线程内部使用的局部变量,那么我就需要在100层函数的每一层都传递这个变量。这样做显然是下下之选,因为不仅容易出错,而且维护起来也非常容易让人困惑(我靠,这个函数传入的这个参数压根没用到,估计谁忘了吧,好,我来删了。然后……)。为了解决这种问题 TLS 应运而生。本质来说,TLS 可以理解为线程上下文变量。举个最简单的例子,假设我的程序会连续启动10个线程去读取10个文件的内容,线程代码是完全一样的。那么,我就可以使用 TLS 来记录线程上下文中的文件句柄(我在一个数组中定义了10个文件的句柄,然后用循环让一个线程领取一个句柄)。此时,我使用一致的文件操作代码,却能保证不管在任何线程中操作的文件句柄都是和该线程唯一绑定且其他线程不可见的。 简单来说,TLS 的作用可以归纳为两点:
- 存储线程上下文相关的信息,使得在一致的代码中,自动访问差异性的线程绑定的上下文相关信息(多态,有木有!)
- 解决了在全局(线程内部)使用“局部变量”的参数传递问题(其实也是得益于第一条)
notes(ningg):ThreadLocals为线程内部的全局变量? Re:是的
其实基于 C 语言,TLS 还可以理解的更简单:
- 全局对象——全局变量的作用域和生命周期都是全局的,这里的全局是指进程级别。也就是说,如果你将其设置为全局变量,就意味着你希望在多线程环境中仍然能并发访问。但有时候不希望多线程同时访问,因为可能会造成变量污染,于是引入了线程互斥访问,互斥的作用就是保证多线程串行访问共享资源。
- 局部变量——如果设计的时候希望将某个变量设计为线程级别的,那典型做法是将其设计为函数的局部变量。可是,我如果又希望在线程执行时,这个线程用到的任意函数里面都可以访问到它。为了满足这个需求,又可能会想到用全局变量,但是,它又会使得这种访问域上升到进程级别。其实,我只是想在线程局部环境中,全局访问该变量而已。此时 TLS 应运而生,做到了这种在线程局部环境(或者称呼为线程执行环境,线程上下文)下可以全局访问该变量的效果。
四、实际中如何正确使用?
在一个支持单进程,多线程的轻量级移动平台中,假设我实现了一个 APP Framework,每一个 APP 都单独运行在一个独立的线程中,APP 运行时关联了很多信息,比如打开的文件、引用的组件、启动的 Timer 等。我希望框架能实现自动垃圾回收,就是当应用退出时,即使该 APP 没有主动释放打开的文件句柄,没有主动 cancel Timer,没有主动释放组件的引用,框架也可以自动完成这些收尾工作,否则,必定会造成内存耗尽(内存泄露)。
假设 APP 退出时调用了框架的 ExitApp API,该 API 允许 APP 调用后关闭自己,也允许关闭别的 APP。 那么,假设该 API 触发了 APP 的退出,最终调用到框架的 App_CleanUp() 函数,那么 App_CleanUp() 函数除了完成 APP 本身实例的释放外,肯定也要将那些文件句柄啊、Timer 啊、组件等资源释放掉。怎么来做呢?如果理解了上面的 ThreadLocal 的概念, 很明显这里可以使用 TLS。具体方法如下:
- 在 Framework 的 API 中,当 APP 的线程启动时,new 一个 AppContext 的对象,然后将对象的指针以 TLS 的方式存储起来。而这个 AppContext 内部包含了文件句柄,timer引用,组件引用等等。
- 后续任何框架的文件操作/Timer操作/组件使用,都可以取当前线程的 TLS,然后拿出 AppContext,将更新的文件句柄、Timer、组件使用等更新在 AppContext 对象内部。
- 应用退出时获取 TLS,拿到 AppContext,取出 AppContext 中的文件句柄,组件引用,Timer引用等完成清理工作。
五、有什么缺点?有危险吗?
听说有内存泄露,但是感觉不会。因为 ThreadLocal 是和 Thread 绑定的,线程死亡的时候,作为线程的属性,是肯定会被清理的啊。怎么会造成内存泄露呢?(因为 ThreadLocal 是线程内部的嘛)看了一些资料是这样说的:
有的说有泄露,有的说没泄露。。。。其实没有仔细看
六、小结
ThreadLocal 几点:
- 作用:线程本地变量,线程之间资源隔离,解决线程并发时,资源共享的问题;
- 实现:
- 每个 Thread 都绑定了一个
ThreadLocalMap - ThreadLocal 的 set、get,都是针对 Thread 的
ThreadLocalMap进行的 ThreadLocalMap中,Entry[] table:- ThreadLocal 作为
key,定位到Entry - ThreadLocal 存储的
value作为 value - Entry 中,同时存储了
key和value - 数据存储时, Entry 数组,出现Hash,采取
避让(开放寻址)策略,而非数组拉链(开放链路)策略 Entry[]数组,初始长度为 16;大于 threshold 时,2 倍扩容。Entry[]数组中,对key是弱引用(WeakReference),ThreadLocal 变量被回收后,Entry 和 Value 并未被回收;ThreadLocalMap 只是用于存储的,供其他地方使用,但如果其他地方不再使用这个 threadLocal 对象了,由于其为弱引用,因此,其弱引用被自动置为 null;因此,Entry[] 可以回收其对应的 Entry 和 value;- 上述弱引用对应的 Entry,什么时候回收?get()、set() 会回收 Entry;
- 内存泄漏问题:如果 threadLocal 不再使用了,但一直未调用 get、set 方法,则,内存泄漏;当然,如果线程彻底销毁,对应 ThreadLocal 会被回收,但在此之前,内存泄露;
- 线程池问题:线程一直存活,下一次使用的时候,获取上一次使用时,设置的 threadLocal 变量,建议:使用之前先清理一次 threadLocal 变量;
- ThreadLocal 作为
- 每个 ThreadLocal 都用于存储一个变量,ThreadLocalMap 中,可以存储多个变量
- 每个 Thread 都绑定了一个
ThreadLocal 在内存中的存储关系:

关于强引用(Strong)、软引用(Soft)、弱引用(Weak)、虚引用(Phantom):
- 强引用:我们平时使用的引用,
Object obj = new Object();只要引用存在,GC 时,就不会回收对象; - 软引用:还有一些用,但非必需的对象,系统发生内存溢出之前,会回收软引用指向的对象;
- 弱引用:非必需的对象,每次 GC 时,都会回收弱引用指向的对象;
- 虚引用:不影响对象的生命周期,每次 GC 时,都会回收,虚引用用于在 GC 回收对象时,获取一个系统通知。
为什么Java ThreadLocal变量应该是静态的
通常,它包含一些类似于对象的东西,这些对象的范围限定为用户对话,Web请求等。您不希望它们也子限定为类的实例。
一个 Web 请求 =>一个持久性会话。
每个对象没有一个 Web 请求 = >一个持久性会话
Because if it were an instance level field, then it would actually be "Per Thread - Per Instance", not just a guaranteed "Per Thread." That isn't normally the semantic you're looking for.
Usually it's holding something like objects that are scoped to a User Conversation, Web Request, etc. You don't want them also sub-scoped to the instance of the class.
One web request => one Persistence session.
Not one web request => one persistence session per object.

浙公网安备 33010602011771号