在 Java 中,假如要问哪一个类应用简易,但用好最不容易?估计你的脑子里一定会闪过出一次词——“ThreadLocal”。
的确这般,ThreadLocal 本来设计方案是为了更好地处理高并发时,进程共享资源自变量的难题,但因为过多设计方案,如弱引用和哈希碰撞,进而造成它的了解难度系数金刚级应用成本费高难题。自然,假如稍不留神或是造成脏数据、内存溢出、共享资源自变量升级等难题,但即使如此,ThreadLocal 依然有合适自身的应用情景,及其无可替代的使用价值,例如文中要详细介绍了这二种应用情景,除开 ThreadLocal 以外,还真沒有适合的取代计划方案。
大家以线程同步恢复出厂设置時间为例子,来演试 ThreadLocal 的使用价值和功效,在我们在好几个进程中恢复出厂设置時间时,一般 会那样实际操作。
① 两个进程恢复出厂设置
当有 2 个进程开展时间格式化时,我们可以那样写:
- import java.text.SimpleDateFormat;
- import java.util.Date;
- public class Test {
- public static void main(String[] args) throws InterruptedException {
- // 建立并运行进程1
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- // 获得時间目标
- Date date = new Date(1 * 1000);
- // 实行时间格式化
- formatAndPrint(date);
- }
- });
- t1.start();
- // 建立并运行进程2
- Thread t2 = new Thread(new Runnable() {
- @Override
- public void run() {
- // 获得時间目标
- Date date = new Date(2 * 1000);
- // 实行时间格式化
- formatAndPrint(date);
- }
- });
- t2.start();
- }
- /**
- * 恢复出厂设置并打印結果
- * @param date 時间目标
- */
- private static void formatAndPrint(Date date) {
- // 恢复出厂设置時间目标
- SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
- // 实行恢复出厂设置
- String result = simpleDateFormat.format(date);
- // 打印出最后結果
- System.out.println("時间:" result);
- }
- }
之上程序流程的实行結果为:
上边的编码由于建立的进程总数并不是很多,因此我们可以给每一个进程建立一个独享目标 SimpleDateFormat 来开展时间格式化。
② 10个进程恢复出厂设置
当进程的总数从 2 个升級为 10 个时,我们可以应用 for 循环系统来建立好几个进程实行时间格式化,实际完成编码以下:
- import java.text.SimpleDateFormat;
- import java.util.Date;
- public class Test {
- public static void main(String[] args) throws InterruptedException {
- for (int i = 0; i < 10; i ) {
- int finalI = i;
- // 建立进程
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- // 获得時间目标
- Date date = new Date(finalI * 1000);
- // 实行时间格式化
- formatAndPrint(date);
- }
- });
- // 运行进程
- thread.start();
- }
- }
- /**
- * 恢复出厂设置并打印時间
- * @param date 時间目标
- */
- private static void formatAndPrint(Date date) {
- // 恢复出厂设置時间目标
- SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
- // 实行恢复出厂设置
- String result = simpleDateFormat.format(date);
- // 打印出最后結果
- System.out.println("時间:" result);
- }
- }
之上程序流程的实行結果为:
从以上結果能够看得出,尽管这时建立的线程数和 SimpleDateFormat 的总数算不上少,但程序流程或是能够一切正常运作的。
③ 1000个进程恢复出厂设置
殊不知在我们将进程的总数从 10 个变为 1000 个的情况下,大家就不可以单纯性的应用 for 循环系统来建立 1000 个进程的方法来解决困难了,由于那样经常的新创建和消毁进程会导致很多的系统软件花销和进程过多争夺 CPU 資源的难题。
因此历经一番思索后,我们决定应用线程池来实行这 1000 次的每日任务,由于线程池能够多路复用进程資源,不用经常的新创建和消毁进程,还可以根据操纵线程池中进程的总数来防止过线程同步所造成的 CPU 資源过多争夺和进程经常转换所导致的特性难题,并且我们可以将 SimpleDateFormat 提高为全局变量,进而防止每一次实行都需要新创建 SimpleDateFormat 的难题,因此大家写出了那样的编码:
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.concurrent.LinkedBlockingQueue;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
- public class App {
- // 时间格式化目标
- private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
- public static void main(String[] args) throws InterruptedException {
- // 建立线程池执行任务
- ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
- TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
- for (int i = 0; i < 1000; i ) {
- int finalI = i;
- // 执行任务
- threadPool.execute(new Runnable() {
- @Override
- public void run() {
- // 获得時间目标
- Date date = new Date(finalI * 1000);
- // 实行时间格式化
- formatAndPrint(date);
- }
- });
- }
- // 线程池实行完每日任务以后关掉
- threadPool.shutdown();
- }
- /**
- * 恢复出厂设置并打印時间
- * @param date 時间目标
- */
- private static void formatAndPrint(Date date) {
- // 实行恢复出厂设置
- String result = simpleDateFormat.format(date);
- // 打印出最后結果
- System.out.println("時间:" result);
- }
- }
之上程序流程的实行結果为:
在我们满怀极其愉悦的情绪去运作程序流程的情况下,却发觉出现意外发生了,那样敲代码居然会发生线程安全的难题。从以上結果能够看得出,程序流程的打印出結果居然有反复內容的,恰当的状况应该是沒有反复的時间才对。
PS:说白了的线程安全难题就是指:在线程同步的实行中,程序流程的实行結果与预期成果不相符合的状况。
a) 线程安全问题分析
为了更好地寻找存在的问题,大家试着查询 SimpleDateFormat 中 format 方式的源代码来清查一下难题,format 源代码以下:
- private StringBuffer format(Date date, StringBuffer toAppendTo,
- FieldDelegate delegate) {
- // 留意此番编码
- calendar.setTime(date);
- boolean useDateFormatSymbols = useDateFormatSymbols();
- for (int i = 0; i < compiledPattern.length; ) {
- int tag = compiledPattern[i] >>> 8;
- int count = compiledPattern[i ] & 0xff;
- if (count == 255) {
- count = compiledPattern[i ] << 16;
- count |= compiledPattern[i ];
- }
- switch (tag) {
- case TAG_QUOTE_ASCII_CHAR:
- toAppendTo.append((char)count);
- break;
- case TAG_QUOTE_CHARS:
- toAppendTo.append(compiledPattern, i, count);
- i = count;
- break;
- default:
- subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
- break;
- }
- }
- return toAppendTo;
- }
从以上源代码能够看得出,在实行 SimpleDateFormat.format 方式时,会应用 calendar.setTime 方式将键入的時间开展变换,那麼大家想一想一下那样的情景:
一切正常的状况下,程序流程的实行是那样的:
非线程安全的实行步骤是那样的:
b) 处理线程安全难题:上锁
当发生线程安全难题时,大家想起的第一解决方法便是上锁,实际的完成编码以下:
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.concurrent.LinkedBlockingQueue;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
- public class App {
- // 时间格式化目标
- private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
- public static void main(String[] args) throws InterruptedException {
- // 建立线程池执行任务
- ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
- TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
- for (int i = 0; i < 1000; i ) {
- int finalI = i;
- // 执行任务
- threadPool.execute(new Runnable() {
- @Override
- public void run() {
- // 获得時间目标
- Date date = new Date(finalI * 1000);
- // 实行时间格式化
- formatAndPrint(date);
- }
- });
- }
- // 线程池实行完每日任务以后关掉
- threadPool.shutdown();
- }
- /**
- * 恢复出厂设置并打印時间
- * @param date 時间目标
- */
- private static void formatAndPrint(Date date) {
- // 实行恢复出厂设置
- String result = null;
- // 上锁
- synchronized (App.class) {
- result = simpleDateFormat.format(date);
- }
- // 打印出最后結果
- System.out.println("時间:" result);
- }
- }
之上程序流程的实行結果为:
从以上結果能够看得出,应用了 synchronized 上锁以后程序流程就可以一切正常的实行了。
上锁的缺陷
上锁的方法尽管能够处理线程安全的难题,但另外也产生了新的难题,当程序流程上锁以后,全部的进程务必排长队实行一些业务流程才行,那样无形之中就减少了程序流程的运作高效率了。
是否有既能处理线程安全难题,又能提升 程序流程的实行速率的解决方法呢?
回答是:有的,这个时候 ThreadLocal就需要出场了。
c) 处理线程安全难题:ThreadLocal
1.ThreadLocal 详细介绍
ThreadLocal 从字面上的含意来理解是进程当地自变量的含意,换句话说它是进程中的独享自变量,每一个进程只有应用自身的自变量。
以上边线程池恢复出厂设置時间为例子,当线程池中有 10 个进程时,SimpleDateFormat 会存进 ThreadLocal 中,它也总是建立 10 个目标,即便 要实行 1000 次时间格式化每日任务,仍然总是新创建 10 个 SimpleDateFormat 目标,每一个进程启用自身的 ThreadLocal 自变量。
2.ThreadLocal 基本应用
ThreadLocal 常见的关键方式有三个:
ThreadLocal 全部方式如下图所显示:
官方网表明文本文档:https://docs.oracle.com/javase/8/docs/api/
ThreadLocal 基本使用方法以下:
- /**
- * @微信公众号:Java汉语社群营销
- */
- public class ThreadLocalExample {
- // 建立一个 ThreadLocal 目标
- private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
- public static void main(String[] args) {
- // 进程执行任务
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- String threadName = Thread.currentThread().getName();
- System.out.println(threadName " 存进值:" threadName);
- // 在 ThreadLocal 中设定值
- threadLocal.set(threadName);
- // 实行方式,打印出进程中设定的值
- print(threadName);
- }
- };
- // 建立并运行进程 1
- new Thread(runnable, "MyThread-1").start();
- // 建立并运行进程 2
- new Thread(runnable, "MyThread-2").start();
- }
- /**
- * 打印出进程中的 ThreadLocal 值
- * @param threadName 进程名字
- */
- private static void print(String threadName) {
- try {
- // 获得 ThreadLocal 中的值
- String result = threadLocal.get();
- // 打印出結果
- System.out.println(threadName " 取下值:" result);
- } finally {
- // 清除 ThreadLocal 中的值(避免 内存溢出)
- threadLocal.remove();
- }
- }
- }
之上程序流程的实行結果为:
从以上結果能够看得出,每一个进程总是载入到归属于自身的 ThreadLocal 值。
3.ThreadLocal 高級使用方法
① 复位:initialValue
- public class ThreadLocalByInitExample {
- // 界定 ThreadLocal
- private static ThreadLocal<String> threadLocal = new ThreadLocal(){
- @Override
- protected String initialValue() {
- System.out.println("实行 initialValue() 方式");
- return "初始值";
- }
- };
- public static void main(String[] args) {
- // 进程执行任务
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- // 实行方式,打印出进程中数据(未设定值打印出)
- print(threadName);
- }
- };
- // 建立并运行进程 1
- new Thread(runnable, "MyThread-1").start();
- // 建立并运行进程 2
- new Thread(runnable, "MyThread-2").start();
- }
- /**
- * 打印出进程中的 ThreadLocal 值
- * @param threadName 进程名字
- */
- private static void print(String threadName) {
- // 获得 ThreadLocal 中的值
- String result = threadLocal.get();
- // 打印出結果
- System.out.println(threadName " 获得值:" result);
- }
- }
之上程序流程的实行結果为:
当应用了 #threadLocal.set 方式以后,initialValue 方式就不容易强制执行了,以下编码所显示:
- public class ThreadLocalByInitExample {
- // 界定 ThreadLocal
- private static ThreadLocal<String> threadLocal = new ThreadLocal() {
- @Override
- protected String initialValue() {
- System.out.println("实行 initialValue() 方式");
- return "初始值";
- }
- };
- public static void main(String[] args) {
- // 进程执行任务
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- String threadName = Thread.currentThread().getName();
- System.out.println(threadName " 存进值:" threadName);
- // 在 ThreadLocal 中设定值
- threadLocal.set(threadName);
- // 实行方式,打印出进程中设定的值
- print(threadName);
- }
- };
- // 建立并运行进程 1
- new Thread(runnable, "MyThread-1").start();
- // 建立并运行进程 2
- new Thread(runnable, "MyThread-2").start();
- }
- /**
- * 打印出进程中的 ThreadLocal 值
- * @param threadName 进程名字
- */
- private static void print(String threadName) {
- try {
- // 获得 ThreadLocal 中的值
- String result = threadLocal.get();
- // 打印出結果
- System.out.println(threadName "取下值:" result);
- } finally {
- // 清除 ThreadLocal 中的值(避免 内存溢出)
- threadLocal.remove();
- }
- }
- }
之上程序流程的实行結果为:
为何 set 以后,复位编码也不实行了?
要了解这个问题,必须从 ThreadLocal.get() 方式的源代码中获得回答,由于复位方式 initialValue 在 ThreadLocal 建立时并不会立即执行,只是在启用了 get 方式总是才会实行,测试程序以下:
- import java.util.Date;
- public class ThreadLocalByInitExample {
- // 界定 ThreadLocal
- private static ThreadLocal<String> threadLocal = new ThreadLocal() {
- @Override
- protected String initialValue() {
- System.out.println("实行 initialValue() 方式 " new Date());
- return "初始值";
- }
- };
- public static void main(String[] args) {
- // 进程执行任务
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- // 获得当今进程名字
- String threadName = Thread.currentThread().getName();
- // 实行方式,打印出进程中设定的值
- print(threadName);
- }
- };
- // 建立并运行进程 1
- new Thread(runnable, "MyThread-1").start();
- // 建立并运行进程 2
- new Thread(runnable, "MyThread-2").start();
- }
- /**
- * 打印出进程中的 ThreadLocal 值
- * @param threadName 进程名字
- */
- private static void print(String threadName) {
- System.out.println("进到 print() 方式 " new Date());
- try {
- // 休眠状态 1s
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 获得 ThreadLocal 中的值
- String result = threadLocal.get();
- // 打印出結果
- System.out.println(String.format("%s 获得值:%s %s",
- threadName, result, new Date()));
- }
- }
之上程序流程的实行結果为:
从以上打印出的時间能够看得出:initialValue 方式并并不是在 ThreadLocal 建立时实行的,只是在启用 Thread.get 方式时才实行的。
下面看来 Threadlocal.get 源代码的完成:
- public T get() {
- // 获得当今的进程
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- // 分辨 ThreadLocal 中是不是有数据信息
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value;
- // 有 set 值,立即回到数据信息
- 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);
- else
- createMap(t, value);
- return value;
- }
从以上源代码能够看得出,当 ThreadLocal 中有值的时候会立即传参 e.value,仅有 Threadlocal 中沒有一切值时才会实行复位方式 initialValue。
常见问题—种类务必保持一致
留意在应用 initialValue 时,传参的种类要和 ThreadLoca 界定的基本数据类型保持一致,如下图所显示:
假如数据信息不一致便会导致 ClassCaseException 数据转换出现异常,如下图所显示:
② 复位2:withInitial
- import java.util.function.Supplier;
- public class ThreadLocalByInitExample {
- // 界定 ThreadLocal
- private static ThreadLocal<String> threadLocal =
- ThreadLocal.withInitial(new Supplier<String>() {
- @Override
- public String get() {
- System.out.println("实行 withInitial() 方式");
- return "初始值";
- }
- });
- public static void main(String[] args) {
- // 进程执行任务
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- String threadName = Thread.currentThread().getName();
- // 实行方式,打印出进程中设定的值
- print(threadName);
- }
- };
- // 建立并运行进程 1
- new Thread(runnable, "MyThread-1").start();
- // 建立并运行进程 2
- new Thread(runnable, "MyThread-2").start();
- }
- /**
- * 打印出进程中的 ThreadLocal 值
- * @param threadName 进程名字
- */
- private static void print(String threadName) {
- // 获得 ThreadLocal 中的值
- String result = threadLocal.get();
- // 打印出結果
- System.out.println(threadName " 获得值:" result);
- }
- }
之上程序流程的实行結果为:
根据以上的编码发觉,withInitial 方式的应用好和 initialValue 仿佛没啥差别,那为什么也要造出2个相近的方式呢?客官莫心急,再次往下看。
③ 更简约的 withInitial 应用
withInitial 方式的优点取决于能够更简易的完成自变量复位,以下编码所显示:
- public class ThreadLocalByInitExample {
- // 界定 ThreadLocal
- private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "初始值");
- public static void main(String[] args) {
- // 进程执行任务
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- String threadName = Thread.currentThread().getName();
- // 实行方式,打印出进程中设定的值
- print(threadName);
- }
- };
- // 建立并运行进程 1
- new Thread(runnable, "MyThread-1").start();
- // 建立并运行进程 2
- new Thread(runnable, "MyThread-2").start();
- }
- /**
- * 打印出进程中的 ThreadLocal 值
- * @param threadName 进程名字
- */
- private static void print(String threadName) {
- // 获得 ThreadLocal 中的值
- String result = threadLocal.get();
- // 打印出結果
- System.out.println(threadName " 获得值:" result);
- }
- }
之上程序流程的实行結果为:
了解了 ThreadLocal 的应用以后,大家返回文中的主题风格,下面大家将应用 ThreadLocal 来完成 1000 个時间的恢复出厂设置,实际完成编码以下:
- import java.text.SimpleDateFormat;
- import java.util.Date;
- import java.util.concurrent.LinkedBlockingQueue;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
- public class MyThreadLocalByDateFormat {
- // 建立 ThreadLocal 并设定初始值
- private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
- ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
- public static void main(String[] args) {
- // 建立线程池执行任务
- ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
- TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
- // 执行任务
- for (int i = 0; i < 1000; i ) {
- int finalI = i;
- // 执行任务
- threadPool.execute(new Runnable() {
- @Override
- public void run() {
- // 获得時间目标
- Date date = new Date(finalI * 1000);
- // 实行时间格式化
- formatAndPrint(date);
- }
- });
- }
- // 线程池实行完每日任务以后关掉
- threadPool.shutdown();
- // 线程池实行完每日任务以后关掉
- threadPool.shutdown();
- }
- /**
- * 恢复出厂设置并打印時间
- * @param date 時间目标
- */
- private static void formatAndPrint(Date date) {
- // 实行恢复出厂设置
- String result = dateFormatThreadLocal.get().format(date);
- // 打印出最后結果
- System.out.println("時间:" result);
- }
- }
之上程序流程的实行結果为:
从以上結果能够看得出,应用 ThreadLocal 还可以处理进程高并发难题,而且防止了编码上锁排长队实行的难题。
除开上边的应用情景以外,大家还能够应用 ThreadLocal 来完成进程中跨类、跨方式的数据信息传送。例如登陆客户的 User 目标信息内容,大家必须在不一样的分系统中数次应用,假如应用传统式的方法,大家必须操作方法传参和传参的方法来传送 User 目标,殊不知那样就无形之中导致了类和类中间,乃至是系统软件和系统软件中间的互相藕合了,因此这时我们可以应用 ThreadLocal 来完成 User 目标的传送。
明确了计划方案以后,下面大家来完成实际的业务流程编码。我们可以先在主线任务程中结构并复位一个 User 目标,并将此 User 阿里云oss在 ThreadLocal 中,储存进行以后,大家就可以在同一个进程的别的类中,如仓储物流类或订单信息类中立即获得并应用 User 目标了,实际完成编码以下。
主线任务程中的业务流程编码:
- public class ThreadLocalByUser {
- public static void main(String[] args) {
- // 复位客户信息
- User user = new User("Java");
- // 将 User 阿里云oss在 ThreadLocal 中
- UserStorage.setUser(user);
- // 启用订单管理系统
- OrderSystem orderSystem = new OrderSystem();
- // 加上订单信息(方式内获得客户信息)
- orderSystem.add();
- // 启用仓储管理系统
- RepertorySystem repertory = new RepertorySystem();
- // 减库存量(方式内获得客户信息)
- repertory.decrement();
- }
- }
User dao层:
- /**
- * 客户dao层
- */
- class User {
- public User(String name) {
- this.name = name;
- }
- private String name;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
ThreadLocal 实际操作类:
- /**
- * 客户信息储存类
- */
- class UserStorage {
- // 客户信息
- public static ThreadLocal<User> USER = new ThreadLocal();
- /**
- * 加密存储信息内容
- * @param user 客户数据信息
- */
- public static void setUser(User user) {
- USER.set(user);
- }
- }
* 订单信息类
- /**
- * 订单信息类
- */
- class OrderSystem {
- /**
- * 订单信息加上方式
- */
- public void add() {
- // 获得客户信息
- User user = UserStorage.USER.get();
- // 业务流程解决编码(忽视)...
- System.out.println(String.format("订单管理系统接到客户:%s 的要求。",
- user.getName()));
- }
- }
仓储物流类:
- /**
- * 仓储物流类
- */
- class RepertorySystem {
- /**
- * 减库存量方式
- */
- public void decrement() {
- // 获得客户信息
- User user = UserStorage.USER.get();
- // 业务流程解决编码(忽视)...
- System.out.println(String.format("仓储管理系统接到客户:%s 的要求。",
- user.getName()));
- }
- }
之上程序流程的最后实行結果:
从以上結果能够看得出,在我们在主线任务程中先复位了 User 目标以后,订单信息类和仓储物流类不用开展一切的参数传递还可以一切正常得到 User 目标了,进而完成了一个进程中,跨类和跨方式的数据信息传送。
应用 ThreadLocal 能够建立进程独享自变量,因此不容易造成线程安全难题,另外应用 ThreadLocal 还能够防止由于引进锁而导致进程排长队实行所产生的特性耗费;其次应用 ThreadLocal 还能够完成一个进程内跨类、跨方式的数据信息传送。
参照 & 鸣谢
《码出高效:Java开发手册》
《Java 并发编程 78 讲》