在并发编程中,如何在多个线程间共享资源是一个核心问题。传统的做法是使用各种锁机制(如synchronized或ReentrantLock)来确保数据一致性。然而,锁机制往往会导致线程等待和性能损耗。为此,我们引入了“乐观锁”的概念,它能够有效的提高并发性能。
一、什么是乐观锁?
乐观锁是一种乐观的并发控制机制,它假设多个线程操作同一个资源时,冲突是偶发事件。每次线程读取数据时,认为其他线程不会修改该数据,只有在更新数据时,才会检查是否有冲突。
乐观锁与传统的锁不同,它不阻塞线程执行。在多线程环境下,乐观锁通过版本号的机制来控制并发的更新操作。如果版本号未发生变化,则允许更新;否则拒绝操作并通知线程重新尝试。
二、乐观锁的实现原理
乐观锁的典型实现方式是通过版本号来控制并发。具体步骤如下:
1.每个记录都带有一个版本号(version)。
2.线程读取记录时,同时读取其版本号。
3.线程在更新数据时,检查当前记录的版本号是否与之前读取的一致。如果一致,则更新成功并增加版本号;否则,说明数据已经被其他线程修改,当前操作失败,需要重新读取数据。
三、乐观锁的代码实现
接下来,我们通过一个多线程操作共享数据的简单示例,来展示如何在Java中使用乐观锁。我们将使用版本号机制,确保数据的正确性。
1.数据类
首先,我们需要一个Data类,用于存储需要操作的数据以及对应的版本号。为了确保线程安全,版本号使用AtomicInteger进行自增。
package com.wyx.interview.optimisticLock;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description Time tries all things
* @Author wyx
* @Date 2024/9/9
**/
public class Data {
// 使用 AtomicInteger 保证版本号的线程安全
private static AtomicInteger version = new AtomicInteger(1);
// 真实数据
private static String data = "实现一个乐观锁";
// 获取当前版本号
public static int getVersion() {
return version.get();
}
// 更新版本号
public static void updateVersion() {
version.incrementAndGet(); // 版本号自增1
}
// 获取当前数据
public static String getData() {
return data;
}
// 设置新数据
public static void setData(String newData) {
data = newData;
}
}
2.线程类
每个线程会读取数据并尝试更新。在线程运行过程中,我们会检查当前版本号,只有版本号与预期相同,才会进行数据的写操作。
package com.wyx.interview.optimisticLock;
/**
* @Description Time tries all things
* @Author wyx
* @Date 2024/9/9
**/
public class OptimisticThread extends Thread {
private int version; // 线程预期的版本号
private String newData; // 线程要写入的新数据
// 构造函数
public OptimisticThread(String name, int version, String newData) {
super(name); // 设置线程名
this.version = version;
this.newData = newData;
}
@Override
public void run() {
int retryCount = 3; // 允许的重试次数
while (retryCount > 0) {
// 1. 读取当前数据
String currentData = Data.getData();
System.out.println("线程" + getName() + ",获得的数据版本号为:" + Data.getVersion());
System.out.println("线程" + getName() + ",预期的数据版本号为:" + version);
System.out.println("线程" + getName() + "读取数据完成=========data = " + currentData);
// 2. 检查版本号是否匹配
if (Data.getVersion() == version) {
synchronized (OptimisticThread.class) {
// 3. 再次检查版本号,并更新数据
if (Data.getVersion() == version) {
Data.setData(newData); // 更新数据
Data.updateVersion(); // 更新版本号
System.out.println("线程" + getName() + "写数据完成========new data = " + newData);
return; // 成功,结束线程
}
}
} else {
// 版本号不匹配,进行重试
System.out.println("线程" + getName() + "获得的数据版本号不匹配,重试...");
}
retryCount--; // 重试次数减少
}
// 如果重试次数耗尽
System.out.println("线程" + getName() + "重试次数用完,操作失败!");
}
}
3.测试类
最后我们创建多个线程,模拟多线程操作共享数据的场景。
package com.wyx.interview.optimisticLock;
/**
* @Description Time tries all things
* @Author wyx
* @Date 2024/9/9
**/
public class Test {
public static void main(String[] args) {
// 创建多个线程,尝试并发修改数据
for (int i = 1; i <= 2; i++) {
new OptimisticThread(String.valueOf(i), 1, "新数据" + i).start();
}
}
}
输出示例:
线程1,获得的数据版本号为:1 线程2,获得的数据版本号为:1 线程1,预期的数据版本号为:1 线程1读取数据完成=========data = 实现一个乐观锁 线程1写数据完成========new data = 新数据1 线程2,预期的数据版本号为:1 线程2读取数据完成=========data = 实现一个乐观锁 线程2获得的数据版本号不匹配,重试... 线程2,获得的数据版本号为:2 线程2,预期的数据版本号为:1 线程2读取数据完成=========data = 新数据1 线程2获得的数据版本号不匹配,重试... 线程2,获得的数据版本号为:2 线程2,预期的数据版本号为:1 线程2读取数据完成=========data = 新数据1 线程2获得的数据版本号不匹配,重试... 线程2重试次数用完,操作失败! Process finished with exit code 0
四、乐观锁的应用场景
1.高并发场景
乐观锁适合用于高并发场景,多个线程同时读写时,冲突概率不高的情况。与悲观锁相比,乐观锁不会阻塞线程,提升了系统的并发性能。
2.无强一致性需求的系统
乐观锁适用于对数据强一致性要求不高的系统。如果每次读写数据冲突的可能性较大,悲观锁可能是更好的选择。
3.数据库中的乐观锁
在数据库操作中,常常使用乐观锁来控制并发。例如,在UPDATE操作时,利用SQL语句中的WHERE子句加上版本号检查,只有在版本号匹配时才能更新成功。
五、总结
乐观锁通过版本号机制,减少了线程之间的锁竞争,从而提高了并发性能。通过文本的示例代码,我们了解了如何在Java中实现一个简单的乐观锁机制。乐观锁并不是万能的,它适用于冲突较少的场景,如果并发冲突较多,则可能导致频繁的重试,进而影响性能。
感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
感兴趣的可以关注公众号一起学习,我会不定期发布学习和一些有意思的见闻。