第六天 Java中的线程缓存和线程池

我们知道线程池缓存一共有两个 ThreadLocal:不能跨线程的 InheritableThreadLocal:可以跨线程的
在弄明白线程缓存之前我们要先弄明白 Thread.currentThread()和this在线程Thread 的区别
Thread.currentThread():标识的是执行当前代码的线程
this:标识创建当前代码的线程

输出结果为

我们看到当我们new MyThrea的时候Thread.currentThread() 是main__true__1,不管是不管是 new MyThrea("a")还是new MyThrea("b")都一样,但是

this是不一样的,new MyThrea("a")的this是Thread-0__false__12而new MyThrea("b")的 this是Thread-2__false__14,当多线程运行起来的时候执行run的时候 ,我们发现 this和创建的时候 是一样的,但是Thread.currentThread()是不一样的 ,因为 多线程的时候 调用 run是另起线程执行的

 
ThreadLocal
然后我们再来看 ThreadLocal,我们先来看看 我们new一个 ThreadLocal都为了什么

什么都没有做 就是声明了一下 ,其他什么都没有

那么我们再来看看他的set做了什么 ,我们知道ThreadLocadl我们声明完 就可以使用了而且我们设置值 是set方法操作

我们看到 set的操作第一步 就是获取当前执行代码的线程,让后把线程传递给了getmap,那么我们再来看看 getmap是做什么的

很简单的一个操作就是返回主线程【当前执行代码线程】的threadlocals,那么这个threadlocals是一个什么那 ,我们来看看

我们可以看到 Thread线程里面有两个 ThreadLocalThreadLocalMap.ThreadLocalMap 这两个一个是跨线程的一个是不跨线程的,那么 ThreadLocalMap有是一个什么那

我们可以看到 ThreadLocalMap就是一个 Entry[]的数组,Entry的声明也很简单,是一个 WeakReference的,保存了ThreadLocal需要保存的值

我们继续看 set里面的方法

 

 

 

 

set里面的createMap也很简单 ,创建一个ThreadLocalMap 然后赋值给Thread的threadLocals,创建的时候 指定了Entry[]数组的大小,

然后用 ThreadLocal的hashcode做哈希拿到一个i做为要插入数据的下标,

操作步骤:

第一步:在new ThreadLocal的时候其实是不做任何操作的,只要在set的时候才会做Thread.threadLocals数据的绑定,注意数据是绑定到了当前执行代码的线上的空间
第二步:如果 当前Thread.threadLocals没有数据,那就创建一个 ThreadLocalMap然后然后插入到数组【ThreadLocalMap本质就是Entry数组】
第三步:如果Thread.threadLocals存在数据那就循看看 ThreadLocal的threadLocalHashCode和数组长度取模看看有没有数据
第四步:如果要插入的下标有数据,判断是不是我们要插入的ThreadLocal,如果是直接赋值,不是的话 就位移一位然后找到可以插入的空位子插入
第五步:如果插入的位置没有数据,那就直接插入 其中ThreadLocal的threadLocalHashCode是一个原子操作类
我们发现 ,ThreadLocal其实没有做什么 ,数据是在Thread.threadLocals 操作也是直接调用了 Thread.threadLocals 的操作,ThreadLocal类只是做了一下中转,那为什么还要还要有 ThreadLocal的存在那,主要原因是Thread.threadLocals 的调用级别是包级别的,我们的代码不能直接调用,所以要使用ThreadLocal中转

ExecutorService
线程池这个也是我们工作中最常用的一个 ,下面我们先来看看 线程池的继承关系

 

 Executor:这是一个接口只只定义了一个方法 void execute(Runnable command); 这个也是线程池执行的核心方法,不同的线程池会实现不同的execute

ExecutorService:这也是个接口继承Executor主要提供了一类型得到方法 submit,有多个
AbstractExecutorService:实现了ExecutorService这个接口并且提供了submit的实现,这是一个抽象类
ThreadPoolExecutor:实现了AbstractExecutorService这个抽象类,并且实现了execute方法,和worker
主要的继承我们说完了 ,现在我们看看 这个线程池到底是怎么跑的

这是我们一个简单的线程池使用,现在我们先来看看 创建线程池是怎么做的,我们最终看到是这样的

上面其他的都很容易理解,但是线程工厂这个 是什么那 ,我们通过源码可以看到

