1. 线程上下文切换
巧妙地利用了时间片轮转的方式, CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存
下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载, 这段过程就叫做
上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。
1.1 进程
(有时候也称做任务)是指一个程序运行的实例。在 Linux 系统中,线程就是能并行运行并且
与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量
级的进程。
1.2 上下文
是指某一时间点 CPU 寄存器和程序计数器的内容。
1.3 寄存器
是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内
存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速
度。
1.4 程序计数器
是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令
的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
1.5 PCB-“切换桢”
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,上下
文切换过程中的信息是保存在进程控制块(PCB, process control block)中的。PCB 还经常被称
作“切换桢”(switchframe)。信息会一直保存到 CPU 的内存中,直到他们被再次使用。
1.6 上下文切换的活动:
(1) 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
(2)在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
(3) 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序
中。
1.7 引起线程上下文切换 引起线程上下文切换的原因
(1)当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
(2)当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
(3)多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
(4) 用户代码挂起当前任务,让出 CPU 时间;
(5)硬件中断;
2 同步锁与死锁
2.1 同步锁
当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程
同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可
以使用 synchronized 关键字来取得一个对象的同步锁。
2.2 死锁
何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
2.3 线程池原理 原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后
启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,
再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
2.3.1 线程复用
每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run
方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写
Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实
现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以
是阻塞的。
2.3.2 线程池的组成
一般的线程池主要分为以下 4 个组成部分:
(1)线程池管理器:用于创建并管理线程池
(2)工作线程:线程池中的线程
(3)任务接口:每个任务必须实现的接口,用于工作线程调度其运行
(4)任务队列:用于存放待处理的任务,提供一种缓冲机制
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,
ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
ThreadPoolExecutor 的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
(1)corePoolSize:指定了线程池中的线程数量。
(2) maximumPoolSize:指定了线程池中的最大线程数量。
(3)keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
次时间内会被销毁。
(4) unit:keepAliveTime 的单位。
(5) workQueue:任务队列,被提交但尚未被执行的任务。
(6)threadFactory:线程工厂,用于创建线程,一般用默认的即可。
(7)handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
2.3.3 拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
(1) AbortPolicy : 直接抛出异常,阻止系统正常运行。
(2) CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的
任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
(3)DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再
次提交当前任务。
(4)DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢
失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际
需要,完全可以自己扩展 RejectedExecutionHandler 接口。
2.3.4 Java 线程池工作过程
(1)线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面
有任务,线程池也不会马上执行它们。
(2)当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
会抛出异常 RejectExecutionException。
(3) 当一个线程完成任务时,它会从队列中取下一个任务来执行。
(4) 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
最终会收缩到 corePoolSize 的大小。