鹰击长空鲸霸海,不试怎知龙与邱!

1.前言

本文介绍锁与临界资源与原子操作的的使用场景。

2.临界资源

2.1 什么是临界资源

临界资源就是被多个线程/进程共享,但在某一时刻只能被一个线程/进程所使用的资源。   下文以一个经典案例(多线程同时进行i++)介绍三种锁,以及cpu指令集支持的原子操作和CAS。

主线程启动后创建十个线程,并将主线程中的count变量当作参数传入子线程中,也就是说十个线程同时操作一个共享资源count,子线程执行10w次count++ 操作,主线程每隔两秒打印一次count的值。下面来看看加锁与不加锁的区别。

2.2 多线程操作临界资源且不加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>

#define THREAD_SIZE 10


//callback
void *func(void *arg) {
int *pcount = (int *) arg;
int i = 0;
while (i++ < 100000) {
(*pcount)++;
usleep(1);
}
}


int main() {
pthread_t th_id[THREAD_SIZE] = {0};

int i = 0;
int count = 0;

for (i = 0; i < THREAD_SIZE; i++) {
int ret = pthread_create(&th_id[i], NULL, func, &count);
if (ret) {
break;
}
}
for (i = 0; i < 100; i++) {
printf("count --> %d\n", count);
sleep(2);
}

}

我们预期count最终是达到100w,为什么在不加锁的时候没有达到预期效果?很明显,count++不是原子操作

image-20240702120141957

2.3 i++不是原子操作,i++对应三条汇编指令

如果i++是原子操作,那么必然会累加到100w,那么i++到底对应着那几步呢?

下面以idx++举例,idx的值是存储在内存里面,首先从内存MOV到eax寄存器里面,然后通过寄存器进行自增,最后再从eax写回到内存中。在编译器不做任何优化的情况下,idx++就对应这三个步骤。

image-20240702120158334

在大多数情况下,程序是这样执行的

image-20240702120209782

但是也会存在下面这两种情况。线程1首先将idx的值读到了寄存器,然后cpu执行线程2,线程2执行完三步骤后,又回到线程1,线程1接着执行剩下的两步。有人可能会想,两个线程不都执行完了吗?有什么不同?

image-20240702120226769

首先,在线程1让出后,线程1的上下文(比如这里的eax),是存储到线程1里面的,线程1恢复后,又将上下文load回去。这里就涉及到yield和resume了,详细介绍看纯c协程框架NtyCo实现与原理的第二节与第三节。 理解了上下文的切换后,就容易理解了,有没有发现,两次++操作,最终会漏加。

image-20240702120244473

所以在多线程中,操作临界资源时,那么这个临界资源是原子的,那么就不用加锁,要么就必须加锁,否在就会出现上述问题!

那么所谓加锁是什么意思?就是将这三条汇编指令变成一个原子操作,只要有一个线程lock加锁了,别的线程就不能执行进来,直到加锁的线程解锁,别的线程才能加锁。那么这三条汇编指令就是原子的了。下面将介绍3中锁以及2个原子操作。

image-20240702120256216

2.4 多线程操作临界资源且加互斥锁

下面来看看两种加锁方式,这两种都可以跑了100w,但是这两种加锁的粒度是不一样的,在这个程序中,谁是临界资源?是pcount,而不是while,所以第二种加锁虽然可以跑通,但是它加锁的粒度太大了,就本程序而言,第二种加锁方式这和单线程跑有什么区别?所以我们要对临界资源加锁,不是临界资源的不加锁,掌控好锁的粒度

1
2
3
4
5
6
7
8
9
10
11
12
//正确加锁
while (i++ < 100000) {
pthread_mutex_lock(&mutex);
(*pcount)++;
pthread_mutex_unlock(&mutex);
}
//错误加锁
pthread_mutex_lock(&mutex);
while (i++ < 100000) {
(*pcount)++;
}
pthread_mutex_unlock(&mutex);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//
// Created by 68725 on 2022/8/3.
//

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>

