天天看点

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

作者:添甄
满怀忧思,不如先干再说!做干净纯粹的技术分享!欢迎评论区或私信交流!

[酷][酷][酷]ThreadLocal在工作和面试中都非常常见,本篇文章收录于《Java并发编程》合集,通过2-3篇文章介绍清楚ThreadLocal的应用,原理,案例,达到实战会用,面试会说的效果。通过本篇你可以得到[奸笑]:

  • ThreadLocal的正确解释和作用
  • ThreadLocal的API以及三种初始化方式的对比
  • 通过ThreadLocal实现春节红包案例
  • 通过线程池引出ThreadLocal隐患和解决方案
  • 总结ThreadLocal特点和应用场景

喜欢的话记得动动手指点关注,升职加薪不迷路,长期稳定输出干货技术文章

ThreadLocal

对于ThreadLocal的翻译有很多,如:线程本地,本地线程,如果根据他的作用来说翻译成【线程局部变量】我认为是比较合适的

ThreadLocal作用

ThreadLocal是java.lang包中的一个泛型类,可以实现为线程创建独有变量,这个变量对于其他线程是隔离的,也就是线程本地的值,这也是ThreadLocal名字的来源

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

每个使用该变量的线程都要初始化一个完全独立的实例副本,不存在多线程间共享的问题

ThreadLocal方法

ThreadLocal用来存储当前线程的独有数据,相关API就是存值,取值,清空值的简单操作

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案
  • withInitial:创建一个ThreadLocal实例,并给定初始值【JDK8推出的新方法,一般都是用该方法初始化ThreadLocal】
  • get:返回当前线程ThreadLocal的值,如果没有设置值返回null
  • set:设置当前线程ThreadLocal的值
  • remove:删除当前线程ThreadLocal的值
  • initialValue:此方法默认返回null, 通过ThreadLocal构造方法初始化时一般重写此方法,来设置初始值,在JDK8之后通过withInitial方法初始化

初始化ThreadLocal

初始化ThreadLocal有三种方式:

  • 直接通过构造方法创建,此时初始值为null
  • 通过构造方法同时重写initialValue方法给定初始值
  • 通过JDK8的withInitial()静态方法创建,可以通过Lambda直接给初始值【推荐使用】

通过构造方法

@Test
public void test1() {
    // 通过构造方法,初始化ThreadLocal
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    // 未给定初始值,通过get方法获取值为null
    System.out.println(threadLocal.get());
}           

通过get方法获取结果为null

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

如果想要设置值,则需要通过set方法

@Test
public void test1() {
    // 初始化ThreadLocal
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    System.out.println("设置前===》" + threadLocal.get());
    // 设置初始值
    threadLocal.set(1);
    System.out.println("设置后===》" + threadLocal.get());
}           

此时获取的值就是1

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

重写initialValue方法

该方法的主要作用是返回当前线程ThreadLocal的初始值,但是在ThreadLocal中默认实现返回为null

这个值和具体的泛型类型有关,通常需要根据实际需求重写此方法,定义初始值

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案
@Test
public void test2() {
    // 初始化ThreadLocal,重写initialValue方法设置默认值
    ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            // 设置,初始值为1024
            return 1024;
        }
    };
    // 启动5个线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":threadLocal初始值--->" + threadLocal.get());
        },"线程:" + i).start();
    }
}           

启动5个线程每个线程的初始值都为1024

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

withInitial静态方法

上方两种方案对创建ThreadLocal时给定初始值都稍显繁琐,在JDK8中新增了withInitial静态方法接收Supplier供给型函数接口设置初始值

@Test
public void test3() {
    // 通过withInitial方法设置初始值
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
        return 100;
    });
    // 启动5个线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":threadLocal初始值--->" + threadLocal.get());
        },"线程:" + i).start();
    }
}           

启动5个线程之后初始值为100,强烈推荐此种方式来实现ThreadLocal的初始化

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

推荐通过withInitial静态方法实现ThreadLocal的初始化

如果线程内需要修改值则可以使用set方法,如果需要获取值则使用get方法

结合下图搞懂ThreadLocal数据存储:一个ThreadLocal实例,在每个线程中都有独自的初始化副本,接下来每个线程对ThradLocal的操作都在线程内,对其他线程隔离

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

​[吐舌]ThreadLocal的数据结构下一篇介绍,知道的小伙伴不妨写在评论区

父母保管孩子春节红包案例[白眼]

春节期间孩子收到的红包都会被父母暂时保管,答应长大后归还,比如,小翠有三个孩子小明,晓明和小茗,为了将来分账,需要单独记录孩子们收到的红包金额

