
前言
小孩子才作选择,我全都要
,今天写一下面试必问的内容:乐观锁与悲观锁。主要从如下几方面来讲:
程序员
-
何为乐观锁 -
何为悲观锁 -
乐观锁经常使用实现方式 -
悲观锁经常使用实现方式 -
乐观锁的缺点 -
悲观锁的缺点
写文章的时候忽然收到朋友发来的消息,说乌兹退役了,LPL0006号选手断开链接。愿你鲜衣怒马,一日看尽长安花,历尽山河万里,归来还是曾经那个少年。来,跟我一块儿喊一句:大道至简-惟我自豪
web
一、何为乐观锁
乐观锁老是假设事情向着好的方向发展,就好比有些人天生乐观,向阳而生!面试
乐观锁老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,由于乐观锁在读取数据的时候不会去加锁,这样能够省去了锁的开销,加大了系统的整个吞吐量。即时偶尔有冲突,这也无伤大雅,要么从新尝试提交要么返回给用户说跟新失败,固然,前提是偶尔发生冲突
,但若是常常产生冲突,上层应用会不断的进行自旋重试,这样反却是下降了性能,得不偿失。算法
二、何为悲观锁
悲观锁老是假设事情向着坏的方向发展,就好比有些人经历了某些事情,可能不太相信别人,只信任本身,身在黑暗,脚踩光明!数据库
悲观锁每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞住,直到我释放了锁,别人才能拿到锁,这样的话,数据只有自己一个线程在修改,就确保了数据的准确性。所以,悲观锁适用于多写的应用类型。编程
三、乐观锁经常使用实现方式
3.1 版本号机制
版本号机制就是在表中增长一个字段,version
,在修改记录的时候,先查询出记录,再每次修改的时候给这个字段值加1,判断条件就是你刚才查询出来的值。看下面流程就明白了:
微信
-
3.1.1 新增用户信息表
CREATE TABLE `user_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_name` varchar(64) DEFAULT NULL COMMENT '用户姓名',
`money` decimal(15,0) DEFAULT '0' COMMENT '剩余金额(分)',
`version` bigint(20) DEFAULT '1' COMMENT '版本号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB COMMENT='用户信息表';
-
3.1.2 新增一条数据
INSERT INTO `user_info` (`user_name`, `money`, `version`) VALUES ('张三', 1000, 1);
-
3.1.3 操做步骤
步骤 | 线程A | 线程B |
---|---|---|
1 | 查询张三数据,得到版本号为1(SELECT * FROM user_info WHERE user_name = '张三';) | |
2 | 查询张三数据,得到版本号为1(SELECT * FROM user_info WHERE user_name = '张三';) | |
3 | 修改张三金额,增长100,版本号+1(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '张三' AND version = 1;),返回修改条数为1 | |
4 | 修改张三金额,增长200,版本号+1(UPDATE user_info SET money = money + 200, version = version + 1 WHERE user_name = '张三' AND version = 1;),返回修改条数为0 | |
5 | 判断修改条数为是否为0,是返回失败,不然返回成功 | |
6 | 判断修改条数为是否为0,是返回失败,不然返回成功 | |
7 | 返回成功 | |
8 | 返回失败 |
3.2 CAS算法
CAS即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步,因此也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操做数:
多线程
-
须要读写的内存值 V(主内存中的变量值) -
进行比较的值 A(克隆下来线程本地内存中的变量值) -
拟写入的新值 B(要更新的新值)
当且仅当 V 的值等于 A 时,CAS 经过原子方式用新值 B 来更新 V 的值,不然不会执行任何操做(比较和替换是一个 native 原子操做)。通常状况下,这是一个自旋操做,即不断的重试,看下面流程:
app
-
3.2.1 CAS算法模拟数据库更新数据(表仍是刚才那个表,用户张三的金额初始值为1000),给用户张三的金额增长100:
private void updateMoney(String userName){
// 死循环
for (;;){
// 获取张三的金额
BigDecimal money = this.userMapper.getMoneyByName(userName);
User user = new User();
user.setMoney(money);
user.setUserName(userName);
// 根据书名和版本号进行阅读量更新
Integer updateCount = this.userMapper.updateMoneyByNameAndMoney(user);
if (updateCount != null && updateCount.equals(1)){
// 若是更新成功就跳出循环
break;
}
}
}
-
3.2.2 流程图以下:
步骤 | 线程A | 线程B |
---|---|---|
1 | 从表中查询出张三的money=1000,设置进行比较的值为1000,要写入的新值为money + 100 = 1100(V:1000--A:1000--B:1100) | |
2 | 从表中查询出张三的money=1000,设置进行比较的值为1000,要写入的新值为money + 100 = 1100(V:1000--A:1000--B:1100) | |
3 | 更新张三的金额(UPDATE user_info SET money = money + 100 WHERE user_name = '张三' AND money = 1000;),返回更新条数为1 | |
4 | 更新张三的金额(UPDATE user_info SET money = money + 100 WHERE user_name = '张三' AND money = 1000;),返回更新条数为0 | |
5 | 跳出循环,返回更新成功 | |
6 | 自旋再次从表中查询出张三的money=1100,设置进行比较的值为1100,要写入的新值为money + 100 = 1200(V:1100--A:1100--B:1200) | |
7 | 更新张三的金额(UPDATE user_info SET money = money + 100 WHERE user_name = '张三' AND money = 1100;),返回更新条数为1 | |
8 | 跳出循环,返回更新成功 |
看到这里,明眼人都发现了一些CAS更新的小问题,至因而什么问题呢、怎么解决呢,放在下面来说,要否则下面几条就没得写了。。。。。。编辑器
注意,这里的加版本号机制和CAS出现ABA问题加版本号解决机制不是同一个。
四、悲观锁经常使用实现方式
4.1 ReentrantLock
可重入锁就是悲观锁的一种,若是你看过前两篇文章,对可重入锁的原理就很清楚了,不清楚的话就看下以下的流程:
-
假设同步状态值为0表示未加锁,为1加锁成功
步骤 | 线程A | 线程B |
---|---|---|
1 | 从主内存中克隆出同步状态值为0,设置进行比较的值为0,要写入的新值为1(V:0--A:0--B:1) | |
2 | 从主内存中克隆出同步状态值为0,设置进行比较的值为0,要写入的新值为1(V:0--A:0--B:1) | |
3 | 更新主内存,用A和主内存的值比较,0 = 0,加锁成功,此时主内存值为1 | |
4 | 更新主内存,用A和主内存的值比较,0 != 1,加锁失败。 | |
5 | 返回加锁成功 | |
6 | 执行业务逻辑 | 自旋再次尝试更新主内存,用A和主内存的值比较,0 != 1,加锁失败 |
7 | 自旋再次尝试更新主内存,用A和主内存的值比较,0 != 1,加锁失败 | |
8 | 自旋再次尝试更新主内存,用A和主内存的值比较,0 != 1,加锁失败 | |
9 | 释放锁,设置同步状态值为0 | |
10 | 自旋再次尝试更新主内存,用A和主内存的值比较,0 = 0,加锁成功,此时主内存值为1 |
能够看到,只要线程A获取了锁,还没释放的话,线程B是没法获取锁的,除非A释放了锁,B才能获取到锁,加锁的方式都是经过CAS去比较再交换,B会一直自旋去CAS,除非线程中断或者获取到了锁,要否则就一直在自旋,这也就说明了为啥悲观锁比起乐观锁来讲更加消耗性能。
4.2 synchronized
其实和上面差很少的,只不过上面自身维护了一个volatile int类型的变量,用来描述获取锁与释放锁,而synchronized是靠指令判断加锁与释放锁的,以下代码:
public class synchronizedTest {
。。。。。。
public void synchronizedTest(){
synchronized (this){
mapper.updateMoneyByName("张三");
}
}
}
上面代码对应的流程图以下:
步骤 | 线程A | 线程B |
---|---|---|
1 | 调用synchronizedTest()方法 | |
2 | 调用synchronizedTest()方法 | |
3 | 插入monitorenter指令 | |
4 | 执行业务逻辑 | 尝试获取monitorenter指令的全部权 |
5 | 执行业务逻辑 | 尝试获取monitorenter指令的全部权 |
6 | 执行业务逻辑 | 尝试获取monitorenter指令的全部权 |
7 | 业务逻辑执行完毕,插入monitorexit指令 | 尝试获取monitorenter指令的全部权,获取成功,插入monitorenter指令 |
8 | 执行业务逻辑 | |
9 | 执行业务逻辑 | |
10 | 业务逻辑执行完毕,插入monitorexit指令 |
若是在某个线程执行synchronizedTest()方法的过程当中出现了异常,monitorexit指令会插入在异常处,ReentrantLock
须要你手动去加锁与释放锁,而synchronized
是JVM来帮你加锁和释放锁。
五、乐观锁的缺点
5.1.1 ABA 问题
上面在说乐观锁用CAS方式实现的时候有个问题,明眼人能发现的,不知道各位有没有发现,问题以下:
步骤 | 线程A | 线程B |
---|---|---|
1 | 从表中查询出张三的money=1000,设置进行比较的值为1000,要写入的新值为money + 100 = 1100(V:1000--A:1000--B:1100) | |
2 | 从表中查询出张三的money=1000,设置进行比较的值为1000,要写入的新值为money + 100 = 1100(V:1000--A:1000--B:1100) | |
3 | 更新张三的金额(UPDATE user_info SET money = money + 100 WHERE user_name = '张三' AND money = 1000;),返回更新条数为1 | |
4 | 更新张三的金额(UPDATE user_info SET money = money + 100 WHERE user_name = '张三' AND money = 1000;),返回更新条数为0 | |
5 | 跳出循环,返回更新成功 | |
6 | 自旋再次从表中查询出张三的money=1100,设置进行比较的值为1100,要写入的新值为money + 100 = 1200(V:1100--A:1100--B:1200) | |
7 | 更新张三的金额(UPDATE user_info SET money = money + 100 WHERE user_name = '张三' AND money = 1100;),返回更新条数为1(注意,问题在这里,在步骤6,咱们查询到money=1100,而咱们在这里判断的时候,能肯定money没有被别的线程修改过吗?答案是并不能,有可线程能C加了100,线程D减了100,而这里的money值仍然是1100,这个问题被称为CAS操做的 "ABA"问题) | |
8 | 跳出循环,返回更新成功 |
-
解决方案:
给表增长一个version
字段,每修改一次值加1,这样就能在写入的时候判断获取到的值有没有被修改过,流程图以下:
步骤 | 线程A | 线程B |
---|---|---|
1 | 从表中查询出张三的money=1000,version=1 | |
2 | 从表中查询出张三的money=1000,version=1 | |
3 | 更新张三的金额(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '张三' AND money = 1000 AND version = 1;),返回更新条数为1 | |
4 | 更新张三的金额(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '张三' AND money = 1000 AND version = 1;),返回更新条数为0 | |
5 | 跳出循环,返回更新成功 | |
6 | 自旋再次从表中查询出张三的money=1100,version = 2 | |
7 | 更新张三的金额(UPDATE user_info SET money = money + 100, version = version + 1 WHERE user_name = '张三' AND money = 1100 AND version = 2;),返回更新条数为1 | |
8 | 跳出循环,返回更新成功 |
5.1.2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)若是长时间不成功,会给CPU带来很是大的执行开销。我的想法是在死循环添加尝试次数,达到尝试次数还没成功的话就返回失败。不肯定有没有什么问题,欢迎指出。
5.1.3 只能保证一个共享变量的原子操做
CAS 只对单个共享变量有效,当操做涉及跨多个共享变量时 CAS 无效。可是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行 CAS 操做。因此咱们可使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操做。
六、悲观锁的缺点
6.1 synchronized
-
锁的释放状况少,只在程序正常执行完成和抛出异常时释放锁; -
试图得到锁是不能设置超时; -
不能中断一个正在试图得到锁的线程; -
没法知道是否成功获取到锁;
6.2 ReentrantLock
-
须要使用import 引入相关的Class; -
不能忘记在finally 模块释放锁,这个看起来比synchronized 丑陋; -
synchronized能够放在方法的定义里面, 而reentrantlock只能放在块里面. 比较起来, synchronized能够减小嵌套;
结尾
若是你以为个人文章对你有帮助话,欢迎关注个人微信公众号:"一个快乐又痛苦的程序员"(无广告,单纯分享原创文章、已pj的实用工具、各类Java学习资源,期待与你共同进步)

本文分享自微信公众号 - 一个快乐又痛苦的程序员(AsuraTechnology)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。