小孩子才作选择,我全都要
,今天写一下面试必问的内容:乐观锁与悲观锁。主要从如下几方面来讲:
程序员
写文章的时候忽然收到朋友发来的消息,说乌兹退役了,LPL0006号选手断开链接。愿你鲜衣怒马,一日看尽长安花,历尽山河万里,归来还是曾经那个少年。来,跟我一块儿喊一句:大道至简-惟我自豪
web
乐观锁老是假设事情向着好的方向发展,就好比有些人天生乐观,向阳而生!面试
乐观锁老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,由于乐观锁在读取数据的时候不会去加锁,这样能够省去了锁的开销,加大了系统的整个吞吐量。即时偶尔有冲突,这也无伤大雅,要么从新尝试提交要么返回给用户说跟新失败,固然,前提是偶尔发生冲突
,但若是常常产生冲突,上层应用会不断的进行自旋重试,这样反却是下降了性能,得不偿失。算法
悲观锁老是假设事情向着坏的方向发展,就好比有些人经历了某些事情,可能不太相信别人,只信任本身,身在黑暗,脚踩光明!数据库
悲观锁每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞住,直到我释放了锁,别人才能拿到锁,这样的话,数据只有自己一个线程在修改,就确保了数据的准确性。所以,悲观锁适用于多写的应用类型。编程
版本号机制就是在表中增长一个字段,version
,在修改记录的时候,先查询出记录,再每次修改的时候给这个字段值加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='用户信息表'; 复制代码
INSERT INTO `user_info` (`user_name`, `money`, `version`) VALUES ('张三', 1000, 1);
复制代码
步骤 | 线程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 | 返回失败 |
CAS即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步,因此也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操做数:
多线程
当且仅当 V 的值等于 A 时,CAS 经过原子方式用新值 B 来更新 V 的值,不然不会执行任何操做(比较和替换是一个 native 原子操做)。通常状况下,这是一个自旋操做,即不断的重试,看下面流程:
app
private void updateMoney(String userName){
// 死循环 for (;;){ // 获取张三的金额 BigDecimal money = this.userMapper.getMoneyByName(userName); User user = new User(); user.setMoney(money); user.setUserName(userName); // 根据用户名和金额进行更新(金额+100) Integer updateCount = this.userMapper.updateMoneyByNameAndMoney(user); if (updateCount != null && updateCount.equals(1)){ // 若是更新成功就跳出循环 break; } } } 复制代码
步骤 | 线程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问题加版本号解决机制不是同一个。
可重入锁就是悲观锁的一种,若是你看过前两篇文章,对可重入锁的原理就很清楚了,不清楚的话就看下以下的流程:
步骤 | 线程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,除非线程中断或者获取到了锁,要否则就一直在自旋,这也就说明了为啥悲观锁比起乐观锁来讲更加消耗性能。
其实和上面差很少的,只不过上面自身维护了一个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来帮你加锁和释放锁。
上面在说乐观锁用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 | 跳出循环,返回更新成功 |
自旋CAS(也就是不成功就一直循环执行直到成功)若是长时间不成功,会给CPU带来很是大的执行开销。我的想法是在死循环添加尝试次数,达到尝试次数还没成功的话就返回失败。不肯定有没有什么问题,欢迎指出。
CAS 只对单个共享变量有效,当操做涉及跨多个共享变量时 CAS 无效。可是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行 CAS 操做。因此咱们可使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操做。
若是你以为个人文章对你有帮助话,欢迎关注个人微信公众号:"一个快乐又痛苦的程序员"(无广告,单纯分享原创文章、已pj的实用工具、各类Java学习资源,期待与你共同进步)