#define THREAD_SIZE 10
pthread_mutex_t mutex;


//callback
void *func(void *arg) {
int *pcount = (int *) arg;
int i = 0;
while (i++ < 100000) {
pthread_mutex_lock(&mutex);
(*pcount)++;
pthread_mutex_unlock(&mutex);


usleep(1);
}

}


int main() {
pthread_t th_id[THREAD_SIZE] = {0};
pthread_mutex_init(&mutex, NULL);

int i = 0;
int count = 0;

for (i = 0; i < THREAD_SIZE; i++) {
int ret = pthread_create(&th_id[i], NULL, func, &count);
if (ret) {
break;
}
}
for (i = 0; i < 100; i++) {
printf("count --> %d\n", count);
sleep(2);
}

}

可以看到加锁之后,成功达到我们的预期

image-20240702120337410

2.5 多线程操作临界资源且加读写锁

读写锁,顾名思义,读临界资源的时候加读锁,写临界资源的时候加写锁。适用于读多写少的场景。

A线程加了读锁,B线程可以继续加读锁,但是不能加写锁。 A线程加了写锁,B线程不能加读锁,也不能加写锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//
// Created by 68725 on 2022/8/3.
//

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>

#define THREAD_SIZE 10
pthread_rwlock_t rwlock;


//callback
void *func(void *arg) {
int *pcount = (int *) arg;
int i = 0;
while (i++ < 100000) {
pthread_rwlock_wrlock(&rwlock);
(*pcount)++;
pthread_rwlock_unlock(&rwlock);


usleep(1);
}

}


int main() {
pthread_t th_id[THREAD_SIZE] = {0};
pthread_rwlock_init(&rwlock, NULL);


int i = 0;
int count = 0;

for (i = 0; i < THREAD_SIZE; i++) {
int ret = pthread_create(&th_id[i], NULL, func, &count);
if (ret) {
break;
}
}
for (i = 0; i < 100; i++) {
pthread_rwlock_rdlock(&rwlock);
printf("count --> %d\n", count);
pthread_rwlock_unlock(&rwlock);

sleep(2);
}

}

image-20240702120405727

2.6 多线程操作临界资源且加自旋锁

spinlock与mutex一样,mutex在哪里加锁,spinlock就在哪加锁,使用方法是一样的,但是其内部行为不一样。那么mutex和spinlock的区别在哪呢?

互斥锁在获取不到锁时,会进入休眠,等待释放时被唤醒。会让出CPU。 自旋锁在获取不到锁时,一直等待,在等待过程种不会有进程,线程切换。只会一直等,死等。

互斥锁与自旋锁的使用场景下文介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//
// Created by 68725 on 2022/8/3.
//

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>

#define THREAD_SIZE 10
pthread_spinlock_t spinlock;


//callback
void *func(void *arg) {
int *pcount = (int *) arg;
int i = 0;
while (i++ < 100000) {
pthread_spin_lock(&spinlock);
(*pcount)++;
pthread_spin_unlock(&spinlock);

usleep(1);
}
}
int main() {
pthread_t th_id[THREAD_SIZE] = {0};
pthread_spin_init(&spinlock, PTHREAD_PROCESS_SHARED);


int i = 0;
int count = 0;

for (i = 0; i < THREAD_SIZE; i++) {
int ret = pthread_create(&th_id[i], NULL, func, &count);
if (ret) {
break;
}
}
for (i = 0; i < 100; i++) {
printf("count --> %d\n", count);

sleep(2);
}

}

image-20240702120430596

image-20240702120437972

补充:自旋锁会不会死锁

给出我的观点:是锁就会有死锁问题!

嵌套自旋锁导致的死锁

1
2
3
4
5
6
7
8
9
10
spinlock_t lock;

void thread_function() {
spin_lock(&lock); // 第一次获取锁
// ... do some work ...
spin_lock(&lock); // 再次尝试获取同一个锁,会导致死锁
// ... do some more work ...
spin_unlock(&lock);
spin_unlock(&lock);
}