我们发现 这个threadfactory也很简单,其实就是做了一些线程池里面线程统一的东西 ,比如线程组 ,线程池里面线程的名称格式。。 然后包含了一个

newThread的方法来生成线程,这个方法生成的线程是在同一个组里面而且名称也是有规律的,因为前面已经定义过了
好了现在我们弄明白了线程的创建了,下面我们看一下线程的提交 submit

提交也很简单因为submit是被我们AbstractExecutorService实现了,所以只要是继承了 AbstractExecutorService 那么提交AbstractExecutorService就是一样的,

我们来看看 第一行代码RunnableFuture<T> ftask = newTaskFor(task);

我们发现 这个就是把 我们提交的任务转化为 FutureTask 这个是我们上一篇里面看到过的 ,有返回值执行线程的时候任务转化

第二行代码 execute(ftask);吧我们的FutureTask 传递给 execute,但是这个 execute是没有被 AbstractExecutorService实现,留给了继承AbstractExecutorService的类来实现,所以就有了千奇百怪的执行,我们看一下 一个比较简单的ThreadPoolExecutor的execute实现

 

这个应该是线程池里面最简单的一个execute实现了,步骤也很明了

第一步:判断线程是否小于核心线程如果小于直接添加为核心线程
第二步:如果核心线程满了,就放到队列里面,
第三步:如果队列满了就放到非核心线程里面
第四步:再次尝试放到非核心线程里面如果还是不行 就拒绝任务

addWorker
 我们再来看看 添加worker的的操作

我们主要看他下面的那一部分

 

这个是addworker的操作,也比较清晰,操作步骤是

第一步:创建一个worker【根据我们传递过来的任务】
第二步:判断线程池状态,只有状态小于等于1的才能加入
第三步:判断我们提交的任务是否有已经执行,如果执行就放弃
第四步:吧任务加入到workers队列里面
第五步:调用线程Thread的start来申请线程执行
我们通过代码发现,t.start();其实是来自worker,那么我们再来看看 worker的操作

 

 worker主要作用是,

第一步:因为他实现了Runnable所以它自己要实现run方法,这个是申请异步线程调用的方法
第二步:把我们的任务赋值给firstTask这样是一个线程
第三步:通过我们new 线程池的时候创建的threadfactory来创建出来一个线程吧自身传递过去,这样在调用 start
的时候就会调用自身的run方法,【addworker的时候添加成功以后那个 t.start】

runworker操作步骤是

第一步:拿到我们的核心任务,然后把这个线程的核心任务清空
第二步:判断任务是否为空,如果为空就去队列里面取一个任务【因为线程池里面线程是重复利用的,所以我们执行的这个线程可能是之前执行了其他任务的线程,在上一次执行完他的核心任务就会被清空,第一步的操作】队列里面的任务我们addworker的时候如果核心任务满了不是有吧任务放到队列吗 ,这里就是执行那些任务了
第三步:那到任务执行run,我们传递的是FutureTask任务,这个任务继承了Runnable所以有run,而且FutereTask前面我们说过,会有返回值,执行原来我们也说过了
第四步:清空任务,解锁【我们拿到任务开始执行的时候 这个任务是加锁操作的w.lock()】
到这里我们的线程池执行就全部串联起来了
借用一张网络图 来说明一下addworker的操作

 

 

拒绝策略
线程池有四种拒绝策略:
AbortPolicy:抛出异常,默认
CallerRunsPolicy:不使用线程池执行
DiscardPolicy:直接丢弃任务
DiscardOldestPolicy:丢弃队列中最旧的任务 对 于 线 程 池 选 择 的 拒 绝 策 略 可 以 通 过 RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();来设置。 
 
 
定时线程池其实核心和这个也一样,就是在提交任务上面做了扩展
固定频率线程池和非固定频率线程池的主要区别

就是初始化的时候不一样

如果我们传递的是固定频率那就是p大于零,就是线程完时间+我们设置的时间,如果不是固定频率,就是执行时间直接当前时间计算的,所以时间不固定

 

 

自己画了一个线程池和线程的完整运行图,图太大了看不清,想看的同学打开下面链接吧
 
 
 
 
 

posted @ 2022-03-22 18:51  瀚海行舟  阅读(324)  评论(0)    收藏  举报