常见锁类型介绍
下面结合代码详细介绍 Mutex、RW Lock、Futex、自旋锁、信号量、条件变量 和 synchronized,并分析它们的适用场景、特点以及为什么这些锁适用于特定场景。我们将从锁的实现机制和性能特点出发,解释其适用性。
1. Mutex(互斥锁)
代码示例
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 定义一个互斥锁
int shared_data = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock(); // 加锁
++shared_data; // 访问共享资源
mtx.unlock(); // 解锁
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value of shared_data: " << shared_data << std::endl;
return 0;
}
特点
-
独占访问:同一时间只有一个线程可以持有锁。
-
阻塞等待:如果锁被占用,请求锁的线程会被阻塞,直到锁被释放。
-
不可重入:标准 mutex 不可重入,但可以通过递归 mutex 实现重入。
适用场景
-
保护临界区:确保同一时间只有一个线程访问共享资源。
-
锁持有时间较长:适用于锁持有时间较长的场景,因为阻塞等待不会浪费 CPU 资源。
为什么适用
-
实现机制:Mutex 通过操作系统提供的阻塞机制(如 futex)实现,线程在锁被占用时会进入睡眠状态,不会占用 CPU。
-
性能特点:适合锁持有时间较长的场景,因为线程睡眠不会浪费 CPU 资源。
2. RW Lock(读写锁)
代码示例
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex rw_mtx; // 定义一个读写锁
int shared_data = 0;
void reader(int id) {
for (int i = 0; i < 5; ++i) {
rw_mtx.lock_shared(); // 加读锁
std::cout << "Reader " << id << " read: " << shared_data << std::endl;
rw_mtx.unlock_shared(); // 解读锁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void writer(int id) {
for (int i = 0; i < 5; ++i) {
rw_mtx.lock(); // 加写锁
++shared_data;
std::cout << "Writer " << id << " wrote: " << shared_data << std::endl;
rw_mtx.unlock(); // 解写锁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::thread readers[3];
std::thread writers[2];
for (int i = 0; i < 3; ++i) readers[i] = std::thread(reader, i);
for (int i = 0; i < 2; ++i) writers[i] = std::thread(writer, i);
for (int i = 0; i < 3; ++i) readers[i].join();
for (int i = 0; i < 2; ++i) writers[i].join();
return 0;
}
特点
-
读共享:多个读线程可以同时持有读锁。
-
写独占:写锁是独占的,写线程持有锁时,其他线程不能获取读锁或写锁。
适用场景
-
读多写少:适用于读操作远多于写操作的场景,如缓存系统、数据库。
-
读操作不修改共享资源:读操作不会修改共享资源,因此可以并发执行。
为什么适用
-
实现机制:读写锁通过计数器跟踪读锁的数量,写锁需要等待所有读锁释放。
-
性能特点:在读多写少的场景中,读写锁可以显著提高并发性能。
3. 自旋锁(Spinlock)
代码示例
#include <iostream>
#include <thread>
#include <atomic>
std::atomic_flag spinlock = ATOMIC_FLAG_INIT; // 定义一个自旋锁
int shared_data = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
while (spinlock.test_and_set(std::memory_order_acquire)); // 自旋等待
++shared_data; // 访问共享资源
spinlock.clear(std::memory_order_release); // 释放锁
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value of shared_data: " << shared_data << std::endl;
return 0;
}
特点
-
忙等待:线程不会进入睡眠状态,而是不断检查锁的状态。
-
低延迟:适用于锁持有时间非常短的场景,避免了线程切换的开销。
-
高 CPU 占用:如果锁持有时间较长,会导致 CPU 资源的浪费。
适用场景
-
锁持有时间非常短:适用于锁持有时间非常短的场景,如内核中的中断处理程序。
-
多核处理器上的高并发场景:在多核处理器上,自旋锁可以避免线程切换的开销。
为什么适用
-
实现机制:自旋锁通过忙等待实现,不会让线程进入睡眠状态。
-
性能特点:适合锁持有时间非常短的场景,因为忙等待的代价低于线程切换的开销。
4. 信号量(Semaphore)
代码示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
class Semaphore {
public:
Semaphore(int count = 0) : count(count) {}
void acquire() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]{ return count > 0; });
--count;
}
void release() {
std::lock_guard<std::mutex> lock(mtx);
++count;
cv.notify_one();
}
private:
std::mutex mtx;
std::condition_variable cv;
int count;
};
Semaphore sem(2); // 初始化信号量,允许 2 个线程同时访问
void task(int id) {
sem.acquire();
std::cout << "Task " << id << " is running!" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
sem.release();
}
int main() {
std::thread t1(task, 1);
std::thread t2(task, 2);
std::thread t3(task, 3);
t1.join();
t2.join();
t3.join();
return 0;
}
特点
-
计数机制:信号量维护一个计数器,用于控制对共享资源的访问数量。
-
阻塞等待:如果计数器为 0,线程会阻塞等待。
适用场景
-
限制并发访问数量:适用于资源池管理(如线程池、连接池)。
-
生产者-消费者模型:用于控制生产者和消费者的并发数量。
为什么适用
-
实现机制:信号量通过计数器和条件变量实现,可以灵活控制并发访问数量。
-
性能特点:适合需要限制并发访问数量的场景。
5. 条件变量(Condition Variable)
代码示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件满足
std::cout << "Worker is running!" << std::endl;
}
int main() {
std::thread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 通知等待的线程
t.join();
return 0;
}
特点
-
条件同步:用于线程间的条件同步。
-
阻塞等待:线程会阻塞等待条件满足。
适用场景
-
复杂的同步需求:如生产者-消费者模型。
-
线程间协作:适用于需要线程间协作的场景。
为什么适用
-
实现机制:条件变量通过阻塞和唤醒机制实现线程间同步。
-
性能特点:适合需要复杂同步的场景。
6. synchronized(Java 中的锁机制)
代码示例
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
特点
-
内置锁:Java 中的
synchronized
关键字提供了一种简单的锁机制。 -
可重入:同一个线程可以多次获取锁。
适用场景
-
简单的同步需求:适用于不需要复杂锁机制的场景。
-
单线程重入:适用于需要重入锁的场景。
为什么适用
-
实现机制:
synchronized
通过 JVM 内置的锁机制实现,简单易用。 -
性能特点:适合简单的同步需求。
总结
锁类型 | 特点 | 适用场景 | 为什么适用 |
---|---|---|---|
Mutex | 独占访问,阻塞等待 | 保护临界区,锁持有时间较长的场景 | 通过阻塞机制实现,适合锁持有时间较长的场景 |
RW Lock | 读共享,写独占 | 读多写少的场景,如缓存系统、数据库 | 通过计数器实现读共享,适合读多写少的场景 |
自旋锁 | 忙等待,不阻塞线程 | 锁持有时间非常短的场景,多核处理器上的高并发场景 | 通过忙等待实现,适合锁持有时间非常短的场景 |
信号量 | 控制对共享资源的访问数量 | 资源池管理,限制同时访问共享资源的线程数量 | 通过计数器和条件变量实现,适合需要限制并发访问数量的场景 |
条件变量 | 线程间条件同步 | 复杂的同步需求,如生产者-消费者模型 | 通过阻塞和唤醒机制实现,适合需要复杂同步的场景 |
synchronized | 内置锁,简单易用 | 简单的同步需求,单线程重入 | 通过 JVM 内置锁机制实现,适合简单的同步需求 |
通过代码示例和实现机制的分析,可以更好地理解各种锁的适用场景和性能特点。选择合适的锁类型取决于具体的应用场景和性能需求。