Java高并发核心编程-多线程原理与实战

第1章 多线程原理与实战

1.程序、进程、线程的概念与区别
  • 程序:存放在硬盘中的可执行文件,只要包括代码指令和数据。
  • 进程一个进程是程序的一次启动和执行(注意对应关系)。
    • 组成部分:程序段(代码段),数据段,PCB(进程控制块:进程的描述信息、调度信息、资源信息、上下文)
  • 线程:“进程代码段”的一次顺序执行流程。
    • 组成部分:PC(程序计数器)、栈内存(方法帧即栈帧、可参考JVM内存结构)、线程基本信息(描述信息)。
2.创建线程的4种方式

Thread的两个静态方法currentThread() 和sleep();

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法
    • 对比上一种方式的优缺点:
      • 缺点:
        • 所创建的”线程“类不是真正的线程类,而是线程的target目标执行类。
        • 如果要访问当前线程的属性,不能直接访问,必须通过Thread.currentThread() 方法获取当前线程。
      • 优点:
        • 避免Java单继承带来的局限性。
        • 逻辑与数据更好分离,更适合并发场景,如同一个资源被多个业务逻辑并行处理。
  • 使用Callable和FutureTask创建线程
    • 带返回值
    • 可以获取异步执行结果
    • 中间桥接接口RunnableFuture。可看FutureTask类的UML关系图
  • 通过线程池创建线程
3.线程的核心原理
  • 线程的调度与时间片
    • 线程调度方式:基于CPU时间片的方式进行线程调度
    • 调度模型:
      • 分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用
      • 抢占式调度模型:系统按照线程优先级分配CPU时间片
  • 线程优先级:优先级越高,获取执行机会的随机性(可能性)越高,但并不代表执行机会一定多,只能说大概率
  • 线程的生命周期:以下指JVM层面的线程,不是操作系统(新建、就绪、运行、终止)
    • NEW:线程创建成功,但是没有调用start()
    • RUNNABLE:获取了cpu时间片,开始执行
      • 就绪状态
        • 调用start()
        • 当前线程的时间片用完
        • 线程sleep结束
        • 对其他线程join操作结束
        • IO阻塞结束
        • 线程抢到对象锁
        • 调用yield(),让出cpu执行权限
      • 运行状态
    • BLOCKED:阻塞,不会占用cpu资源
      • 线程等待获取锁
      • IO阻塞
    • WAITING:无时限等待,一般为条件等待,需要被手动唤醒
      • Object.wait()
      • Thread.join()
      • LockSupport.park()
    • TIMED_WAITING:限制等待
      • Thread.sleep(times)
      • Object.wait(times)
      • Thread.join(times)
      • LockSupport.parkNanos(times)
      • LockSupport.parkUntil(times)
    • TERMINATION:执行完run() 之后
  • 线程的Interrupt
    • interrupt():
      • 处于阻塞状态的线程:立马退出阻塞,并抛出InterruptedException异常。清空线程的中断标记(设置为false)
      • 处于运行状态的线程:不受影响,继续运行。设置线程的中断标记为true
    • isInterrupted():判断中断标记,不会清空中断标记
    • interrupted():判断中断标记,清空中断标记
4.线程池原理

为什么需要线程池?

  • Java线程的创建非常昂贵,需要JVM和OS(操作系统)的大量工作
    • 必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存
    • 需要进行系统调用时,以便在OS中创建和注册本地线程

JUC中线程池的简单结构图