循环依赖导致的死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spinlock_t lock1, lock2;

void thread_A() {
spin_lock(&lock1);
// ... do some work ...
spin_lock(&lock2); // 线程A等待lock2
// ... do some more work ...
spin_unlock(&lock2);
spin_unlock(&lock1);
}

void thread_B() {
spin_lock(&lock2);
// ... do some work ...
spin_lock(&lock1); // 线程B等待lock1
// ... do some more work ...
spin_unlock(&lock1);
spin_unlock(&lock2);
}

在上述示例中,线程A持有lock1并等待获取lock2,而线程B持有lock2并等待获取lock1,这种情况会导致两个线程都无法继续执行,从而导致死锁。

总结

自旋锁本身并不会直接导致死锁,但在特定的使用场景下,如果代码设计不合理,可能会出现死锁。为了避免死锁,开发者应该:

  • 避免嵌套使用同一个自旋锁。
  • 设计时避免循环依赖关系,确保获取锁的顺序一致。
  • 考虑使用其他同步机制,如互斥锁(mutex)或读写锁(rwlock),这些锁可以避免自旋锁的一些问题,特别是在锁持有时间较长的情况下。

2.7 原子操作

我们发现加锁,都是将i++对应的汇编的三个步骤,变成原子性。那么我们有没有办法直接将i++对应的汇编指令,变成一条指令?可以,我们使用xaddl这条指令。

Intel X86指令集提供了指令前缀lock⽤于锁定前端串⾏总线FSB,保证了指令执⾏时不会收到其他处理器的⼲扰。

所谓原子操作,它不是某条具体的指令,它是CPU支持的指令集,都是原子操作。比如说CAS,CAS是原子操作的一种,而不能说原子操作就是CAS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
xaddl -----> Inc
xaddl:第二个参数加第一个参数,并把值存储到第一个参数里
//
// Created by 68725 on 2022/8/3.
//

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>

#define THREAD_SIZE 10

int inc(int *value, int add) {
int old;
__asm__ volatile (
"lock; xaddl %2, %1;"
: "=a" (old)
: "m" (*value), "a" (add)
: "cc", "memory"
);
return old;
}

//callback
void *func(void *arg) {
int *pcount = (int *) arg;
int i = 0;
while (i++ < 100000) {
inc(pcount, 1);

usleep(1);
}

}


int main() {
pthread_t th_id[THREAD_SIZE] = {0};

int i = 0;
int count = 0;

for (i = 0; i < THREAD_SIZE; i++) {
int ret = pthread_create(&th_id[i], NULL, func, &count);
if (ret) {
break;
}
}
for (i = 0; i < 100; i++) {
printf("count --> %d\n", count);

sleep(2);
}

}

cmpxchg-----> CAS CAS:Compare And Swap,先比较,再赋值,翻译成代码就是下面

1
2
3
if(a==b){//Compare
a=c;//Swap
}

CPU的指令集支持了先比较后赋值的指令,叫cmpxchg。正因为CPU执行了这个指令,它才是原子操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
//  Perform atomic 'compare and swap' operation on the pointer.
// The pointer is compared to 'cmp' argument and if they are
// equal, its value is set to 'val'. Old value of the pointer is returned.
inline T *cas (T *cmp_, T *val_)
{
T *old;
__asm__ volatile (
"lock; cmpxchg %2, %3"
: "=a" (old), "=m" (ptr)
: "r" (val_), "m" (ptr), "0" (cmp_)
: "cc");
return old;
}

3.三种锁的api介绍

3.1 互斥锁 mutex

【互斥锁也会自选一会,超过时间的时候OS才会将线程阻塞起来!】

有两个特殊的api,pthread_mutex_trylock 尝试加锁,如果没有获取到锁则返回,而不是休眠。pthread_mutex_timedlock 等待一段时间,超时了还没获取倒锁则返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* Mutex handling.  */

