1、如何设计锁?
设计一把锁,首先要解决以下四个问题:
(1)如何表示锁的状态?
# 记录锁的状态,无锁、有锁、可重入锁的次数
int states;
一般来说,锁只有两种状态,一种是无锁,一种是有锁。因此,用布尔值来表示锁的状态即可,但是在JUC包中,却是int类型表示锁的状态,这是为何?
在JUC包中,之所以用int 类型表示锁状态,是因为可重入锁的缘故。虽然定义多个布尔值类型同样可以解决可重入锁的问题,但是这就让代码冗余了,因此,选择int类型不禁是要记录无锁与有锁的状态,还要记录可重入的次数。
(2)如何保证多线程抢锁线程安全?
层面 | 机制 |
---|---|
源码 | CAS |
JVM | 屏障机制 |
硬件 | lock cmpxchg |
要想回答这个问题,得从三个方面来谈,锁的源码,JVM层面以及CPU硬件层面。
首先是源码,在源码中,就是CAS操作。其次则是JVM层,JVM提供了四种屏障机制,loadstore、storeload、loadload、storestore。最后是CPU硬件层面,以x64的英特尔的来说,它提供了一个指令lock cmpxchg。
(3)如何处理获取不到锁的线程?
处理方式:自旋、阻塞、自旋+阻塞。
自旋,线程获取不到锁,就一直占用CPU进行等待,直到获得锁;这种方式比较占用CPU资源,在CPU核数多线程并发高的情形下,会直接导致性能问题;因此,自旋的适用场景,线程争用少且代码量少的临界区。
阻塞,线程尝试获取锁失败达到一定次数后,就告诉OS让其进行阻塞,从而进入到等待队列,直到被唤醒。
(4)如何释放锁?
自旋:自个抢锁;阻塞:等待唤醒。
2、自旋与阻塞,哪一种锁等待方式更优?
自旋与阻塞无优劣之分,它们有各自的适用场景。
自旋,即线程抢不到锁,占用CPU自旋,直到抢到锁;阻塞,即线程抢不到锁,就让OS阻塞其,直到被唤醒。
自旋适用于执行步骤少且快的操作,自旋一会儿就能马上获得锁,这样就不会占用太多的CPU的资源。这是自旋的优点。
当CPU个数增加且线程数增加的情况下,自旋占用CPU,浪费资源,其的优点就会退化成缺点。在这种场景下,阻塞比自旋更有优势。
3、公平锁与非公平锁,哪个性能更优?
公平锁,线程先查看等待队列中有没有线程,如果有,那么就进入等待队列,如果没有,则进行抢锁。这一过程,就会产生线程上下文切换时间和调度延迟的时间。
非公平锁,线程不管等待队列中有没有线程,率先抢锁,抢锁失败,再进入等待队列。这一过程,同样会产生线程上下文切换时间和调度延迟的时间,但是并非绝对的,因为一旦抢锁成功,就会有这两个时间了。
因此,非公平锁的性能优于公平锁。