面试中的“锁”

总结面试中常问的“锁”。

锁,顾名思义就是锁住一些资源,只有我们拿到钥匙的时候,才能操作锁住的资源,在多线程的情况下,保证操作数据的正确性与一致性。

“锁”的使用场景很多,这里的资源也是一种广泛的概念,比如CPU多核共享主存,多进程或多线程共享的硬件资源和数据,数据库中的数据,分布式环境下共享的资源,等等。

由此可见,在硬件和软件层面都会实现各种“锁”机制。在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制;OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等等)。同时在编程语言级别,也要提供“锁”的机制来支持并发编程和实现多线程应用程序。

✏️ 并发编程中的“锁”

在并发的场景中,逻辑上来说,常见的锁有:悲观锁、乐观锁、独占锁、共享锁、公平锁、非公平锁、分布式锁、自旋锁……需要说明的是,数据库本质上是一种多线程的应用程序,因此数据库中的“锁”是指具体的实现。

面试中问“锁”的几个切入点:

  1. 操作系统中的“锁”

  2. 多线程编程中,各种“锁”的特点和区别

  3. 特定编程语言中提供的“锁”机制

  4. 具体的数据库中实现的“锁”

  5. 分布式场景下的“锁”

“公平锁”与“非公平锁”

  • 公平锁:指线程在等待获取同一个锁的时候,是严格按照申请锁的时间顺序来进行的,这就意味着在程序正常运作的时候,不会有线程执行不到,而被“饿死”,但是也需要额外的机制来维护这种顺序,所以效率相对于非公平锁会差点。

  • 非公平锁:概念跟“公平锁”恰恰相反,随机线程获取锁,效率相对高。

对于公平锁,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。对于非公平锁, 一上来就直接尝试占有锁,如果尝试失败,才会加入到等待队列。只要线程进入了等待队列,队列里面依然是FIFO的原则,跟公平锁的顺序是一样的。

“重入锁(递归锁)”与“不可重入锁(自旋锁)”

重入/递归,不可重入/自旋,虽然名字不同,但是确实是同一种锁,只是从锁的表现跟实现方式的角度来命名而已。

  • 重入锁:指的是同一线程外层函数获得所之后,内层递归函数任然能获取该锁的代码,在同一个线程外层方法获取的时候,在进入内层方法会自动获取锁。也就是说线程可以进入任何一个它已经拥有的锁所同步着的代码块。ReentrantLockSynchronize 就是一个典型的非公平重入锁。

  • 不可重入锁: 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。

“悲观锁”与“乐观锁”

  • 悲观锁:正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

  • 乐观锁:乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。

实现:

在数据库中,悲观锁的流程如下:

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。

  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

  4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。

数据库中的行锁,表锁,读锁,写锁,以及syncronized实现的锁均为悲观锁。

乐观锁不需要借助数据库的锁机制,乐观锁的实现包括两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap),当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

“共享锁”与“排他锁/独占锁”——读写锁

悲观锁主要分为共享锁或排他锁

  • 共享锁【Shared lock】:又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

  • 排他锁【Exclusive lock】:又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

互斥锁”和“条件锁”

  • 互斥锁:多任务操作中,同时运行的多个任务可能需要访问同一个资源,那么线程中就有这么一把锁,限制对共享资源的访问。互斥锁只有两种状态,上锁和不上锁。强调原子性和唯一性。

  • 条件锁:条件锁强调的是条件等待而不是互斥,条件锁会阻塞当前线程,直到某个条件成立才会继续向下执行。NSCondition和信号量都可以实现条件锁。

✏️ 分布式锁

实现方式:

  1. 基于数据库实现分布式锁;

  2. 基于缓存(Redis等)实现分布式锁;

  3. 基于Zookeeper实现分布式锁;

Last updated