@Test
public void test5() {
    // 通过withInitial方法设置初始红包为0
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    // 设置随机数
    Random random = new Random();
    // 通过map集合映射线程名字
    Map<Integer, String> map = new HashMap<>();
    map.put(0,"小明");
    map.put(1,"晓明");
    map.put(2,"小茗");
    // 启动3个线程,分别为3个孩子
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            // 去七大姑八大姨家拜年,比如5家亲戚吧
            for (int j = 0; j < 5; j++) {
                // 每家亲戚给随机的200以内的红包
                int yasuiqian = random.nextInt(200);
                // 红包金额加1
                threadLocal.set(yasuiqian + threadLocal.get());
            }
            System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元红包");
        },map.get(i)).start();
    }
}           

三个孩子去5个亲戚家,收到的红包金额实现独自记录

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

线程池实现保管红包

项目开发过程中对于多线程的场景都推荐使用线程池实现,可以避免线程频繁创建和销毁的资源浪费,使用线程池是一定要记得在finally代码块中关闭线程池

如下:开启一个有三个核心线程的线程池来处理3个孩子的拜年领红包任务

@Test
public void test6() {
    // 通过withInitial方法设置初始值
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    // 设置随机数
    Random random = new Random();

    Map<Integer, String> map = new HashMap<>();
    map.put(0,"小明");
    map.put(1,"晓明");
    map.put(2,"小茗");

    // 开启有3个核心线程的线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(3);
    try {
        // 3个孩子
        for (int i = 0; i < 3; i++) {
            // 记录当前循环值,映射对应的孩子名字
            int index = i;
            threadPool.submit(() -> {
                // 设置线程名:Thread.currentThread().getName()为默认的线程池给的名字,方便查看是哪一个线程执行的此任务
                Thread.currentThread().setName(Thread.currentThread().getName() + map.get(index));
                // 去七大姑八大姨家拜年,比如5家亲戚吧
                for (int j = 0; j < 5; j++) {
                    // 每家亲戚给随机的200以内的红包
                    int yasuiqian = random.nextInt(200);
                    // 红包金额加1
                    threadLocal.set(yasuiqian + threadLocal.get());
                }
                System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元红包");
            });
        }
    }catch (Exception e) {
        e.printStackTrace();
    }finally {
        // 关闭线程池
        threadPool.shutdown();
    }
}           

运行程序的电脑为8核,可以通过线程池中的3个核心线程,同时执行3条任务,此时的结果没有任何问题

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

线程池中线程可复用引发的问题

如果此时又多出来一个孩子,或者核心线程数变为2,即任务数大于核心线程数,会复用线程处理其他任务,即一个线程需要处理多个任务,这里减少核心线程数为例演示:

@Test
public void test6() {
    ......
    // 修改线程池核心线程数为2,其他不变
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    ......
}           

此时发现小明和小茗都是通过1号线程执行的任务,每个亲戚最多发200红包,5个亲戚,最大值应该为1000,但是小茗收到了1172元的红包,这肯定是不对的

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

原因在于:1号线程处理完小明之后,发现小茗任务没有执行,此时1号线程处理两个任务,ThreadLocal也还是同一个,即处理小茗任务时,ThreadLocal的值为小明任务处理后的值,并不是初始值0

在阿里Java开发规范中强制要求回收自定义的ThreadLocal变量

​​[机智]如果有需要《阿里开发规范手册》的评论区留言或者私信免费获取

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

通过try块将任务逻辑包裹,在finally中通过remove方法回收该任务执行后的ThreadLocal值

@Test
public void test7() {
    ......
    try {
        ......
            try {
                for (int j = 0; j < 5; j++) {
                    // 每家亲戚给随机的200以内的红包
                    int yasuiqian = random.nextInt(200);
                    // 红包金额加1
                    threadLocal.set(yasuiqian + threadLocal.get());
                }
                System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元红包");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 回收数据
                threadLocal.remove();
            }
    }catch (Exception e) {
        e.printStackTrace();
    }finally {
        // 关闭线程池
        threadPool.shutdown();
    }
}           

回收ThreadLocal变量后,复用该线程也不会对后续程序造成影响

关于并发编程线程间数据隔离和安全,ThreadLocal给出高质量答案

ThreadLocal特点

  • 统一设置初始值,每个线程可以通过set方法设置值,也可以通过get方法获取当前值
  • ThreadLocal被每一个线程单独持有副本,相互独立,只能在该线程内部使用
  • 如果配合线程池使用,线程可复用,需要调用remove方法回收数据,即重新设置为初始值,避免对后续程序造成影响和内存泄漏
  • ThreadLocal变量因为线程独立,所以不在线程安全问题

ThreadLocal应用场景

如上文所述,ThreadLocal 适用于如下两种场景

  • 每个线程需要自己独立的数据
  • 数据在线程内的共享,不需要在多线程之间共享

如:

  • 游戏玩家个人的属性,装备,积分等
  • Spring中也通过ThreadLocal解决线程安全问题,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的

​[666]本篇达成实战会用,下一篇通过原理介绍,实现面试会说!

继续阅读