CPU 时间片轮转机制
为了不让单一进程独占 CPU 太长时间, 出现了 CPU 时间片的概念, 即每个进程只能在一个时间片所规定的间隔运行, 当运行的时长大于时间片时, CPU 会接收到一个中断指令, 因此当前进程停止运行. 这时候系统需要选择下一个可以运行的程序, 因此有了轮询调度(Round Robin) 策略. 该策略维护了一个就绪进程队列表, 当 CPU 接收到中断信号时, 便会调用轮询调度算法, 获取下一个可以执行的进程.
对于 RR调度策略, 需要关注的有三点(我个人简单的理解, 无法保证不出错):
- 所有进程获取 CPU 的权利是否相同
- 如何让 CPU 资源最有效的运行, 而不是将时间消耗在上下文切换上.
进程与线程的关系
进程是操作系统调度的最小单位
线程是CPU调度的最小单位(因此获取到时间片的是线程吗, 而线程的优先级由进程里的调度策略管理, 这个对吗?)
并行与并发的关系
并行是指同一时间内, 系统可以同时多个待执行任务的能力. 并发是依托于并行的(这个对吗?), 是指在并行的能力下, 指定时间内系统的吞吐量. 系统越有序越高效, 并发的吞吐量就越高.
创建线程有哪几种方式
创建一共有两种方式:
一种是通过复写 Thread 的 run 方法, 通过 Thread.start() 启动一个线程.
一种是通过实现 Runnable 中的 run 方法, 通过 Thread 的构造函数将 Runnable 对象传递给 Thread, 最终通过 Thread.start() 启动线程.
还有一中调用方法是通过 FutureTask(Callable callable) 的方式, 将 FutureTask 传递给 Thread 实现多线程任务执行. 但是 FutureTask 其实也是实现了 Runnable 接口, 同时 FutureTask 实现了其中的 run 方法, 在 run 方法中执行 Callable.run方法. 而 Callable 接口和 Runable 接口的区别在于有没有返回值. 同时 FutureTask 提供了一个 get 方法, 在 FutureTask 中维持了对 Callable 任务的执行状态, 如果 Callable 非完成的状态, 则通过 LockSupport.park 方法达到中断当前线程的效果. 同时在子线程执行完成后, 会调用 LockSupport.unpark(Thread) 的方法, 通知所有中断的线程恢复运行.
Thread 是如何启动线程的
Thread 通过 Thread.start() 调用 Thread 中的 native 函数, 将 Thread 和操作系统的线程对应起来(这里是一一对应吗, 还是有其他的映射关系?), 在系统的子线程中通过 native 调用 java 层的 Thread.run 函数的方式, 完成业务函数的调用.
如何安全的停止线程
- Thread.stop() 函数可以提供停止线程的方式, 最终会调用到
private native void stop0(Object var1);方法完成线程的终止. 但是这种方式会让程序在任意时刻停止, 让程序变得不稳定, 是过时不推荐的方式. - 通过 Thread.interrupt() 的方式产生中断信号. 对于 Thread.interrupt() 中断信号, 有三种接收方式:
2.1 在 Thread.sleep 函数中, 捕获 InterruptedException
2.2 在 Thread.wait 函数中, 捕获 InterruptedException
2.3 在 run 函数中检测 Thread.isInterrupted() 函数, 如果返回 true , 结束代码逻辑.
2.4 需要注意的是, 当通过 Thread.interrupt() 函数从 sleep 或者 wait 返回后, Thread .isInterrupt() 标志会被清空, 返回 false. 因此需要根据业务逻辑决定是否重新调用 Thread.interrupt() 设置标识位.
线程的状态
一共有七种状态:
- 创建: new Thread() 后处于该状态
- 就绪: 调用 Thread.start() 函数后处于该状态. 在该状态下, 当进程获取到时间片时, 可以在就绪线程列表中, 按一定的算法选择线程, 通过系统调度使线程运行起来, 则线程进入运行态.
- 运行: 获取到时间片处于执行状态的线程, 则处于运行态. 可以通过 Thread.yield() 的方法让出时间片, 使线程进入就绪态.
- 等待: 运行状态调用 Object.wait 让出 CPU 资源, 并等待他人唤醒. 或者通过 Thread.join, LockSupport.park() 让出 CPU 资源, 并等待线程执行完成后被自动唤醒.
- 等待超时: 通过 Object 的 wait(long timeout), 或者Thread 的 sleep(long timeout) 和 join(long timeout), 或者 LockSupport.parkNanos(long timeout)进入等待状态. 若超时后, 则恢复就绪状态, 等待调度.
- 阻塞: ReentranceLock 造成的等待不属于阻塞(没想明白), 只有通过 synchroinzed 造成的等待才算阻塞. (我感觉这个概念可能和锁的底层实现有关)
- 结束(线程执行完毕)
LockSupport.park(Thread), LockSupport.unpark(Thread), LockSupport.parkNanos(), LockSupport.parkUnit()
ReentranceLock 也是通过 LockSupport 实现的等待唤醒.
先不要问我 LockSupport 底层怎么实现的. Unsafe.park 方法是 native 方法. 我本地暂时看不了.
如何控制线程执行的顺序
- 通过 Thread.join(): 让出当前调用 Thread.join() 方法线程的 CPU 资源, 等被调用 thread.join() 的 thread 处理完成后, 再恢复当前线程的执行.
- 通过 wait/notify 方法实现等待唤醒.
Thread.setDaemon()
通过 Thread.setDaemon 方法设置 Thread 为当前线程的守护进程. 若没有调用 Thread.setDaemon 方法, 当前线程执行完毕, 还需要等待被调用 setDaemon 方法的线程执行完毕, 进程才算结束. 否则若当前线程执行完毕, 当前线程开启的线程无论状态如何, 进程都将结束.
国之将亡必有妖, 老而不死是为贼.
Thread.sleep 和 Object.wait 的区别.
sleep 和 wait 都会释放 CPU 资源. sleep 在指定的时间后, 会由 CPU 自动切换为就绪状态, 等待系统调度执行. 而 wait 需要等待用户主动唤醒, 才会回到就绪状态, 等待系统调度执行. sleep 休眠状态期间, 当前代码块占有的锁不会被释放, 而 wait 等待期间, 当前占有的锁资源会被释放.
开发过程中你会考虑线程优先级吗
不会考虑线程优先级, 因为线程优先级是高度依赖于底层系统的实现的, 而 java 的线程优先级没有和系统优先级一一对应, 这会造成线程优先级与预期结果不符合, 造成不确定的风险.
run 和 start 的区别是什么
run 里面封装的是业务代码, 当前线程调程 run 方法, 不会创建子线程, 因此业务代码跑在当前线程. 当前线程调用 start 创建子线程后, start 会调用 native 方法, 通过系统调用创建子线程, 而新创建的子线程中, 会调用 Thread 的 run 方法, 完成业务代码的调用, 因此 run 方法跑在新的线程中.
如何让当前线程释放 CPU 资源
调用 Thread.currentThread().yield() 方法
wait/notify 为什么需要处于同步代码块中
因为 wait/notify 需要获取到同步锁, 才能完成等待和唤醒的功能.(很牵强有没有......有的...)
死锁产生的条件(都要满足):
- 多个用户争夺多个资源, 并且 userCount > resourceCount >= 2
- 用户争夺资源的顺序不同
- 用户不释放获得的资源
活锁
产生死锁的时候, 如果我们通过 ReentranceLock.tryLock 方法, 便可以避免 用户不释放锁资源 的条件, 规避死锁. 但是当多线程产生死锁时, 下次执行还可能继续产生短暂的死锁, 造成多线程不停的在产生短暂的死锁, 恢复, 短暂的死锁, 恢复之间循环. 虽然没有死锁, 但是业务代码也并没有执行成功.
A(1)(2) -> A()() -> A(1)(2) -> A()() -> A(1)(2) -> A()() -> A(1)(2) ... B(2)(1) -> B()() -> B(2)(1) -> B()() -> B(2)(1) -> B()() -> B(2)(1) ...
Random random = new Random();
ReentranceLock lockA = new ReentranceLock();
ReentranceLock lockB = new ReentranceLock();
public void threadFunctionA() {
while (true) {
if (lockA.tryLock()) {
try {
if (lockB.tryLock()) {
try {
...
} finally {
lockB.unlock();
}
}
} finnaly {
lockA.unlock();
}
}
// 通过随机休眠, 让线程错开执行, 减少活锁产生的时长.
Thread.sleep(random.nextInt(3));
}
}
public void threadFunctionA() {
while (true) {
if (lockB.tryLock()) {
try {
if (lockA.tryLock()) {
try {
...
} finally {
lockB.unlock();
}
}
} finnaly {
lockA.unlock();
}
}
// 通过随机休眠, 让线程错开执行, 减少活锁产生的时长.
Thread.sleep(random.nextInt(3));
}
}
线程饥饿
线程长时间获取不到 CPU 时间片, 例如优先级低, 例如有其他线程长时间获取锁. 优先级需要系统线程调度来解决, 锁则没有办法了.
CAS 指令
一个对比值相同则设置的 CPU 指令. 自旋: while/for 等循环