继承
实现
实现
继承
继承
实现
继承
<<接口>>
Executor
<<接口>>
ExecutorService
Executors
AbstractExecutorService
<<接口>>
ScheduledExecutorService
ThreadPoolExecutor
ScheduledThreadPoolExecutor
  • Executor:Java异步目标任务的“执行者”接口,目标仅仅是执行目标任务。
    • 只包含一个函数式方法:void execute( Runnable command )
  • ExecutorService:继承于Execute接口,对外提供异步任务的接收服务
    • 提供了“接收异步任务并转交给执行者”的方法,如submit,invoke系列等方法。
  • AbstractExecutorService:ExecutorService的默认实现
  • ThreadPoolExecutor:JUC线程池的核心实现类
  • ScheduledExecutorService:可以完成“延时”和“周期性”任务的调度线程池接口,其功能和Timer/TimeTask类似。
  • ScheduledThreadPoolExecutor:ScheduledExecutorService的默认实现
  • Executors:静态工厂类,返回ExecutorService,ScheduledExecutorService等线程池示例对象。
    • newSingleThreadExecutor():创建只有一个线程的线程池
      • 单线程化的线程池中的任务是按照提交的次序顺序执行的
      • 池中的唯一线程的存活时间是无限的
      • 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且阻塞队列是无界的
    • newFixedThreadPool(int nThreads):创建固定大小的线程池
      • 如果线程数没有达到“固定数量”,每提交一个任务线程池内就会创建一个新的线程,直到线程数量达到“固定数量”
      • 线程池的大小一旦达到了“固定数量”
        • 所有线程正常,那么新提交的任务就会进入(无界的)阻塞队列中。
        • 但是如果某个线程因为执行异常而结束了,仍然会补充新的线程,直到达到“固定数量”
    • newCachedThreadPool():创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但是空闲的线程会得到即时回收
      • 在接受新的异步任务target 执行目标实例时,如果池内所有线程繁忙,线程池就会创建一个新的线程来处理任务
      • 此线程池不会对线程池的大小进行限制,线程池的大小完全依赖于OS(或者说JVM)能够创建的最大线程大小
      • 如果有部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60 秒内不执行任务)线程
    • newScheduledThreadPool():创建一个可定期或者延时执行任务的线程池
						// 两个重要的方法
						// 以固定速率执行
						public ScheuledFuture<?> scheduledAtFixedRate(
						  		Runnable command, // 异步执行目标实例
						  		long initialDelay, // 首次执行延时
						  		long period, // 两次开始执行最小间隔时间
						  		TimeUnit unit, // 所设置的时间的计时单位,如TimeUnit.SECONDS常量
						);
						
						// 以固定延时执行
						public ScheuledFuture<?> scheduledWithFixedDelay(
						  		Runnable command, // 异步执行目标实例
						  		long initialDelay, // 首次执行延时
						  		long delay, // 前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间)
						  		TimeUnit unit, // 所设置的时间的计时单位,如TimeUnit.SECONDS常量
						);
						
						/**
						*    当调用任务的执行时间大于指定的时间间隔时,ScheduledExecutorService不会创建新的线程
						* 去并发执行这个任务,而是等待前一次调度执行完毕
						*/

