061、为什么要使用多线程?
- 从计算机层面考虑,线程可以看做是轻量级进程,线程间切换和线程调度的成本远小于进程,此外,现在是多核 CPU 的时代,多个线程可以并发执行,极大降低了线程上下文切换的开销。
- 从互联网的发展趋势来看,现在很多系统都要求百万级甚至千万级并发量,而多线程正式开发高并发系统的基础,利用好多线程机制可以极大地提高系统的并发能力和整体性能。
062、使用多线程可能会带来什么问题?
并发编程的目的是提高程序的执行效率,但如果使用不当的话是会带来很多问题的,如内存泄漏、内存溢出、死锁、线程不安全等。
063、线程的生命周期和状态?
在 Java 中,线程的整个生命周期包含以下六种状态。
有一点需要特别注意:Java 将操作系统中的就绪和运行两种状态统一称为运行状态。
线程在整个生命周期中,不会固定的处于某种状态,而是会随着代码的执行在不同的状态之间进行切换,如下图所示。
064、什么是上下文切换?
大多数情况下,线程的数量是多余 CPU 的核心数的,每个 CPU 核心在同一时间只能供一个线程使用,为了让所有线程都能得到有效执行,操作系统采取的策略是为每个线程分配时间片轮转执行,当一个线程的时间片用完之后就会处于就绪状态等待下一次时间片的分配,线程从保存当前执行状态到再加载执行的过程就是一次上下文切换。
065、什么是死锁?
多个线程同时阻塞,并且都在等待彼此释放资源,导致线程被无限期阻塞,程序不能正常终止,这种现象就是死锁。
死锁的产生必须具备的四个条件?
- 互斥条件:一个资源在任意时刻只能被一个线程使用。
- 请求与保持条件:线程在阻塞的时候对已获得的资源保持不放。
- 不可剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能由自己释放。
- 循环等待条件:若干线程之间形成一种头尾相连的循环等待资源关系。
如何避免死锁?
只需要破坏产生死锁的四个必要条件之一即可。
066、sleep 和 wait 有什么异同?
// wait 有三个重载的方法,后两个实际上都是调用第一个 native 的 wait 方法
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
// sleep 有两个重载的静态方法,第二个实际上是调用第一个 native 的 sleep 方法
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
- 两者的共同点是都可以用于暂停线程的执行。
- sleep 通常用于暂停当前线程,sleep 方法不会释放锁,当 sleep 方法执行完后,当前线程会自动苏醒,然后继续执行。
- wait 通常用于线程间的交互和通信,wait 方法会释放锁,如果调用的是有参 wait 方法,超时后线程会自动苏醒,然后去参与锁的竞争;如果调用的是无参 wait 方法,线程不会自动苏醒,需要其他线程调用同一个对象上的 notify() 或 notifyAll() 方法才能唤醒当前线程。
- 两者最本质的区别是 sleep 方法不会释放锁,而 wait 方法会释放锁。
067、为什么调用 start() 方法时会执行 run() 方法?为什么不能直接调用 run() 方法?
调用 start() 方法后,会启动一个新线程并使线程进入就绪状态,当分配到时间片后就会开始运行,start() 方法会进行线程的相应准备工作,然后自动执行 run() 方法的内容;但是如果直接执行 run() 方法,会把 run() 方法当做 main 线程下的一个普通方法去执行,并不会启动一个新的线程去执行。
068、为什么在 Java 6 之前 sychronized 效率低下?
在 Java 6 之前的版本中,synchronized 属于重量级锁,它是基于监视器锁 monitor 实现的,而监视器锁是依赖于底层操作系统的 Mutex Lock
来实现的,因此 Java 的线程是映射到底层操作系统的原生线程上的,如果要挂起或唤醒一个线程,都需要操作系统来帮忙,而操作系统实现线程之间的切换需要从用户态转换到核心态,这个转换耗时相对较长,时间成本比较高,因此在 Java 6 之前 sychronized 效率低下。
069、Java 6 及之后 sychronized 有了哪些改进?
为了减少获得锁和释放锁带来的性能消耗,Java 6 引入了偏向锁和轻量级锁,对象头的 markword 会记录第一个获得锁的线程的 ID,以后这个线程只需要简单判断一下 ID 就可以成功获取锁,这是偏向锁,如果有其他线程来竞争锁,偏向锁就升级为轻量级锁,线程会通过 CAS 操作来竞争轻量级锁,竞争失败则会进行自旋操作,默认情况下自旋 10 次之后轻量级锁就会升级为重量级锁。
锁只能升级,不能降级。
偏向锁的优缺点?
优点:加锁和解锁不需要额外的消耗,和执行非同步方法只有纳秒级的差距;缺点:如果线程之间存在竞争,会带来额外的锁撤销的消耗;适用场景:只有一个线程访问台同步块的场景。
轻量级锁的优缺点?
优点:竞争的线程不会阻塞,提高了程序的响应速度;缺点:不断进行自旋操作会带来额外的 CPU 消耗;适用场景:追求响应速度、线程数量少、同步块执行时间短、读多写少(读多写少时冲突一般比较少)的场景。
重量级锁的优缺点?
优点:没有自旋操作,不会产生额外的 CPU 消耗;缺点:会造成线程阻塞,响应时间慢;适用场景:追求吞吐量、线程数量多、同步块执行时间长、写多读少(写多读少冲突一般比较多)的场景。
070、谈谈你对 synchronized 的理解?
synchronized 解决的是多个线程之间访问资源的同步性问题,它可以保证被它修饰的方法或代码块在任意时刻只有一个线程在执行。然后再说一下 sychronized 在 Java 6 前后的实现,如前两题所示。