【校招VIP】[面试必备]乐观锁VS悲观锁,你真的了解吗

1天前 收藏 0 评论 0 java开发

【校招VIP】[面试必备]乐观锁VS悲观锁,你真的了解吗

转载声明:文章来源https://blog.csdn.net/weixin_53991455/article/details/129786972

前言
乐观锁和悲观锁是在并发编程中用于解决数据竞争的两种不同的策略。乐观锁假设在大多数情况下,读操作比写操作更频繁,因此不会直接使用锁来保护共享资源。相反,它使用版本号或时间戳等机制来检测是否有其他线程修改了数据。如果检测到了冲突,乐观锁会回滚事务或者重新尝试操作。悲观锁则假设写操作比读操作更频繁,因此会直接使用锁来保护共享资源,以防止其他线程同时修改数据。

一、什么是悲观锁与乐观锁?
乐观锁:在操作同一数据时,都认为他人不会对数据进行操作,所以不会对数据进行上锁,但是在做数据更新的时候会判断是否有其他人已经同时更新或已经更新完这个数据【可以使用版本号机制和CAS算法进行实现】
应用类型: 常见乐观锁适用于多读的应用类型,能够提高应用整体的TPS
实例说明: 数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了 乐观锁的一种实现方式 CAS 实现的


悲观锁: 在操作数据的时候,认为其他人也会对数据进行操作更改,因此在获取数据的时候会先加锁,其他线程想拿这个数据就会阻塞直到它拿到锁(公共资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)
实例说明: 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中 synchronized关键字和Lock的实现类都是悲观锁。数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁) 等均为悲观锁


二、锁的实现
2.1 乐观锁实现
在Java中,乐观锁常常使用CAS(Compare-And-Swap)操作来实现。CAS操作是一种原子性操作,它将内存位置的值与一个期望值进行比较,如果相等,则将该位置的值更新为一个新值。如果不相等,则表示有其他线程修改了该位置的值,CAS操作失败,需要重新尝试。下面是一个使用CAS操作来实现乐观锁的例子:

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLock {
private AtomicInteger value = new AtomicInteger(0);

public void increment() {
int expectedValue = value.get();
int newValue = expectedValue + 1;
while (!value.compareAndSet(expectedValue, newValue)) {
expectedValue = value.get();
newValue = expectedValue + 1;
}
}

public int getValue() {
return value.get();
}
}

在这个例子中,我们使用AtomicInteger类来表示共享资源,它提供了一些原子性操作,包括get()和compareAndSet()。increment()方法使用CAS操作来实现乐观锁,它首先获取当前值,然后尝试将该值加1。如果compareAndSet()操作返回false,表示有其他线程同时修改了该值,需要重新获取当前值并重试。

2.1.1 版本号机制
一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

2.1.2 CAS算法
代码实现

//number自增10次,道理来说增长50次,但是每次都不同,线程不安全的
private static int number = 0;

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
number++;
}
}
}).start();
}

try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(number);
}
// 使用AtomicInteger之后,最终的输出结果同样可以保证是50,且Atomic操作的底层实现正是利用的CAS机制

private static AtomicInteger number = new AtomicInteger(0);
//每个线程自增10次,共计自增到50
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}

for (int i = 0; i < 10; i++) {
number.incrementAndGet();
}
}
}).start();
}

try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(number);
}

2.2 悲观锁实现
2.2.1 悲观锁实例1
悲观锁在Java中通常使用synchronized关键字来实现。synchronized关键字在进入同步块之前会自动获取锁,然后在离开同步块时释放锁。下面是一个使用synchronized关键字来实现悲观锁的例子:

public class PessimisticLock {
private int value = 0;

public synchronized void increment() {
value++;
}

public synchronized int getValue() {
return value;
}
}

在这个例子中,我们使用synchronized关键字来确保increment()和getValue()方法的原子性。在进入任何一个方法之前,线程会自动获取锁,然后在离开方法时释放锁,以防止其他线程同时修改共享资源。

2.2.2悲观锁实例2
下面的代码实现使用了一个对象锁和一个布尔变量来控制锁的状态。当锁被占用时,其他线程调用 lock() 方法会被阻塞,直到锁被释放。当锁被释放时,unlock() 方法会通知其他线程可以尝试获取锁。
这种实现方式比较简单,但是有一个明显的缺点:如果有很多线程需要获取锁,但是锁一直被某个线程占用,那么其他线程就会一直处于等待状态,造成资源浪费。因此,这种实现方式适合于并发访问不是很高的场景,如果并发访问比较高,可以考虑使用乐观锁等更高效的实现方式。

public class PessimisticLock {
private final Object lock = new Object();
private boolean isLocked = false;

public void lock() throws InterruptedException {
synchronized (lock) {
while (isLocked) {
lock.wait();
}
isLocked = true;
}
}

public void unlock() {
synchronized (lock) {
isLocked = false;
lock.notify();
}
}
}

三、乐观锁与悲观锁的比较
乐观锁和悲观锁是在不同的情况下使用的锁机制,它们各自有不同的优缺点。下面是乐观锁和悲观锁的比较:
3.1 性能比较
乐观锁适用于并发访问量较少的情况下,可以提高系统的并发性能和吞吐量。悲观锁适用于并发访问量较大的情况下,可以保证数据的一致性和正确性,但会降低系统的并发性能。
3.2 安全性比较
乐观锁采用版本控制等方式来保证数据的正确性,但在并发访问量较大的情况下容易出现数据冲突和错误。悲观锁采用加锁的方式来保证数据的一致性和正确性,避免了数据冲突和错误,但可能会出现死锁和饥饿等问题。
3.3 应用场景比较
乐观锁适用于读多写少的情况下,例如网站的浏览量统计。悲观锁适用于读写频繁的情况下,例如银行系统的转账操作。

四、总结
乐观锁和悲观锁是并发编程中常用的锁机制,它们各自有不同的优缺点和应用场景。在选择锁机制时,需要根据实际情况选择合适的锁机制,以保证系统的性能、安全和稳定性。同时,需要进行细致的设计和优化,避免出现死锁、饥饿等问题,提高系统的可靠性和可维护性。
注意: 不论是乐观锁或者是悲观锁,在程序应用的过程中如果没有针对于并发做好校验,那么就会出现一系列的数据问题,比如没有做好并发控制,就可能导致脏读、幻读和不可重复读等问题

C 0条回复 评论

帖子还没人回复快来抢沙发