线程
创建线程的方式
有两种创建线程的方式,第 1 种方式是通过实现 Runnable 接口实现多线程;第 2 种方式是继承 Thread 类;我还知道线程池和Callable 也是可以创建线程的,但是它们本质上也是通过前两种基本方式实现的线程创建。
对于线程池而言,本质上是通过线程工厂创建的,可以给创建的线程设置一些默认值,比如:线程的名字、是否是守护线程,以及线程的优先级等,但是本质上还是通过new Thread()创建的
第 4 种线程创建方式是通过有返回值的 Callable 创建线程,Runnable 创建线程是无返回值的,而 Callable 和与之相关的 Future、FutureTask,它们可以把线程执行的结果作为返回值返回
本质上,实现线程只有一种方式,就是构造一个Thread类。而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。
如何停止线程
首先,从原理上讲应该用 interrupt 来请求中断,而不是强制停止,因为这样可以避免数据错乱,也可以让线程有时间结束收尾工作。volatile 这种方法不能够全面的停止线程,比如线程被长时间阻塞的情况,就无法及时感受中断。
典型的线程不安全的场景
- 运行结果错误,因为执行步骤非原子操作,分为读取,增加,保存
- 发布和初始化导致线程安全问题,没有等待初始化完毕就调用,导致空指针问题
- 活跃性问题,包括死锁、活锁和饥饿,双方互相等待资源
多线程的性能问题
-
线程调度
- 上下文切换带来的性能开销比执行线程本身内容带来的开销还要大
- 缓存失效导致重新缓存新的数据,造成开销
-
线程协作
- 为了保证数据正确性,和线程安全,会出现禁止编译器和CPU冲排序优化,反复将线程工作内存数据flush进主存的情况间接降低性能
线程池
没有线程池的时候,每发布一个任务就需要创建一个新的线程。
但是如果创建了10000个子线程,会有以下问题:
- 返回创建线程系统开销大,线程的创建和销毁都需要时间
- 过多的线程会占用过多的内存等资源带来过多的上下文切换,导致系统不稳定
所以需要线程池来平衡线程和系统资源之间的关系。
- 针对反复创建线程开销大的问题,线程池用一些固定的线程一直保持工作状态并反复执行任务
- 针对过多线程占用太多内存资源的问题,根据需要创建线程,控制线程的总数量,避免占用过多内存资源
线程池的好处:
- 第一点,线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应速度。因为线程池中的线程是可以复用的,我们只用少量的线程去执行大量的任务,这就大大减小了线程生命周期的开销。而且线程通常不是等接到任务后再临时创建,而是已经创建好时刻准备执行任务,这样就消除了线程创建所带来的延迟,提升了响应速度,增强了用户体验。
- 第二点,线程池可以统筹内存和 CPU 的使用,避免资源使用不当。线程池会根据配置和任务数量灵活地控制线程数量,不够的时候就创建,太多的时候就回收,避免线程过多导致内存溢出,或线程太少导致 CPU 资源浪费,达到了一个完美的平衡。
- 第三点,线程池可以统一管理资源。比如线程池可以统一管理任务队列和线程,可以统一开始或结束任务,比单个线程逐一处理任务要更方便、更易于管理,同时也有利于数据统计,比如我们可以很方便地统计出已经执行过的任务的数量。
构造自己的线程池
核心线程数: 线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程。
阻塞队列:ArrayBlockingQueue,在容量和maxPoolSize之间权衡。
- 如果容量大,最大线程数小,可以减少上下文切换带来的开销,降低整体的吞吐量
- 稍小容量的队列和更大的最大线程数,整体的效率更高,但是会因为拒绝新提交的任务造成数据丢失
线程工厂:使用默认也可以自定义
拒绝策略: :AbortPolicy,DiscardPolicy,DiscardOldestPolicy 或者 CallerRunsPolicy。
如何关闭:用 shutdown() 方法来关闭,这样可以让已提交的任务都执行完毕,但是如果情况紧急,那我们就可以用 shutdownNow 方法来加快线程池“终结”的速度。