线程

创建线程的方式

有两种创建线程的方式,第 1 种方式是通过实现 Runnable 接口实现多线程;第 2 种方式是继承 Thread 类;我还知道线程池和Callable 也是可以创建线程的,但是它们本质上也是通过前两种基本方式实现的线程创建。

对于线程池而言,本质上是通过线程工厂创建的,可以给创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等,但是本质上还是通过new Thread()创建的

第 4 种线程创建方式是通过有返回值的 Callable 创建线程,Runnable 创建线程是无返回值的,而 Callable 和与之相关的 Future、FutureTask,它们可以把线程执行的结果作为返回值返回

本质上,实现线程只有一种方式,就是构造一个Thread类。而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。

如何停止线程

首先,从原理上讲应该用 interrupt 来请求中断,而不是强制停止,因为这样可以避免数据错乱,也可以让线程有时间结束收尾工作。volatile 这种方法不能够全面的停止线程,比如线程被长时间阻塞的情况,就无法及时感受中断。

典型的线程不安全的场景

  1. 运行结果错误,因为执行步骤非原子操作,分为读取,增加,保存
  2. 发布和初始化导致线程安全问题,没有等待初始化完毕就调用,导致空指针问题
  3. 活跃性问题,包括死锁、活锁和饥饿,双方互相等待资源

多线程的性能问题

  1. 线程调度
  1. 上下文切换带来的性能开销比执行线程本身内容带来的开销还要大
  2. 缓存失效导致重新缓存新的数据,造成开销
  1. 线程协作
  1. 为了保证数据正确性,和线程安全,会出现禁止编译器和CPU冲排序优化,反复将线程工作内存数据flush进主存的情况间接降低性能

线程池

没有线程池的时候,每发布一个任务就需要创建一个新的线程。

但是如果创建了10000个子线程,会有以下问题:

  1. 返回创建线程系统开销大,线程的创建和销毁都需要时间
  2. 过多的线程会占用过多的内存等资源带来过多的上下文切换,导致系统不稳定

所以需要线程池来平衡线程和系统资源之间的关系。

  1. 针对反复创建线程开销大的问题,线程池用一些固定的线程一直保持工作状态并反复执行任务
  2. 针对过多线程占用太多内存资源的问题,根据需要创建线程,控制线程的总数量,避免占用过多内存资源

线程池的好处:

  1. 第一点,线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应速度。因为线程池中的线程是可以复用的,我们只用少量的线程去执行大量的任务,这就大大减小了线程生命周期的开销。而且线程通常不是等接到任务后再临时创建,而是已经创建好时刻准备执行任务,这样就消除了线程创建所带来的延迟,提升了响应速度,增强了用户体验。
  2. 第二点,线程池可以统筹内存和 CPU 的使用,避免资源使用不当。线程池会根据配置和任务数量灵活地控制线程数量,不够的时候就创建,太多的时候就回收,避免线程过多导致内存溢出,或线程太少导致 CPU 资源浪费,达到了一个完美的平衡。
  3. 第三点,线程池可以统一管理资源。比如线程池可以统一管理任务队列和线程,可以统一开始或结束任务,比单个线程逐一处理任务要更方便、更易于管理,同时也有利于数据统计,比如我们可以很方便地统计出已经执行过的任务的数量。

构造自己的线程池

核心线程数: 线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程。

阻塞队列:ArrayBlockingQueue,在容量和maxPoolSize之间权衡。

  • 如果容量大,最大线程数小,可以减少上下文切换带来的开销,降低整体的吞吐量
  • 稍小容量的队列和更大的最大线程数,整体的效率更高,但是会因为拒绝新提交的任务造成数据丢失

线程工厂:使用默认也可以自定义

拒绝策略: :AbortPolicy,DiscardPolicy,DiscardOldestPolicy 或者 CallerRunsPolicy。

如何关闭:用 shutdown() 方法来关闭,这样可以让已提交的任务都执行完毕,但是如果情况紧急,那我们就可以用 shutdownNow 方法来加快线程池“终结”的速度。