乐观锁与悲观锁简介

主要介绍乐观锁以及悲观锁的概念以及实现方式和适用场景。

乐观锁和悲观锁

1 基本概念

  • 乐观锁 临界区资源资源被修改的概率低。因此在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

  • 悲观锁 临界区资源资源被修改的概率非常大。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

2 实现方式

悲观锁的实现方式是加锁; 乐观锁的实现方式有两种,CAS机制和版本号机制;

2.1 CAS(Compare And Swap)

CAS操作包括了3个操作数:

  • 需要读写的内存位置(V)
  • 进行比较的预期值(A)
  • 拟写入的新值(B)

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

2.2 版本号机制

版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。 需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。

3 优缺点和适用场景

3.1 功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

3.2 竞争的激烈程度

如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

  • 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
  • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

4 CAS有哪些缺点

4.1 ABA问题

4.2 高竞争下的开销问题

在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。

4.3 功能限制

CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着: (1)原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全; (2)当涉及到多个变量(内存值)时,CAS也无能为力。除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。

5 两个互斥锁实现读写锁

下面代码说明了一个问题,即一个线程加锁,而另一个线程解锁,是可行的,只是这种情况容易导致死锁。必须保证第二次加锁时,已经调用了解锁了。

pthread_mutex_t    r_mutex;
pthread_mutex_t     w_mutex;
int reader = 0;

pthread_mutex_init(&w_mutex, NULL);
pthread_mutex_init(&r_mutex, NULL);

//加写锁
pthread_mutex_lock(&w_mutex);

//解写锁
pthread_mutex_unlock(&w_mutex);

//加读锁
pthread_mutex_lock(&r_mutex);
if (reader == 0)
    pthread_mutex_lock(&w_mutex);
reader++;
pthread_mutex_unlock(&r_mutex);

//解读锁
pthread_mutex_lock(&r_mutex);
reader--;
if (reader == 0)
    pthread_mutex_unlock(&w_mutex);
pthread_mutex_unlock(&r_mutex);
Share: X (Twitter) Facebook LinkedIn