08、随便聊聊ThreadLocal&ForkJoin

ThreadLocal

解决线程安全性问题。

ThreadLocal的使用

多线程环境下安全地使用日期格式化

public class ThreadLocalDemo {
    //非线程安全的
    private static final SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    //创建一个ThreadLocal变量,用于存储每个线程的DateFormat对象
    private static ThreadLocal<DateFormat> dateFormatThreadLocal=new ThreadLocal<>();

    //获取当前线程的DateFormat对象,如果当前线程还没有设置过,就创建一个新的SimpleDateFormat对象并设置到ThreadLocal中
    private static DateFormat getDateFormat(){
        DateFormat dateFormat=dateFormatThreadLocal.get(); //从当前线程的范围内获得一个DateFormat
        if(dateFormat==null){
            dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            //Thread.currentThread();
            dateFormatThreadLocal.set(dateFormat); //要在当前线程的范围内设置一个simpleDateFormat对象.
        }
        return dateFormat;
    }
    //解析字符串为日期对象的方法
    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService= Executors.newFixedThreadPool(10);
        //创建20个任务,每个任务都会调用parse方法解析日期字符串
        for (int i = 0; i < 20; i++) {
            executorService.execute(()->{
                try {
                    System.out.println(parse("2021-05-30 20:12:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
        //关闭线程池
        executorService.shutdown();
    }
}

ThreadLocal的原理

set():在当前线程范围内,设置一个值存储到ThreadLocal中,这个值仅对当前线程可见。
相当于在当前线程范围内建立了副本。
get():从当前线程范围内取出set方法设置的值.
remove():移除当前线程中存储的值
withInitial:java8中的初始化方法
ThreadLocal原理猜想

  • 能够实现线程的隔离,当前保存的数据,只会存储在当前线程范围内。-> 线程私有的
  • 存储结构. ->
  • key -> 当前线程

Thread Local 源码分析

    public void set(T value) {
        Thread t = Thread.currentThread();
// 如果当前线程已经初始化了map。
// 如果没有初始化,则进行初始化。
        ThreadLocalMap map = getMap(t);
        if (map != null) //修改value
            map.set(this, value);
        else //初始化
            createMap(t, value);
    }
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY]; //默认长度为16的数组
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //计算数组下标
        table[i] = new Entry(firstKey, firstValue); //把key/value存储到i的位置.
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
        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();
// i的位置已经存在了值, 就直接替换。
            if (k == key) {
                e.value = value;
                return;
            }
//如果key==null,则进行replaceStaleEntry(替换空余的数组)
            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数组中
  • 清理无效的key
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
        int slotToExpunge = staleSlot;
        for (int i = prevIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
        for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == key) {
                e.value = value;
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }
// If key not found, put new entry in stale slot
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
  • 如果当前值对应的entry数组中key为null,那么该方法会向前查找到还存在key失效的entry,进行
    清理。
  • 通过线性探索的方式,解决hash冲突的问题。

内存泄漏的问题

通过上面的分析,我们知道 expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现
get 和set 方法都可能触发清理方法 expungeStaleEntry() ,所以正常情况下是不会有内存溢出的 但
是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用
remove(),加快垃圾回收,避免内存溢出
退一步说,就算我们没有调用get 和set 和remove 方法,线程结束的时候,也就没有强引用再指向
ThreadLocal 中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会被回收掉,但是有一
种危险是,如果线程是线程池的, 在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候
ThreadLocalMap 和里面的元素是不会回收掉的。

Fork/Join

package com.gupaoedu.concurrent;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

public class ForkJoinExample {

    // 针对一个数字,做计算。
    private static final Integer MAX = 200;

    // 定义一个内部类继承RecursiveTask<Integer>,用于执行具体的计算任务
    static class CalcForJoinTask extends RecursiveTask<Integer> {
        private Integer startValue; // 子任务的开始计算的值
        private Integer endValue; // 子任务结束计算的值

        public CalcForJoinTask(Integer startValue, Integer endValue) {
            this.startValue = startValue;
            this.endValue = endValue;
        }

        @Override
        protected Integer compute() {
            // 如果当前的数据区间已经小于MAX了,那么接下来的计算不需要做拆分
            if (endValue - startValue < MAX) {
                System.out.println("开始计算:startValue:" + startValue + " ; endValue:" + endValue);
                Integer totalValue = 0;
                for (int i = this.startValue; i <= this.endValue; i++) {
                    totalValue += i;
                }
                return totalValue;
            }

            // 拆分任务,将区间一分为二,创建两个子任务
            CalcForJoinTask subTask = new CalcForJoinTask(startValue, (startValue + endValue) / 2);
            subTask.fork();
            CalcForJoinTask calcForJoinTask = new CalcForJoinTask((startValue + endValue) / 2 + 1, endValue);
            calcForJoinTask.fork();

            // 等待子任务完成并合并结果
            return subTask.join() + calcForJoinTask.join();
        }
    }

    public static void main(String[] args) {
        // 创建一个计算任务实例,指定计算的起始值和结束值
        CalcForJoinTask calcForJoinTask = new CalcForJoinTask(1, 10000);
        // 创建一个ForkJoinPool线程池
        ForkJoinPool pool = new ForkJoinPool();
        // 提交任务到线程池执行
        ForkJoinTask<Integer> taskFuture = pool.submit(calcForJoinTask);
        try {
            // 获取任务执行结果
            Integer result = taskFuture.get();
            System.out.println("result:" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

运行结果如下:
在这里插入图片描述

工作流程图

为了更清晰的了解fork/join的原理,我们通过一个图形来理解。整体思想其实就是拆分与合并。
图中最顶层的任务使用submit方式被提交到Fork/Join框架中,Fork/Join把这个任务放入到某个线程
中运行,工作任务中的compute方法的代码开始对这个任务T1进行分析。如果当前任务需要累加的数
字范围过大(代码中设定的是大于200),则将这个计算任务拆分成两个子任务(T1.1和T1.2),每个子任务各自负责计算一半的数据累加,请参见代码中的fork方法。如果当前子任务中需要累加的数字范围足够小(小于等于200),就进行累加然后返回到上层任务中。
在这里插入图片描述

Fork/Join API代码分析

简单给大家解释一下Fork/Join的相关api,在刚刚的案例中,涉及到几个重要的API, ForkJoinTask ,
ForkJoinPool .
ForkJoinTask : 基本任务,使用fork、join框架必须创建的对象,提供fork,join操作,常用的三个子类

  • RecursiveAction : 无结果返回的任务
  • RecursiveTask : 有返回结果的任务
  • CountedCompleter :无返回值任务,完成任务后可以触发回调。

ForkJoinTask提供了两个重要的方法:

  1. fork : 让task异步执行
  2. join : 让task同步执行,可以获取返回值

ForkJoinPool : 专门用来运行 ForkJoinTask 的线程池,(在实际使用中,也可以接收
Runnable/Callable 任务,但在真正运行时,也会把这些任务封装成 ForkJoinTask 类型的任务)
在这里插入图片描述
ForkJoinTask 在不显示使用 ForkJoinPool.execute/invoke/submit() 方法进行执行的情况下,也可
以使用自己的fork/invoke方法进行执行

ForkJoin实战

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值