线程池的标准创建方式

  • 线程池的标准构造器
			// 使用标准构造器构造一个普通的线程池
			public ThreadPoolExecutor(
				int corePoolSize, // 核心线程数,即时线程空闲(Idle),也不会回收
			  int maximumPoolSize, // 线程数的上线
			  long keepAliveTime, // 线程最大空闲(Idle)时间
			  TimeUnit unit, // 时间单位
			  BlockingQueue<Runnable> workQueue,// 任务的排队队列
			  ThreadFactory threadFactory, // 新线程的产生方式
			  RejectExecutionhandler handler, // 拒绝策略
			)
  • 线程池的任务调度流程
    • 如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
    • 如果线程池中总的任务数量大于核心线程数量,新接受的任务将会被加入阻塞队列中,一直到阻塞队列已满。在核心线程数量用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
    • 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行任务,一直到阻塞队列为空,其中所有的缓存任务被取光。
    • 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接受到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
    • 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过了maximumPoolSize,线程池就会执行拒绝接受任务,当新任务到来时,会为新任务执行拒绝策略。
      在这里插入图片描述
  • ThreadFactory(线程工厂)

    package java.util.concurrent
    
    public interface ThreadFactory {
      // 函数式编程
      Thread newThread(Runnable target);
    }
    
  • 任务阻塞队列

    • ArrayBlockingQueue:
      • 是一个数组实现的有界阻塞队列
      • 队列中的元素按FIFO排序
      • 在创建时必须设置大小
    • LinkedBlockingQueue
      • 是一种基于链表实现的阻塞队列
      • 按FIFO排序任务
      • 可以设置容量,不设置时则默认使用Integer.Max_VALUE作为容量(无界队列)
      • 该队列的吞吐量高于ArrayBlockingQueue
    • PriorityBlockingQueue
      • 具有优先级的无界队列
    • DelayQueue
      • 无界阻塞延迟队列
      • 底层基于PriorityBlockingQueue实现
      • 队列中的每个元素都有过期时间
      • 只有已经过期的元素才会出队,队列头部的元素是过期最快的元素
    • SynchronousQueue
      • (同步队列)是一个不存储元素的阻塞队列
      • 每个插入操作必须等到另一个线程调用移除操作,否则插入操作会一直处于阻塞队列
      • 该队列的吞吐量高于LinkedBlockingQueue
  • 调度器的钩子函数

    • beforeExecute:异步任务执行之前的钩子函数
    • afterExecute:异步任务执行之后的钩子函数
    • terminated:线程池终止时的钩子函数
  • 拒绝策略

    • AbortPolicy:拒绝策略
      • 使用该策略时,如果线程池队列满了,新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略
    • DiscardPolicy:抛弃策略
      • 该策略是AbortPolicy的Slient(安静)版本,如果线程池队列满了,新任务就会直接被丢弃,并且不会抛出任何异常
    • DiscardOldestPolicy:抛弃最老任务策略
      • 如果队列满了,就会将最早加入队列的任务抛弃,从队列中腾出空间,在尝试加入队列。
      • 因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队
    • CallerRunsPolicy:调用者执行策略
      • 在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
    • 自定义策略
      • 实现RejectedExecutionHandler接口的rejectedExecution方法。
  • 如何优雅的关闭线程池

    • 线程池的五种状态
      • RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务
      • SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕
      • STOP:该状态下线程池不会再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程
      • TIDYING:该状态下所有任务都已终止或者处理完毕,将会执行timinated()钩子方法
      • TERMINATED:执行完timinated()钩子方法之后的状态
    • 线程池的状态转换规则:
      • 线程池创建之后的状态为RUNNING
      • 执行线程池的shutdown() 实例方法,会使线程池状态从RUNNING转变为SHUTDOWN
      • 执行线程池的shutdownNow() 实例方法,会使线程池状态从RUNNING转变为STOP
      • 当线程池处于SHUTDOWN状态时,执行shutdownNow()方法会将其状态转变为STOP
      • 等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为TIDYING
      • 执行完timinated()之后,线程池状态由TIDYING转TERMINATED
    执行shutdownNow()
    执行shutdownNow()
    执行shutdown()
    queue empty
    pool empty
    回调timinated()
    RUNNING
    STOP
    SHUTDOWN
    TIDYING
    TERMINATED
    • awaitTermination()方法
      • 调用了线程池的shutdown()与shutdownNow()方法之后,用户程序都不会主动等待线程池关闭完成,如果需要等待线程池关闭完成,需要调用awaitTermination()进行主动等待。
      • 如果线程池完成关闭,该方法会返回true,否则当等待时间超过指定时间之后会返回false。
      • 如果需要等待线程池完成关闭,建议不是永久等待,而是设置一定的重试次数,可以参考Dubbo框架中的线程池的关闭源码
    • 优雅步骤
      • ,拒绝新任务的提交,并等待所有任务有序地执行完毕
      • 执行awaitTermination(long timeout, TimeUnit unit)方法,指定超时时间,并判断是否已经关闭所有任务,线程池关闭完成
      • 如果awaitTermination()返回false,或者被中断,就调用shutdownNow()方法立即关闭线程池的所有任务
      • 补充执行awaitTermination(long timeout, TimeUnit unit)方法,判断是否关闭完成。如果超时,就可以进入循环关闭。循环到一定的次数(1000次),不能关闭线程池,直到其关闭或者循环结束
    // 优雅的关闭线程池
    public static void shutdownThreadPoolGracefully(ExecutorService threadPool){
      // 若已经关闭则直接返回
      if(!(threadPool instanceof ExecutorService) || threadPool.isTerminated() ){
        return;
      }
      // 执行shutdown()方法
      try{
        threadPool.shutdown();
    	}catch(SecurityException e){
        // 异常处理
    	}catch(NullPointException e){
        // 异常处理
    	}
      try{
        // 等待60秒的关闭时间
    		if(!threadPool.awaitTermination(60, TimeUnit.SECONDS)){
          // 如果还没关闭,则直接强制关闭
          threadPool.shutdownNow();
          if(!threadPool.awaitTermination(60, TimeUnit.SECONDS)){
            // 如果还没关闭,打印提示日志
            log.error();
          }
        }
      }catch(InterruptedException ie){
        // 捕获异常,继续强制关闭
        threadPool.shutdownNow();
      }
      // 如果还没关闭,开始循环关闭
      if(!threadPool.isTerminated()){
        try{
          for(int i = 0; i < 1000; i++){
            if(!threadPool.awaitTermination(10, TimeUnit.SECONDS)){
              break;
            }
            threadPool.shutdownNow();
          }
        }catch(InterruptedException ie){
          // 异常处理
    		}catch(Throwable e){
          // 异常处理
    		}
      }
    
      // 还是没有关闭,自定义后续处理步骤
    
    }
    
  • 确定线程池的线程数

    • 使用线程池的好处
      • 降低资源消耗:线程是稀缺资源,如果无限地创建资源,不仅会消耗系统资源,还会降低系统的稳定性,通过重复利用已创建的线程可以降低线程创建和销毁造成的消耗
      • 提升响应速度:当任务到达时,不需要等待线程创建就能立即执行
      • 提高线程的可管理性:线程池提供了一种机制、管理资源的策略,维护了一些基本的线程统计信息,如已完成的任务数量等
    • 线程池的异步任务分类
      • IO密集型任务:IO处理线程数为CPU核数的2陪
      • CPU密集型任务:CPU密集型任务并行执行的数量等于CPU核数
      • 混合型任务:最佳线程数 = (线程等待时间+线程CPU时间)/ 线程CPU时间 * CPU核数
