关于多线程我记住的那些概念

227 阅读7分钟

CPU 时间片轮转机制

为了不让单一进程独占 CPU 太长时间, 出现了 CPU 时间片的概念, 即每个进程只能在一个时间片所规定的间隔运行, 当运行的时长大于时间片时, CPU 会接收到一个中断指令, 因此当前进程停止运行. 这时候系统需要选择下一个可以运行的程序, 因此有了轮询调度(Round Robin) 策略. 该策略维护了一个就绪进程队列表, 当 CPU 接收到中断信号时, 便会调用轮询调度算法, 获取下一个可以执行的进程.

对于 RR调度策略, 需要关注的有三点(我个人简单的理解, 无法保证不出错):

  1. 所有进程获取 CPU 的权利是否相同
  2. 如何让 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 函数的方式, 完成业务函数的调用.

如何安全的停止线程

  1. Thread.stop() 函数可以提供停止线程的方式, 最终会调用到 private native void stop0(Object var1); 方法完成线程的终止. 但是这种方式会让程序在任意时刻停止, 让程序变得不稳定, 是过时不推荐的方式.
  2. 通过 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() 设置标识位.

线程的状态

一共有七种状态:

  1. 创建: new Thread() 后处于该状态
  2. 就绪: 调用 Thread.start() 函数后处于该状态. 在该状态下, 当进程获取到时间片时, 可以在就绪线程列表中, 按一定的算法选择线程, 通过系统调度使线程运行起来, 则线程进入运行态.
  3. 运行: 获取到时间片处于执行状态的线程, 则处于运行态. 可以通过 Thread.yield() 的方法让出时间片, 使线程进入就绪态.
  4. 等待: 运行状态调用 Object.wait 让出 CPU 资源, 并等待他人唤醒. 或者通过 Thread.join, LockSupport.park() 让出 CPU 资源, 并等待线程执行完成后被自动唤醒.
  5. 等待超时: 通过 Object 的 wait(long timeout), 或者Thread 的 sleep(long timeout) 和 join(long timeout), 或者 LockSupport.parkNanos(long timeout)进入等待状态. 若超时后, 则恢复就绪状态, 等待调度.
  6. 阻塞: ReentranceLock 造成的等待不属于阻塞(没想明白), 只有通过 synchroinzed 造成的等待才算阻塞. (我感觉这个概念可能和锁的底层实现有关)
  7. 结束(线程执行完毕)

LockSupport.park(Thread), LockSupport.unpark(Thread), LockSupport.parkNanos(), LockSupport.parkUnit()

ReentranceLock 也是通过 LockSupport 实现的等待唤醒.

先不要问我 LockSupport 底层怎么实现的. Unsafe.park 方法是 native 方法. 我本地暂时看不了.

如何控制线程执行的顺序

  1. 通过 Thread.join(): 让出当前调用 Thread.join() 方法线程的 CPU 资源, 等被调用 thread.join() 的 thread 处理完成后, 再恢复当前线程的执行.
  2. 通过 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 需要获取到同步锁, 才能完成等待和唤醒的功能.(很牵强有没有......有的...)

死锁产生的条件(都要满足):

  1. 多个用户争夺多个资源, 并且 userCount > resourceCount >= 2
  2. 用户争夺资源的顺序不同
  3. 用户不释放获得的资源

活锁

产生死锁的时候, 如果我们通过 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 等循环