0
点赞
收藏
分享

微信扫一扫

蹊源的Java笔记—线程与线程池


溪源的Java笔记—线程与线程池

前言

​Java​​​的进阶之路上不得不说的技术点就是——多线程,上期博客我们对​​JVM​​的知识进行了简单地整理,本期博客将针对线程与线程池后端知识点一一阐述。

JVM虚拟机 可参考我的博客:​​溪源的Java笔记—JVM​​线程并发与线程安全可参考我的博客:溪源的Java笔记—线程并发与线程安全

正文

线程

线程是​​CPU​​调度分派的基本单位。

线程的周期

  • 新建:当程序使用​​new​​​关键字创建了一个线程之后,该线程就处于新建状态,此时仅由​​JVM​​为其分配内存,并初始化其成员变量的值。
  • 就绪:当线程对象调用了​​start()​​​方法之后,该线程处于就绪状态。​​Java​​ 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
  • 运行:如果处于就绪状态的线程获得了​​CPU​​​,开始执行 ​​run()​​方法的线程执行体,则该线程处于运行状态。
  • 阻塞: 是指线程因为某种原因放弃了​​CPU​​​使用权,暂时停止运行。分为等待阻塞(​​wait​​​)、同步阻塞(​​lock​​​锁)、其他阻塞(​​sleep/join​​)三种情况。
  • 死亡: 结束后就是死亡状态,分正常死亡、异常结束、调用​​stop​​三种情况。

蹊源的Java笔记—线程与线程池_多线程

线程常见的方法:

  • void start()​:开启线程的方法
  • void run()
  • static void sleep(long t)​​: 释放​​CPU​​​的执行权,不释放锁 让优先级较低的线程有运行的机会, 是属于​​Thread​​类的方
  • final void wait()​​:释放​​CPU​​​的执行权,释放锁属于​​Object​​类
  • final void notify()​: 重新唤醒等待的线程。
  • static void yield()​:可以对当前线程进行临时暂停(让线程将资源释放出来 让同级的线程由运行的机会)
  • public final void join()​​:让线程加入执行,执行某一线程​​join​​方法的线程会被冻结,等待某一线程执行结束,该线程才会恢复到可运行状态
  • suspend()
  • resume()

终止线程的4种方式:

  • 正常运行结束: 程序运行结束,线程自动结束。
  • 使用退出标志退出线程:常有些线程是伺服线程。它们需要长时间的
    运行,只有在外部某些条件满足的情况下,才能关闭这些线程。这种结束类似自旋锁的实现。
  • Interrupt 方法结束线程a.当线程处于阻塞状态时:调用​​interrupt()​​​方法时,会抛​​InterruptException​​​异常。这个时候捕获异常,创造其实触发线程正常关闭的条件,就可关闭线程。实际上​​interrupt()​​不会直接关线程。 b.线程处于非阻塞状态时:使用 ​​isInterrupted()​​​判断线程的中断标志来退出循环。当使用​​interrupt()​​​方法时,中断标志就会置 ​​true​​。
  • stop 方法终止线程:这种方式不是线程安全的,在调用 ​​thread.stop()​​后导致了该线程所持有的所有锁的突然释放(不可控制)

线程中断

  • ​Java​​​的中断是一种协作机制。也就是说调用线程对象的​​interrupt​​方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时机中断自己。
  • 设置线程中断不影响线程的继续执行,但是线程设置中断后,线程内调用了​​wait​​​、​​jion​​​、​​sleep​​​方法中的一种, 立马抛出一个​​InterruptedException​​,且中断标志被清除,重新设置为false。

线程的实现方式:

  • 使用​​Thread​​​,覆盖​​run()​​方法
  • 实现​​Runnable​​​接口,重写​​run()​​方法
  • 实现​​Callable​​​接口,相对于​​Runnable​​​是有返回值,重写​​call()​​方法
  • 使用​​Executor​​框架来创建线程池

volatile

​Java​​​ 语言提供了一种稍弱的同步机制,即 ​​volatile​​ 变量,用来确保将变量的更新操作通知到其他线程,它具有两大特性:

  • 变量可见性:其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的 值对于其他线程是可以立即获取的。
  • 禁止重排序:​​volatile​​​ 禁止了指令重排,是​​sychronized​​​ 更轻量级的同步锁,但是​​volatile​​变量的操作如果不是原子操作,不能保证线程同步。

原子操作:指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束。

