• 多线程进阶篇


    目录

    线程池

    创建线程池的目的

    线程池的概念

    JDK中线程池的使用

    线程池的核心父类接口:ExecutorService接口

    ​编辑 Excutors=>线程池的工具类

    固定大小的线程池

    数量动态变化的缓存池

    单线程池

    定时线程池

     线程池的接口和类

     ThreadPoolExector子类的核心构造方法参数

     线程池工作流程

     常见锁的策略

    1.乐观锁和悲观锁

    乐观锁

     悲观锁:

    2.读写锁

    读写锁的适用条件

    读写锁的特性

    3.重量级锁和非重量级锁

    重量级锁

    轻量级锁

    4.公平锁和非公平锁

    公平锁

    非公平锁

    synchronized锁的升级策略


    线程池

    创建线程池的目的

    线程池创建的目的在于:如果我们直接采用像之前的Thread构造方法创建线程,其实在每次创建和销毁线程时都有一定的开支,当线程太多时这个开支还是挺明显的。

    “池”:目的就是让某些对象被多次重复利用,减少频繁创建和销毁对象带来的开支问题

    所以说,线程池最大的好处就是可以减少每次启动和销毁线程的损耗(提高时间和空间利用率)

    线程池的概念

    线程池内部创建了若干个线程,这些线程都是Runnable的状态,只需要从系统中取出任务(run),就可以立即开始执行。

    我们将线程池比作一个餐厅:

    餐厅中的固定员工:线程池中的线程

    后面招聘的临时员工:当线程池中的线程不够用时,可产生临时员工。

    JDK中线程池的使用

    线程池的核心父类接口:ExecutorService接口

    执行任务方法:excute

    提交任务方法:submit

    提交一个任务到线程池(线程的run或者call方法),池中会派遣空闲线程执行任务 

     终止线程方法:shutdown

    立即终止所有线程:shutdownNow();  无论线程是否在空闲状态

    停止在空闲态的线程,运行线程在结束后停止:shutdown();

     
    Excutors=>线程池的工具类

    这个类可以创建JDK内置的四大线程池

    加s的基本都是工具类 比如Arrays(数组工具类,copyOf,sort等等)

    固定大小的线程池

    1. //固定大小的线程池
    2. ExecutorService pool = Executors.newFixedThreadPool(10);

    采用.submit()方法执行任务

    1. pool.submit(new Runnable() {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 3; i++) {
    5. try {
    6. Thread.sleep(1000);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. System.out.println(Thread.currentThread().getName()+"绕"+i+"圈");
    11. }
    12. }
    13. });

    工具类源码:

    数量动态变化的缓存池

    1. //数量动态变化的缓存池
    2. ExecutorService pool1 = Executors.newCachedThreadPool();

    工具类源码:

    橘色的参数基本是用不到的,因为最大线程数有四十多亿

    单线程池

    ps:单线程池的意义在于,省去创建线程和销毁线程的时间,来一个任务执行一个任务,还未执行的任务在工作队列排队执行。

    1. //只包含一个线程的单线程池
    2. ExecutorService pool2 = Executors.newSingleThreadExecutor();

    工具类源码:

    定时线程池

    ps:使用的是定时器线程接口,线程池核心接口的子接口

    1. //定时器线程池
    2. ScheduledExecutorService pool3 = Executors.newScheduledThreadPool(10);

    采用.schedule接收任务,可规定延迟多久执行任务

    1. pool3.schedule(new Runnable() {
    2. @Override
    3. public void run() {
    4. for (int i = 0; i < 3; i++) {
    5. try {
    6. Thread.sleep(1000);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. System.out.println(Thread.currentThread().getName()+"绕"+i+"圈");
    11. }
    12. }
    13. },3, TimeUnit.SECONDS);

    工具类源码:

     线程池的接口和类

    ExecutorService:线程池的核心接口,提供excute方法,submit方法,shutdown方法

    ScheduledExecutorService:定时器接口,线程池核心接口的子接口,追加.schedule方法,可以延迟启动任务

    ThreadPoolExector类:实现核心接口的子类,也是线程池接口的最常用实现子类

    Executors:线程池的工具类,提供了ThreadPoolExector的对象,内置创建好的四大线程池。

     ThreadPoolExector子类的核心构造方法参数

    一图流

     线程池工作流程

    此工作流程基于submit提交一个新任务时线程池内部的执行流程

    首先判断核心池是否已经拉满,没拉满我们可以再创建一个固定线程(正式工)执行此任务

    若已经拉满,我们就去判断工作队列是否已满,没有满的话,我们就在队列中排队等待,

    否则,进行线程池的最大数量判断,如果没满的话就创建一个空闲线程来执行此任务,

    否则,若线程池已经达到了最大数量,说明此时线程池已经达到最大负荷了,我们就需要执行拒绝策略了

    开始执行

     若已经达到核心池最大数量

     若队列已经满了

     当线程数量达到上线,执行拒绝策略

     常见锁的策略

    1.乐观锁和悲观锁

    synchronized最开始就是乐观锁,当竞争激烈就变成悲观锁。

    乐观锁

    每次读写数据都认为不会发生冲突,不会阻塞,一般来说只有在数据更新时才会检查是否发生冲突,若没有冲突直接更新,有冲突再去解决冲突。

    当线程冲突不严重时,可以采用乐观锁策略来避免多次的加锁解锁操作

    eg:

    乐观锁一般通过版本号机制来实现

     比如这两个线程,如果线程1先结束并将主内存的版本从version1变成version2时,线程2写回主内存检测到主内存的版本和自己的工作内存版本对不上,就会直接报错,不写回。

     当下次再进行线程操作时,就会读取新的版本号,这个时候重新进行操作,尝试写回。

     悲观锁:

    每次进行读写数据都会冲突,都需要尝试加锁操作,保证同一时间只有一个线程在读写数据。

    当线程冲突严重时,就需要加锁,来避免线程频繁访问共享数据失败带来的CPU空转问题

    2.读写锁

    读写锁的适用条件

    特别适用于线程基本都在读数据,很少有些数据的情况。

    多数据在访问数据时,并发读数据不会冲突,只有在写数据时才有可能发生冲突,所有就有了读写锁的概念,JDK内置的读写锁是ReentrantReadWriteLock,用这个可实现读写操作。

    读写锁的特性

    1.多个线程并发读数据时,都可以访问到读锁,并发执行不互斥。

    2.多个线程写数据时,两个线程互斥,只有一个能访问到写锁,其他线程阻塞。

    3.一个线程读,另一个线程写,也会互斥,当写的过程结束读线程才能继续执行。

    3.重量级锁和非重量级锁

    重量级锁

    需要操作系统和硬件支持,线程获取重量级锁失败进入阻塞状态(os,用户态切换到内核态,开销非常大)

    eg:

    比如去银行办理业务

    用户态:在窗口外自己处理的业务

    内核态:窗口内部,需要工作人员协助

    这样来回切换是非常耗时的

    轻量级锁

    尽量在用户态执行操作,线程不阻塞,不会进行状态切换。

    轻量级锁的常用实现是采用自旋锁:

    之前的方式是,获取锁失败就进入Blocked状态,线程阻塞等待锁释放,然后由CPU唤醒(这个时间一般都比较长,由用户态切换到内核态)。

    自旋锁:

    就是一个循环的概念

    while(获取lock==false)就进行死循环,但不会让出CPU,线程也不会阻塞,不会切换状态,线程就在CPU上空跑,直到锁被释放,就可以很快速地获取到锁。

    4.公平锁和非公平锁

    公平锁

    获取锁失败的线程进入等待队列,当锁被释放,在队列中等待时间最长的线程首先获取到锁。

    非公平锁

    阻塞队列的各个线程获取到锁的几率相等,不分先后。

    synchronized锁的升级策略

    偏向锁 ->轻量级锁 ->重量级锁

  • 相关阅读:
    【.net core】yisha框架单页面双列表联动效果示例
    统计学习:逻辑回归与交叉熵损失(Pytorch实现)
    BI大屏可视化开发源码,Java+Netcore大屏源码,大屏开发
    采购管控: 缩短采购周期,从源头上降本增效
    技术架构职责和应该注意哪些
    2023年中国城市轨道交通信号系统行业现状分析:城市轨道交通建设市场进入快车道,拉动产品需求发展[图]
    WPF 入门笔记 - 04 - 数据绑定 - 补充内容:资源基础
    测试行业本科应届生薪资大概是多少?外行人15k垫底25k是人均水平...
    python---if语句
    ES6 - 模块化
  • 原文地址:https://blog.csdn.net/weixin_65278827/article/details/125479212