1.概要
乐观锁(Optimistic Locking)
乐观锁的核心思想是假设在大多数情况下,资源不会发生冲突,因此允许多个用户或线程同时读取和修改资源。只有在真正发生冲突的时候才会进行冲突解决。
乐观锁的工作原理如下:
- 版本标识或时间戳:在资源中引入一个版本标识(Version)或时间戳(Timestamp)字段,用于记录资源的修改版本或修改时间。
- 读取资源:当一个用户或线程要读取资源时,会获取当前的版本号或时间戳,并将其保存在本地。
- 修改资源:当用户或线程要修改资源时,它会检查本地保存的版本号或时间戳与资源当前的版本号或时间戳是否匹配。如果匹配,表示没有其他用户或线程在修改这个资源,可以安全地进行修改。
- 冲突检测:如果本地的版本号与资源的版本号不匹配,表示资源已经被其他用户或线程修改,那么当前用户或线程需要处理冲突。通常的处理方式包括放弃修改、重新读取资源并重新应用修改,或者采用其他冲突解决策略。
乐观锁的优点是它不会在资源读取时进行锁定,允许多个用户并发地读取资源,提高了系统的并发性能。但是,如果冲突频繁发生,可能需要增加冲突解决的复杂性,以及重新读取和应用修改可能会导致性能损失。
乐观锁通常用于情况下,其中资源冲突的概率相对较低,例如读多写少的情况。另一方面,悲观锁则是一种更保守的并发控制机制,它会在读取资源时立即锁定,以确保不会发生冲突,但可能会降低系统的并发性能。选择哪种锁取决于应用程序的需求和性能要求。
悲观锁(Pessimistic Locking)
在多个用户或线程访问共享资源时采取一种悲观的态度,即假定在任何时刻都会发生冲突,因此在访问资源之前会将其锁定,以确保只有一个用户或线程能够访问资源,从而防止冲突和数据不一致性。
悲观锁的主要特点如下:
- 锁定资源:在用户或线程访问资源之前,悲观锁会锁定资源,阻止其他用户或线程对其进行读取或修改。这可以通过数据库中的行级锁、表级锁、文件锁或其他机制来实现,具体取决于应用程序和数据存储方式。
- 保守策略:悲观锁采用一种保守的策略,即假定并发访问会导致冲突,因此在访问资源时会进行锁定。这可以确保资源的一致性,但也可能导致性能问题,特别是在高并发环境下。
- 阻塞等待:如果一个用户或线程已经锁定了资源,其他试图访问相同资源的用户或线程可能需要等待,直到锁被释放为止。这可能导致竞争和延迟。
- 事务性:悲观锁通常与事务一起使用,以确保在事务中对资源进行读取和修改时不会被其他事务干扰。
- 适用场景:悲观锁通常用于资源冲突的概率较高的情况,或者当资源的一致性是至关重要的情况下。例如,在银行系统中,对于一个银行账户的并发访问,悲观锁可以确保不会出现超支或其他不一致的情况。
悲观锁是一种保守的并发控制机制,通过锁定资源以确保数据一致性,但可能导致性能问题和竞争。在选择锁定策略时,应根据应用程序的需求和性能要求来决定是否使用悲观锁。
差异
乐观锁和悲观锁是两种不同的并发控制机制,它们用于管理多个用户或线程同时访问共享资源的情况,但它们的工作方式有很大的区别。
- 态度差异:
- 乐观锁:假设在大多数情况下不会发生冲突,允许多个用户或线程同时读取和修改资源,只有在发生冲突时才会进行处理。
- 悲观锁:假设在任何时刻都会发生冲突,因此在访问资源之前会将其锁定,以确保只有一个用户或线程能够访问资源。
- 锁定时机:
- 乐观锁:在资源访问时不进行锁定,只在提交修改时才检查冲突。
- 悲观锁:在访问资源之前就会进行锁定,以防止其他用户或线程同时访问。
- 性能影响:
- 乐观锁:通常具有较高的并发性能,因为它允许多个用户或线程同时读取资源,只在冲突发生时才会引入竞争和延迟。
- 悲观锁:可能导致性能问题,因为它在访问资源时会锁定,其他用户或线程需要等待锁的释放,可能会引入竞争和延迟。
- 冲突解决方式:
- 乐观锁:发生冲突时,通常需要重新读取资源并重新应用修改,或者采用其他冲突解决策略,如版本号比对。
- 悲观锁:在资源访问之前就会锁定资源,因此冲突的概率较低。冲突通常通过等待其他锁定的释放来解决。
- 适用场景:
- 乐观锁:通常适用于资源冲突的概率较低的情况,例如读多写少的情况。
- 悲观锁:通常适用于资源冲突的概率较高,或者当资源的一致性是至关重要的情况下。
乐观锁和悲观锁适用于不同的应用场景。选择哪种策略取决于应用程序的需求、性能要求以及对一致性的要求。乐观锁通常用于提高并发性能,而悲观锁用于确保数据的强一致性。
2.详细内容
如何实现一个乐观锁?
以下代码仅供参考。
class Program
{
private static int sharedValue = 0;
private static int version = 0;
static void Main(string[] args)
{
// 模拟两个线程尝试同时更新共享值
Task t1 = Task.Run(() => UpdateSharedValue(1));
Task t2 = Task.Run(() => UpdateSharedValue(2));
Task.WaitAll(t1, t2);
Console.WriteLine($"Final Shared Value: {sharedValue}");
}
static void UpdateSharedValue(int id)
{
int localVersion;
localVersion = Interlocked.CompareExchange(ref version, 0, 0); // 获取当前版本号
Console.WriteLine($"Thread {id}: Current Version: {localVersion}");
// 模拟一些计算或操作
Thread.Sleep(100);
// 检查版本是否仍然匹配
if (localVersion == version)
{
Interlocked.Increment(ref sharedValue); // 更新共享值
Interlocked.Increment(ref version); // 增加版本号
Console.WriteLine($"Thread {id}: Value Updated. New Value: {sharedValue}, New Version: {version}");
}
else
{
Console.WriteLine($"Thread {id}: Update Failed due to version mismatch.");
}
}
}
如何实现一个悲观锁?
这里使用lock
语句来锁定共享资源,以确保在一个线程访问资源时其他线程无法同时访问。
class Program
{
static object resourceLock = new object(); // 定义一个锁对象,用于锁定共享资源
static int sharedResource = 0; // 共享资源
static void Main(string[] args)
{
Console.WriteLine("Starting updates...");
Thread t1 = new Thread(UpdateResource);
Thread t2 = new Thread(UpdateResource);
t1.Start("Thread 1");
t2.Start("Thread 2");
t1.Join();
t2.Join();
Console.WriteLine("Updates completed.");
Console.WriteLine($"Final shared resource value: {sharedResource}");
}
static void UpdateResource(object threadName)
{
Console.WriteLine($"{threadName} is updating the resource.");
// 使用锁来锁定共享资源
lock (resourceLock)
{
// 模拟一些工作
Thread.Sleep(1000);
// 更新共享资源
sharedResource++;
Console.WriteLine($"{threadName} updated the resource to {sharedResource}");
}
Console.WriteLine($"{threadName} finished updating the resource.");
}
}
示例中使用了resourceLock
的对象来锁定共享资源sharedResource
。两个线程(Thread 1 和 Thread 2)尝试同时更新共享资源,但只有一个线程可以在某一时刻获得锁,进而访问和更新共享资源。其他线程必须等待锁的释放。