面试多线程八股文十问十答第三期
作者:程序员小白条,个人博客
相信看了本文后,对你的面试是有一定帮助的!
⭐点赞⭐收藏⭐不迷路!⭐
1.介绍一下自旋锁
重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁
注意:
- 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
- 自旋失败的线程会进入阻塞状态
优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源
自旋锁情况:
- 自旋成功的情况:
- 自旋失败的情况:
自旋锁说明:
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
- Java 7 之后不能控制是否开启自旋功能,由 JVM 控制
//手写自旋锁
public class SpinLock {
// 泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " come in");
//开始自旋,期望值为null,更新值是当前线程
while (!atomicReference.compareAndSet(null, thread)) {
Thread.sleep(1000);
System.out.println(thread.getName() + " 正在自旋");
}
System.out.println(thread.getName() + " 自旋成功");
}
public void unlock() {
Thread thread = Thread.currentThread();
//线程使用完锁把引用变为null
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + " invoke unlock");
}
public static void main(String[] args) throws InterruptedException {
SpinLock lock = new SpinLock();
new Thread(() -> {
//占有锁
lock.lock();
Thread.sleep(10000);
//释放锁
lock.unlock();
},"t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
lock.unlock();
},"t2").start();
}
}
2.了解锁消除吗?
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)
逃逸分析
逃逸分析(Escape Analysis),是一种可能减少有效 Java 程序中同步负载和内存堆分配压力的跨全局函数数据流分析算法。通过逃逸分析, Java Hotspot 编译器能够分析出一个新的对象引用范围从而决定是否要将这个对象分配到堆上,逃逸分析的基本行为就是分析对象的动态作用域。
方法逃逸
当一个对象在方法里面被定义后,它可能被外部方法所引用,例如调用参数传递到其他方法中,这种称为方法逃逸。
线程逃逸
当一个对象可能被外部线程访问到,比如:赋值给其他线程中访问的实例变量,这种称为线程逃逸。
通过逃逸分析,编译器对代码的优化
如果能够证明一个对象不会逃逸到到方法外或线程外(其他线程方法或者线程无法通过任何方法访问该变量),或者逃逸程度比较低(只逃逸出方法而不逃逸出线程)则可以对这个对象采用不同程度的优化:
1、栈上分配(Stack Allocations)完全不会逃逸的局部变量和不会逃逸出线程的对象,采用栈上分配,对象就会跟随方法的结束自动销毁。以减少垃圾回收器的压力。
2、标量替换(Scalar Replacement)有个对象可能不需要作为一个连续的存储结果存储也能被访问到,那么对象的部分(或者全部)可以不存储在内存,而是存储在 CPU 寄存器中。
3、同步消除(Synchronization Elimination)如果一个对象发现只能在一个线程访问到,那么这个对象的操作可以考虑不同步。
3.锁粗化大致了解哪些?
对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化
如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部
- 一些看起来没有加锁的代码,其实隐式的加了很多锁:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
- String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为 StringBuffer 对象的连续 append() 操作,每个 append() 方法中都有一个同步块
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以
4.重量级锁会发生线程阻塞,那么阻塞之后CPU会对它进行怎样的处理?
当线程在使用重量级锁时发生阻塞,CPU 会将该线程置于一种等待状态,通常是通过操作系统的线程调度机制实现的。具体的处理方式取决于操作系统和 JVM 的实现,但一般来说,阻塞的线程会进入休眠状态,不再占用 CPU 资源,直到以下条件之一发生:
锁的拥有者线程释放锁:如果锁的拥有者线程执行完了它的同步代码块或方法,并释放了锁,等待的线程将被唤醒,有机会再次竞争锁。
超时等待:等待线程可以设置一个超时时间,如果在超时时间内锁没有被释放,线程将被唤醒,可以继续尝试获取锁或执行其他操作。
中断:等待线程可以被外部线程中断,如果发生了线程中断,等待线程也会被唤醒。
其他通知机制:有时候,等待线程可以通过其他通知机制被唤醒,如 Condition 对象的 signal() 或 signalAll() 方法。
在这个过程中,CPU 不会一直持续检查等待线程是否可以获得锁,而是通过操作系统提供的等待/唤醒机制来管理线程的状态。这有助于节省 CPU 资源,因为被阻塞的线程不会占用处理器时间,直到它有机会获得锁或被唤醒。
5.锁被释放之后,队列中阻塞线程获取锁的流程是怎么样的?
当锁被释放后,队列中的阻塞线程会竞争锁的获取。这个过程通常由操作系统的线程调度机制和 JVM 的锁实现来管理。以下是阻塞线程获取锁的典型流程:
竞争锁:一旦锁的持有者线程释放了锁,阻塞线程队列中的线程会竞争锁。操作系统的线程调度器会选择其中一个线程来执行。通常,线程的选择是非确定性的,即哪个线程会被选中是不可预测的。
锁竞争:竞争锁的线程会尝试获取锁。如果竞争成功,线程将成为锁的新持有者,可以继续执行同步代码块或方法。
如果竞争失败,线程将进入一个阻塞状态,等待重新竞争或者通过操作系统的线程调度器重新分配 CPU 时间片。
这个过程涉及到操作系统级别的线程调度和竞争,所以具体的行为会受到操作系统和 JVM 实现的影响。竞争锁时,通常会采用一些策略来提高公平性和效率。例如,有的 JVM 实现可能会采用公平锁策略,即先进入队列的线程会更有机会获得锁,以减少线程饥饿的情况。其他实现可能会采用非公平锁策略,以提高性能。
6.线程池任务提交,比如调用execute或submit API之后的流程有了解吗
注意:execute() 执行任务时,如果有异常没有被捕获会直接抛出
submit() 执行任务时,会吞并异常,除非调用get() 获取计算结果,当抛出异常时会捕获异常
Executors (内部使用AbstractExecutorService的子类DelegatedExecutorService):
执行execute(),最终是调用ThreadPoolExecutor的execute方法
执行submit(),会通过newTaskFor创建FutureTask,最终还是执行ThreadPoolExecutor的execute方法
7.Synchronized四种状态,哪些可访问系统资源?
其实就是问用户态和内核态,只有重量级锁是内核态可以访问系统资源。