/* Initialize a mutex. */
extern int pthread_mutex_init (pthread_mutex_t *__mutex,
const pthread_mutexattr_t *__mutexattr)
__THROW __nonnull ((1));

/* Destroy a mutex. */
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex)
__THROW __nonnull ((1));

/* Try locking a mutex. */
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));

/* Lock a mutex. */
extern int pthread_mutex_lock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));

#ifdef __USE_XOPEN2K
/* Wait until lock becomes available, or specified time passes. */
extern int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex,
const struct timespec *__restrict
__abstime) __THROWNL __nonnull ((1, 2));
#endif

/* Unlock a mutex. */
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));

3.2 读写锁 rdlock

读写锁适用于多读少写的情况,否则还是用互斥锁。

A线程加了读锁,B线程可以继续加读锁,但是不能加写锁。 A线程加了写锁,B线程不能加读锁,也不能加写锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* Functions for handling read-write locks.  */

/* Initialize read-write lock RWLOCK using attributes ATTR, or use
the default values if later is NULL. */
extern int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,
const pthread_rwlockattr_t *__restrict
__attr) __THROW __nonnull ((1));

/* Destroy read-write lock RWLOCK. */
extern int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock)
__THROW __nonnull ((1));

/* Acquire read lock for RWLOCK. */
extern int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock)
__THROWNL __nonnull ((1));

/* Try to acquire read lock for RWLOCK. */
extern int pthread_rwlock_tryrdlock (pthread_rwlock_t *__rwlock)
__THROWNL __nonnull ((1));

# ifdef __USE_XOPEN2K

/* Try to acquire read lock for RWLOCK or return after specfied time. */
extern int pthread_rwlock_timedrdlock (pthread_rwlock_t *__restrict __rwlock,
const struct timespec *__restrict
__abstime) __THROWNL __nonnull ((1, 2));

# endif

/* Acquire write lock for RWLOCK. */
extern int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock)
__THROWNL __nonnull ((1));

/* Try to acquire write lock for RWLOCK. */
extern int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock)
__THROWNL __nonnull ((1));

# ifdef __USE_XOPEN2K

/* Try to acquire write lock for RWLOCK or return after specfied time. */
extern int pthread_rwlock_timedwrlock (pthread_rwlock_t *__restrict __rwlock,
const struct timespec *__restrict
__abstime) __THROWNL __nonnull ((1, 2));

# endif

/* Unlock RWLOCK. */
extern int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock)
__THROWNL __nonnull ((1));

3.3 自旋锁 spinlock

自旋锁最大的特点是,获取不到锁就一直等待,即使CPU时间片用完了也不会发生切换,死等。而上面两种锁不一样,获取不到就会休眠,让出CPU时间片,切换到其他线程或进程执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Functions to handle spinlocks.  */

/* Initialize the spinlock LOCK. If PSHARED is nonzero the spinlock can
be shared between different processes. */
extern int pthread_spin_init (pthread_spinlock_t *__lock, int __pshared)
__THROW __nonnull ((1));

/* Destroy the spinlock LOCK. */
extern int pthread_spin_destroy (pthread_spinlock_t *__lock)
__THROW __nonnull ((1));

/* Wait until spinlock LOCK is retrieved. */
extern int pthread_spin_lock (pthread_spinlock_t *__lock)
__THROWNL __nonnull ((1));

/* Try to lock spinlock LOCK. */
extern int pthread_spin_trylock (pthread_spinlock_t *__lock)
__THROWNL __nonnull ((1));

/* Release spinlock LOCK. */
extern int pthread_spin_unlock (pthread_spinlock_t *__lock)
__THROWNL __nonnull ((1));

3.4 递归锁

递归锁(Recursive Lock)也称为可重入互斥锁(reentrant mutex),是互斥锁的一种,同一线程对其多次加锁不会产生死锁。递归锁会使用引用计数机制,以便可以从同一线程多次加锁、解锁,当加锁、解锁次数相等时,锁才可以被其他线程获取。