volatile变量可见性
造成多线程下内存不可见的原因:

  • ​Java​​​内存模型(​​JMM​​)规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

当这个变量被标记成​​volatile​​时,这个变量就会直接从主内存获取更新,而不是从自己工作空间,从而解决可解决多线程下内存不可见的问题。

volatile禁止重排序
​​​volatile​​可以通过插入内存屏障的方式,防止指令重排序。

As-If-Serial原则: 不管怎么进行指令重排序,单线程内程序的执行结果不能被改变。

Happens-Before原则: 用来指定两个操作之间的执行顺序,由于这个两个操作可以在一个线程之内,也可以在不同线程之间,通过这个规则可以保证跨线程的内存可见性。
具体规则如下:

  1. 程序次序规则:一个线程内,按照代码书写顺序,书写在前面的操作先发生于书写在后面的操作.
  2. 锁定规则:一个​​UNLOCK​​​操作先行发生于后面对同一个锁的​​UNLOCK​​操作.
  3. volatile变量规则:对​​volatile​​修饰的变量的写操作 先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:​​Thread​​​对象的​​start()​​方法先行发生于此线程的每个一个动作
  6. 线程中断规则:对线程​​interrupt()​​方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过​​Thread.join()​​​方法结束、​​Thread.isAlive()​​的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的​​finalize()​​方法的开始;

线程共享数据
​​​Java​​​ 里面进行多线程通信的主要方式就是共享内存的方式,核心做法就是将共享数据封装在​​Runnable​​对象中,具体实现分一下两种情况:

  • 多线程行为一致,共同操作一个数据源:使用同一个​​Runnable​​​对象(实现​​Runable​​接口)。共享数据作为这个类的成员变量,例如电影买票系统。
  • 多线程行为不一致,共同操作一个数据源:使用一个或多个​​Runnable​​​,使用​​set()​​​、​​get()​​​方法进行数据源在​​Runnable​​之间传递。例如为了保证安全,银行系统分步骤进行操作。

ThreadLocal
用来采用副本变量的方式来,实现每一个线程都有一个自己的变量(内部实现时​​​ThreadLocalMap​​)。这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或 者组件之间一些公共变量的传递的复杂度。

常见的应用:

  • 用来解决数据库连接,存放​​connection​​​对象,不同线程存放各自​​session​​;
  • 解决​​simpleDateFormat​​线程安全问题;
  • 在使用结束时,调用​​ThreadLocal.remove​​​来释放其​​value​​的引用,可以防止造成内存泄漏。

关于线程池

线程是处理器调度的基本单位。我们会为每一个请求都独立创建一个线程,而操作系统创建线程、切换线程状态、结束线程都要使用​​CPU​​进行调度。

Java当中主要有两类线程池:

  • Executor线程池:​​Executor​​​是个简单的接口,它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用​​Runnable​​​来表示任务。​​Executor​​基于"生产者-消费者"模式,提交任务的操作相当于生产者,执行任务的则相当于消费者。
  • ForkjoinPool线程池:它非常适合执行可以分解子任务的任务,比如树的遍历,归并排序,或者其他一些递归场景。

Executor线程池

Executor线程池的逻辑结构

蹊源的Java笔记—线程与线程池_溪源的Java笔记_02


创建线程池的参数

public ThreadPoolExecutor(
int corePoolSize, #核心线程数
int maxinmumPoolSize, #线程总数 非核心数=总数-核心数
long keepAliveTime, #当前线程数大于核心线程数时 线程的等待新任务的等待时间(核心线程也会面临死亡)
TimeUnit unit, #时间单位
BlockingQueue<Runnable> workQueue`在这里插入代码片` #任务队列
RejectedExecutionHandler #(选填) 拒绝处理器
)

ThreadPoolExecutor线程池处理线程的过程:

  1. 当前运行线程数 小于​​corePoolSize​​ 任务直接交给核心线程进行执行
  2. 当前运行线程数 大于或等于 ​​corePoolSize​​​ 任务且满足队列未满,那么
    任务将进入任务队列进行等待,并且任务队列都具有阻塞性,所以只有当核心线程数的任务执行完了,才会从任务队列中获取任务。
  3. 当前运行线程数 大于或等于 ​​corePoolSize​​ 任务且队列已满,那么 任务进入非核心线程。
  4. 当核心线程、等待队列、非核心线程都被占用的时候线程会被拒绝器处理。

阻塞特性:当队列满了,便会阻塞等待,直到有元素出队,后续的元素才可以被加入队列。

