并发编程中常见面试题

304 阅读4分钟

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

前言

这篇文章写的有点乱,像是一个大杂烩,主要就是为了面试扫盲用的,更深入的理解可以单独再看哈。

sleep VS wait

共同点:wait() wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

方法的归属不同

sleep(long) 是 Thread 的静态方法。

而 wait() wait(long) 都是 Object 的成员方法,每个对象都有。

醒来时机不同

执行 sleep(long) 和 wait(long) 的线程都会等待相应毫秒后醒来。

wait() 和 wait(long) 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去。

它们都可以被打断唤醒。

锁特征不同

wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制

wait 方法执行后会释放对象锁,允许其它线程获得该对象锁,而 sleep 方法如果在 synchronized 代码块中执行,并不会释放对象锁。

没有获得锁的时候,不能调用 wait,会报错。在同步代码块内,调用 wait 会释放锁,sleep 不会释放(我不执行,你也别想执行)。

Lock VS synchronized

语法层面

synchronized 是关键字,源码在 JVM 中,用 c ++ 实现、Lock 是接口,源码由 JDK 实现,用 Java 语言实现。

使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁。

功能层面

二者均属于悲观锁,都具备基本的互斥,同步,锁重入功能。

Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量。

Lock 有适合不同场景的实现,如 ReentrantLock、ReentrantReadWriteLock。

性能层面

在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁、性能不错。

在竞争激烈时,Lock 的实现通常会提供更好的性能。

ps. 实际上它两个都不用……

悲观锁和乐观锁

悲观锁:获取锁失败的线程,会停下来等待。

乐观锁:无需加锁,失败的线程不需要停止,不断重试直至成功。

悲观锁的代表是 synchronized 和 Lock 锁。其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】线程从运行到阻塞,再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能。实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已经被占用,都会做几次重试操作,减少阻塞的机会。

乐观锁的代表是 AtomicInteger 使用 CAS 来保证原子性。其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它的线程不需要停止,不断重试直至成功】由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换,它需要多核 CPU 支持,且线程数不应超过 CPU 核数。

Unsafe 类与 CAS

修改共享变量时,保证原子性。Unsafe 里面有很多 CAS 方法,compare and swap 也有人说 compare and set 呃呃,理解原理最重要。

CAS 操作包含三个操作数 —— 内存值(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

使用 while 循环就是一个乐观锁的应用。while + CAS + volatile 保证了共享变量的安全。

CAS 存在的问题,1 只能操作一个变量、2 循环开销大、3 ABA 问题。简单说一下 ABA 问题,一个变量被先加后减,在另外一个线程看来,数据好像没有变,会成功更新为新的值,然而数据其实已经被更新两次了,怎么办呢?添加一个版本信息就好。