递归锁(Reentrant Lock)在C++中的实现和应用场景,主要是为了处理递归函数或在同一线程中多次进入临界区的情况。递归锁可以避免因多次获取同一个锁而导致的死锁问题。

递归锁的用途

  1. 递归函数中的同步
    • 当一个递归函数需要同步时,使用普通的互斥锁会导致在递归调用时锁无法再次获取,从而导致死锁。递归锁允许同一个线程多次获取锁,因此在递归函数中非常有用。
  2. 复杂逻辑中的多次加锁
    • 在某些复杂的逻辑中,同一个线程可能需要多次进入临界区。使用递归锁可以避免由于多次加锁而导致的死锁问题。
  3. 代码重用和模块化
    • 当函数A调用函数B,而函数B又需要同步时,且函数A已经持有锁,这种情况下使用递归锁可以使代码更加模块化,减少代码耦合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <thread>
#include <mutex>

// 定义递归锁
std::recursive_mutex recMutex;

void recursiveFunction(int count) {
if (count < 0) return;

// 获取递归锁
recMutex.lock();
std::cout << "Lock acquired by thread " << std::this_thread::get_id() << " with count " << count << std::endl;

// 递归调用自身
recursiveFunction(count - 1);

// 释放递归锁
std::cout << "Lock released by thread " << std::this_thread::get_id() << " with count " << count << std::endl;
recMutex.unlock();
}

int main() {
std::thread t1(recursiveFunction, 3);
t1.join();

return 0;
}

4.三种锁的使用场景

比如说读一个文件,就使用mutex。而如果是简单的加加减减操作,就是用spinlock。如果系统提供了原子操作的接口,对于i++这种操作来说,用原子操作更合适。

spinlock:临界资源操作简单/没有发生系统调用/持续时间较短(自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,等待时消耗cpu资源较多,自旋锁一般用于多核的服务器。)

mutex:临界资源操作复杂/发生系统调用/持续时间比较长

  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈
  • 单核处理器

补充:

案例1:单核处理器上的自旋锁讲述

在单核处理器上,虽然同时只有一个进程在执行,但通过操作系统的调度,多个进程仍然可以并发地运行。比如,进程A正在执行,并且持有自旋锁,这时操作系统决定切换到进程B。

  1. 进程A持有自旋锁:进程A获取了自旋锁,并正在执行某些操作。
  2. 上下文切换:操作系统决定切换到进程B,因为进程A的时间片用完了,或者操作系统认为进程B需要运行。
  3. 进程B尝试获取自旋锁:进程B开始运行,并试图获取同一个自旋锁。因为锁被进程A持有,进程B会进入自旋等待状态,不断地检查锁的状态。
  4. 再次切换到进程A:操作系统可能再次切换回进程A,进程A继续运行并最终释放自旋锁。
  5. 进程B获取自旋锁:一旦进程A释放锁,操作系统可能会切换回进程B,进程B就可以成功获取锁并继续执行。

案例2:多核处理器上的自旋锁讲述

初始状态

  • 共享资源初始状态为未被锁定。
  • 自旋锁初始状态为未被持有。

进程A尝试获取锁

  • 进程A运行在CPU核1上,它试图获取自旋锁来访问共享资源。
  • 自旋锁目前未被持有,因此进程A成功获取锁,并进入临界区开始访问共享资源。

进程B尝试获取锁

  • 在进程A持有锁的同时,进程B在CPU核2上运行,也试图获取同一个自旋锁。
  • 由于锁已被进程A持有,进程B进入自旋等待状态,在CPU核2上不断循环检查锁的状态。

进程A释放锁

  • 进程A完成对共享资源的访问,释放自旋锁。
  • 锁的状态变为未被持有。

进程B获取锁

  • 进程B在自旋等待中检测到锁已被释放,立即获取自旋锁。

  • 进程B进入临界区开始访问共享资源。

    【因为始终只有一把自旋锁】