5.ThreadLocal原理与使用

​ ThreadLocal 的英文字面意思为 “ 本地线程 ”,实际上ThreadLocal 代表的是线程的本地变量,可能将其命名为ThreadLocalVariable 更加容易让人理解

​ ThreadLocal 是解决线程安全问题的较好方案,它通过为每个线程提供一个独立的本地值去解决并发冲突问题。在很多情况下,使用ThreadLocal 比直接使用同步机制(如synchronized)解决线程安全问题更简单、更方便且并发性更好。

  • 使用场景:
    • 线程隔离:ThreadLocal 的主要价值在于线程隔离,ThreadLocal 中的数据只属于当前线程,其本地值对于别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改
    • 跨函数传递数据:通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递是比要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合性
  • 内部结构的演变:
    • 早期版本
      • ThreadLocal 的内部结构是一个Map,其中每一个线程实例作为Key,线程在 ThreadLocal 中绑定的值为Value。
    • JDK8
      • Map迁移到了Thread 中,每一个Thread 实例拥有一个 Map,并且Key为ThreadLocal 实例
    • JDK8相比于早期版本额优势
      • 每个ThreadLocalMap 存储的 ”Key - Value“ 值变少。早期版本的 ”Key - Value“ 数量与线程个数强关联,若线程数量变多,则存储 ”Key - Value“ 也变多。
      • 早期版本的ThreadLocalMap的拥有者为ThreadLocal,在线程实例被销毁后,ThreadLocalMap还是存在的;而新版本拥有者时Thread,线程实例销毁后,ThreadLocalMap也会被销毁回收,在一定程度上能减少内存的消耗。
  • 内存泄露
    • ThreadLocalMap中的Entry 的Key需要使用弱引用
    • 什么是弱引用?:仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前。
    • 什么是内存泄露?:不再用到的内存没有及时释放(归还系统),就叫做内存泄露
    • 发生内存泄露的前提条件:
      • 线程长时间运行而没有被销毁。线程池中的线程就是这样
      • ThreadLocal 引用设置为null,且后续在同一个Thread 实例执行期间,没有发生对其他ThreadLocal 实例的get(), set() 或remove() 操作。只要存在一个针对任何ThreadLocal 实例的get(), set() 或remove(),就会触发Thread 实例拥有的ThreadLocalMap的Key为null 的Entry 清理工作,释放掉ThreadLocal 弱引用为null 的Entry。
  • 使用原则
    • 尽量使用private static final 修饰ThreadLocal 实例。使用private 和 final 主要是为了不让尽可能不让别人修改、变更ThreadLocal的引用,使用static 确保ThreadLocal 实例是全局唯一的
    • ThreadLocal 使用完成后,请务必调用remove() 方法。这是最简单、有效地避免ThreadLocal 引发内存泄露问题的方法
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值