任务队列
线程池中的任务队列,实现​​​BlockingQueue​​​接口,即阻塞队列接口 ​​Queue​​本身也是先进先出的数据结构,它常见的有实现类有:

  • SyschronousQueue​:在某次添加元素后必须等待其他线程取走后才能继续添加。所以一次只能有一个任务,它可以保证线程安全,但使用通常业务会要求非核心线程无限大。
  • ArrayBlockingQueue​:数组的方式,大小创建后不能改变大小,具有阻塞特性。
  • LinkedBlockingQueue​:无限容量基于链表的形式。
  • PriorityBlockingQueue​:按照优先级进行内部元素排序的无限队列。
  • DelayQueue​:一个带有延迟时间的无界阻塞队列。
  • LinkedTransferQueue​:无限队列,先进先出,具有阻塞特性。
  • LinkedBlockingDeque

关于拒绝处理器
适用:那些既不能进入核心线程、等待队列,也无法使用非核心线程来处理,或者线程异常的线程:

  • CallerRunsPolicy​​:直接运行该任务的​​run​​方法,但不是在线程池内部,适合处理业务比较重要且数量不多的场景。
  • AbortPolicy​​:​​RejectedExecutionException​​异常抛出。适用对业务非常重要的完全不能不执行的场景。(默认)
  • DiscardPolicy​:不会做任何处理。适合处理丢失对业务影响不大的场景。
  • DiscardOldestPolicy​:检查等待队列 强行取出队列头部任务(并抛弃该头部任务)后再进行执行该任务。适合新数据比旧数据重要的场景。

Java Executor预定义的4种线程池
1.CachedThreadPool
​​​CachedThreadPool​​,可缓存的线程池:

  • 该线程池中没有核心线程,非核心线程的数量为​​Integer.max_value​​( 最大值 2 的 31 次方 - 1)
  • 调用 ​​execute​​ 将重用以前构造 的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
    从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资 源。
  • 采用​​SynchronousQueue​​任务队列,适用于耗时少,任务量大的情况。

2.ScheduledThreadPool
​​​ScheduledThreadPool​​,周期性执行任务的线程池:

  • 它可安排在给定延迟后运行命令或者定期地执行。
  • 有核心线程,也有非核心线程,非核心线程数:​​Integer.max_value​​ -核心线程数。
  • 采用​​DelayedWorkQueue​​队列,适用延迟执行、定时执行的情况。

3.SingleThreadPool
​​​SingleThreadPool​​,只有一条线程来执行任务的线程池:

  • 只有一个核心线程,这个线程 池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去
  • 采用​​LinkedBlockingQueue​​队列,适合适用于有顺序的任务的情况。

4.FixedThreadPool
​​​FixedThreadPool​​,定长的线程池:

  • 有核心线程,核心线程的即为最大的线程数量,没有非核心线程。
  • ​FixedThreadPool​​​能保证每一个​​Runnable​​​对象都不会丢失,线程过小会大量进入拒绝策略中,线程数过大,但是一定程度上可能会造成​​OOM​​​。在真实场景采用设置线程总数设为​​Integer.max_value​​。
  • 采用​​LinkedBlockingQueue​​队列,适合能够避免频繁回收线程和创建线程,适合长期任务的情况。

特定场景如何去设置线程池
线程池的关键点是:

  • 1.尽量减少线程切换和管理的开支;
  • 2.最大化利用​​CPU​​。

对于1,要求线程数尽量少,这样可以减少线程切换和管理的开支;
对于2,要求尽量多的线程,以保证​​​CPU​​资源最大化的利用。

根据场景给与的建议:

  • 高并发,低耗时的情况:建议少线程,只要满足并发即可;例如并发100,线程池可能设置为10就可以
  • 低并发,高耗时的情况:建议多线程,保证有空闲线程,接受新的任务;例如并发10,线程池可能就要设置为20;
  • 高并发高耗时的情况:1要分析任务类型,2增加排队,3、加大线程数

ForkJoin Pool

ForkJoinPool 线程池在 ​​JDK 8​​ 加入,主要用法和之前的线程池是相同的,也是把任务交给线程池去执行,线程池中也有任务队列来存放任务,和之前的五种线程池不同的是,它非常适合执行可以分解子任务的任务,比如树的遍历,归并排序,或者其他一些递归场景。

蹊源的Java笔记—线程与线程池_java_03

蹊源的Java笔记—线程与线程池_大厂_04


举报

相关推荐

0 条评论