原子操作:使用场景很小,必须需要CPU的指令集支持才行。(原子操作适用于简单的加减等数学运算,属于粒度最小的操作。比如往链表里增加一个结点,可以做出原子操作吗?不行,因为CPU指令集没有同时多个赋值的指令。cas 多线程同时竞争的时候效率并不会特别高,如果互斥锁和自旋锁能满足要求了尽量不要用cas)

5.原子操作的接口

对于gcc、g++编译器来讲,它们提供了⼀组API来做原⼦操作:

1
2
3
4
5
6
7
8
9
10
11
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_lock_test_and_set (type *ptr, type value, ...)
void __sync_lock_release (type *ptr, ...)

详细⽂档⻅:https://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html#AtomicBuiltins

补充

Q:悲观锁和乐观锁的使用场景

悲观锁和乐观锁是并发控制的两种策略,它们在不同的场景下有不同的应用。

  1. 悲观锁

概念:
悲观锁假设数据在多线程环境下非常容易发生冲突,因此在数据访问前会锁定资源,确保其他线程不能访问或修改数据,直到当前操作完成。悲观锁一般通过数据库锁机制或操作系统锁机制实现。

应用场景:

  • 高并发环境下频繁写操作:在高并发的系统中,如果对数据的写操作非常频繁,发生冲突的概率很高,此时使用悲观锁可以有效避免数据的不一致性。
    • 示例: 银行系统中的转账操作。当一个用户正在转账时,需要锁定账户信息,防止其他用户同时对同一账户进行操作,导致数据错误。
  • 长事务:在需要长时间持有锁的事务中,使用悲观锁可以确保数据在整个事务过程中不被其他事务修改。
    • 示例: ERP 系统中,处理订单或库存时需要确保数据的一致性和完整性,因此会在整个事务期间锁定相关资源。
  • 强一致性要求:如果业务场景对数据一致性要求极高,任何时候都不能容忍数据不一致,悲观锁是更好的选择。
    • 示例: 在交易撮合系统中,为了确保每个交易的唯一性和准确性,可能会对关键数据使用悲观锁。

2.乐观锁

概念:
乐观锁假设数据冲突较少,因此不会锁定资源,而是允许多个线程同时操作。乐观锁在提交数据时,会检查是否发生冲突,如果有冲突则回滚并重试。乐观锁通常通过版本号机制或时间戳机制实现。

应用场景:

  • 读多写少的场景:在读操作远多于写操作的系统中,数据冲突的概率较低,使用乐观锁可以提高并发性能,因为它不会对数据加锁,减少了系统开销。
    • 示例: 电子商务系统中的商品浏览功能。大量用户同时浏览商品时,使用乐观锁可以提高系统的并发性,因为读取操作不需要加锁。
  • 短事务:在处理短时间的事务时,乐观锁的性能优势更加明显,因为数据冲突的概率更低,且回滚和重试的代价不高。
    • 示例: 社交网络的用户信息更新。大部分情况下,用户的更新操作是快速完成的,而且冲突的概率较低,因此乐观锁更合适。
  • 低冲突场景:在业务逻辑中,大多数操作对同一数据的冲突很少发生,乐观锁允许多个线程并发操作,提高了系统的吞吐量。
    • 示例: 在线编辑文档的协作系统。多个用户同时编辑不同部分时,乐观锁允许大家并行操作,只有在最终保存时才进行冲突检测。

总结

  • 悲观锁:适用于高并发写操作场景、长事务、以及对数据一致性要求极高的场景。其优点是能有效避免冲突,但缺点是可能导致性能瓶颈和死锁问题。
  • 乐观锁:适用于读多写少、短事务、和低冲突的场景。其优点是高并发性能好,但缺点是在高冲突场景下可能导致大量重试和性能下降。

在实际开发中,选择哪种锁策略应根据业务场景的特点和需求来决定。

学习自:互斥锁、读写锁、自旋锁,以及原子操作指令xaddl、cmpxchg的使用场景剖析