悲观锁和乐观锁
乐观锁和悲观锁是并发控制的两种不同策略,主要区别如下:
什么是悲观锁?
悲观锁
:
悲观锁的基本思想是假设并发访问会导致冲突,因此在访问共享资源之前会先加锁,以确保数据的一致性。悲观锁认为在整个数据处理过程中会发生并发冲突,因此对数据进行加锁,使得其他线程无法修改数据,直到当前线程完成操作。典型的悲观锁机制包括使用互斥锁(如synchronized关键字)或数据库中的行级锁。
什么是乐观锁?
乐观锁
:
**乐观锁的基本思想是假设并发访问不会导致冲突,因此在访问共享资源时不会加锁,而是在更新数据时进行检查,如果发现数据已被其他线程修改,则进行回滚或重新尝试。**乐观锁认为在整个数据处理过程中并发冲突的概率较低,因此不会立即对数据进行加锁。典型的乐观锁机制包括使用版本号或时间戳来判断数据是否被修改。
乐观锁和悲观锁的主要区别:
- 加锁方式:
悲观锁
在访问共享资源之前会先加锁,而乐观锁
在访问共享资源时不加锁。 - 冲突处理:悲观锁假设会发生冲突,因此加锁保证数据的一致性;乐观锁假设不会发生冲突,因此在更新数据时进行检查,发现冲突时进行回滚或重新尝试。
- 并发性能:悲观锁在整个数据处理过程中都会持有锁,因此可能限制了并发性能;乐观锁在大部分情况下不会加锁,可以提高并发性能,但在发生冲突时需要进行回滚或重新尝试,可能会导致额外的开销。
选择悲观锁还是乐观锁取决于具体的应用场景和并发访问的特点。如果并发冲突较多,使用悲观锁可以确保数据的一致性;如果并发冲突较少,使用乐观锁可以提高并发性能。
悲观锁的实现方式
悲观锁的实现方式常见的有互斥锁(Mutex Lock)
:在访问共享资源之前,线程会先尝试获取互斥锁,如果锁已被其他线程占用,则当前线程会被阻塞,直到锁被释放。常见的互斥锁有 synchronized
关键字和 ReentrantLock
类。
互斥锁:用于保护共享资源,确保在任何时刻只有一个线程可以访问该资源。它的主要特点是互斥性,即同一时间只允许一个线程持有锁并执行关键代码块,其他线程需要等待锁的释放
使用synchronized
实现互斥锁的示例代码:
public class MutexLockExample {
private int count = 0; //共享变量,可能会被多个线程同时访问和修改
private Object lock = new Object();
public void increment() {
//保证对count的安全访问,使用synchronized关键字对关键字代码块进行同步
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
在increment()
方法和getCount()
方法中,我们通过synchronized (lock)
语句来获取互斥锁。当一个线程进入synchronized
块时,它将获得lock
对象的锁,其他线程将被阻塞,直到该线程释放锁。
这样,当多个线程调用increment()
方法时,每次只有一个线程可以执行该方法的代码块,保证了对count
变量的互斥访问。同样地,当多个线程调用getCount()
方法时,也只有一个线程可以执行该方法的代码块。
通过互斥锁的使用,我们确保了对共享变量的安全访问,避免了多线程环境下的竞态条件和数据不一致问题。
乐观锁的实现方式
乐观锁的实现方式主要有两种:版本号机制
和CAS(Compare and Swap)操作
。
-
版本号机制
:
在数据表中添加一个版本号字段,每次更新数据时都对版本号进行更新。当读取数据时,同时读取版本号。在进行数据更新时,先比较当前数据的版本号与之前读取的版本号是否一致,如果一致,则执行更新操作,并增加版本号;如果不一致,则表示数据已被其他线程修改,需要进行冲突处理(如回滚或重新尝试)。示例代码:
// 假设data是要更新的数据对象,version是版本号字段 int version = data.getVersion(); // 读取数据并版本号 // ... // 更新数据 data.setValue(newValue); data.setVersion(version + 1); // 增加版本号 // 执行更新操作,检查版本号是否一致 // ...
版本号机制实现乐观锁的关键是通过版本号来判断数据是否被修改,从而决定是否执行更新操作。
-
CAS(Compare and Swap)操作
:
**CAS是一种原子操作,用于实现乐观锁。它通过比较内存中的值与期望值是否一致,如果一致则进行更新,否则不做任何操作。**CAS操作通常使用底层硬件指令实现,可以保证原子性。在Java中,可以使用Atomic
类提供的原子类来进行CAS操作。示例代码:
AtomicInteger value = new AtomicInteger(0); // 原子整型变量 int expect = value.get(); // 读取当前值 int update = expect + 1; // 更新值 while (!value.compareAndSet(expect, update)) { expect = value.get(); // 再次读取当前值 update = expect + 1; // 更新值 }
**CAS操作适用于更新单个变量的情况,它不需要加锁,因此具有较高的并发性能。**如果CAS操作失败(即内存中的值与期望值不一致),则需要进行冲突处理(如回滚或重新尝试)。
乐观锁的实现方式可以根据具体的应用场景和技术选型进行选择。版本号机制适用于数据库等存储系统,CAS操作适用于多线程编程中的共享变量更新。
悲观锁会引发的问题
悲观锁的使用可能引发以下问题:
- 性能下降:悲观锁通常需要在访问数据之前获取锁,并在使用完数据后释放锁。导致其他线程必须等待锁的释放才能进行访问,从而导致整体性能降低。
- 死锁风险:如果在使用悲观锁时处理不当,可能会出现死锁情况。例如,一个线程持有锁A并等待锁B,而另一个线程持有锁B并等待锁A,这样就形成了死锁。死锁会导致线程无法继续执行,从而影响系统的正常运行。
- 并发性低:悲观锁在访问数据时需要锁定整个数据对象或数据表。这就限制了其他线程对数据的并发访问能力,从而降低了系统的并发性。
- 长时间占用资源:悲观锁在获取到锁之后会一直占用资源,直到任务完成或锁被释放。
乐观锁会引发的问题
乐观锁的使用可能引发以下问题:
- 冲突和重试:乐观锁不会主动加锁,如果在更新过程中发现数据已被修改,则需要进行冲突处理和重试操作。这可能导致额外的开销和延迟。
- 数据一致性问题:由于乐观锁不会主动加锁,如果多个线程同时修改同一数据,可能会导致其中一个线程的修改被覆盖或丢失,从而导致数据不一致的情况。例如ABA问题。
多学一招:
ABA问题
ABA问题是在并发编程中可能出现的一种情况。它的名称来自于一个典型的示例,假设一个共享变量开始时的值为A,然后被线程1修改为B,最后又被线程1修改回A。在这种情况下,如果另一个线程2只关注变量的值是否发生变化,它可能无法察觉到变量值的中间修改过程,导致出现错误的结果。
ABA问题可能会影响使用CAS(比较并交换)操作的并发算法,其中线程会比较变量的当前值与期望值,并在相等时进行修改。在上述的例子中,线程2如果只关注变量的值是否与期望值A相等,那么它可能会错误地认为变量没有被修改,从而导致并发问题。
为了解决ABA问题,通常会引入版本号或标记,以便在比较并交换操作中除了比较值之外,还要求比较版本号或标记是否匹配。这样可以避免因为中间的修改导致的错误判断。
总之,ABA问题是在并发编程中,由于变量在修改过程中经历了一系列的变化,导致某些操作无法正确判断变量的状态。通过引入版本号或标记等机制,可以避免ABA问题的发生。
结语:若文章有错误,欢迎各位读者指正,并联系作者进行修改,感谢!