11. 乐观锁与悲观锁
乐观锁
- 总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或 CAS 操作实现。
- version 方式:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。 当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相 等时才更新,否则重试更新操作,直到更新成功。
核心 SQL 代码:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
- CAS 操作方式:即 compare and swap(比较并交换) 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重 试,一般情况下是一个自旋操作,即不断的重试。
悲观锁
- 总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在 Java 中,synchronized 的思想也是悲观锁。
程序中的乐观锁与悲观锁
概念:
- 这里抛开数据库来谈乐观锁和悲观锁,扯上数据库总会觉得和 Java 离得很远。
- 悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。
- 乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作。
从解释上可以看出,悲观锁具有很强的独占性,也是最安全的.而乐观锁很开放,效率高,安全性比悲观锁低,因为在乐观锁检查数据版本一致性时也可能被其他线程修改数据。
package com.example.lock;
/**
* 乐观锁实现
* @author qinxuewu
* @version 1.00
* @time 20/7/2018 下午 12:53
*/
public class OptimisticLock {
public static int value = 0; // 多线程同时调用的操作对象
/**
* A 线程要执行的方法
*/
public static void invoke(int Avalue, String i) throws InterruptedException {
Thread.sleep(1000L);//延长执行时间
if (Avalue != value) {//判断 value 版本
System.out.println(Avalue + ":" + value + "A 版本不一致,不执行");
value--;
} else {
Avalue++;//对数据操作
value = Avalue;;//对数据操作
System.out.println("invoke: "+i + ":" + value);
}
}
/**
* B 线程要执行的方法
*/
public static void invoke2(int Bvalue, String i)
throws InterruptedException {
Thread.sleep(1000L);//延长执行时间
if (Bvalue != value) {//判断 value 版本
System.out.println(Bvalue + ":" + value + "B 版本不一致,不执行");
} else {
System.out.println("B:利用 value 运算,value="+Bvalue);
}
}
/**
* 测试,期待结果:B 线程执行的时候 value 数据总是当前最新的
*/
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {//A 线程
public void run() {
try {
for (int i = 0; i < 3; i++) {
int Avalue = OptimisticLock.value;//A 获取的 value
OptimisticLock.invoke(Avalue, "A");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {//B 线程
public void run() {
try {
for (int i = 0; i < 3; i++) {
int Bvalue = OptimisticLock.value;//B 获取的 value
OptimisticLock.invoke2(Bvalue, "B");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
测试结果:
A:1
0:1B 版本不一致,不执行
B:利用 value 运算,value=1
A:2
B:利用 value 运算,value=2
A:3
从结果中看出,B 线程在执行的时候最后发现自己的 value 和执行前不一致,说明被 A 修改了,那么放弃了本次执行.
多运行几次发现了下面的结果:
A:1
B:利用 value 运算,value=0
A:2
1:2B 版本不一致,不执行
A:3
B:利用 value 运算,value=2
从结果看 A 修改了 value 值,B 却没有检查出来,利用错误的 value 值进行了操作. 为什么会这样呢?
这里就回到前面说的乐观锁是有一定的不安全性的,B 在检查版本的时候 A 还没有修改,在 B 检查完版本后更新数据前(例子中的输出语句),A 更改了 value 值,这时 B 执行更新数据(例子中的输出语句) 就发生了与现存 value 不一致的现象。
针对这个问题,我觉得乐观锁要解决这个问题还需要在检查版本与更新数据这个操作的时候能够使用悲观锁,比如加上 synchronized,让它在最后一步保证数据的一致性.这样既保证多线程都能同时执行,牺牲最后一点的性能去保证数据的一致。
有两种方式来保证乐观锁最后同步数据保证它原子性的方法
1,CAS 方式:Java 非公开 API 类 Unsafe 实现的 CAS(比较-交换),由 C++编写的调用硬件操作内存,保证这个操作的原子 性,concurrent 包下很多乐观锁实现使用到这个类,但这个类不作为公开 API 使用,随时可能会被更改.我在本地测试了一下,确实不能够直接调用,源码中 Unsafe 是私有构造函数,只能通过 getUnsafe 方法获取单例,首先去掉 eclipse 的检查(非 API 的调用限制) 限制以后,执行发现报 java.lang.SecurityException 异常,源码中 getUnsafe 方法中执行访问检查,看来 java 不允许应用程序获取 Unsafe 类. 值得一提的是反射是可以得到这个类对象的。
2,加锁方式:利用 Java 提供的现有 API 来实现最后数据同步的原子性(用悲观锁).看似乐观锁最后还是用了悲观锁来保证安全,效率没有提高.实 际上针对于大多数只执行不同步数据的情况,效率比悲观加锁整个方法要高.特别注意:针对一个对象的数据同步,悲观锁对这个对象加锁和乐观锁效率差不多,如 果是多个需要同步数据的对象,乐观锁就比较方便。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论