前言一场由于“抢票”引发的血案在多线程和高并发的场景下最怕的就是共享数据被改乱了。比如两个人同时买最后一张火车票如果不加控制两人都以为自己买到了结果系统扣了两次钱票却只有一张。为了解决这个问题我们在程序设计中引入了“锁”的概念。针对不同的业务场景大佬们发明了两种截然不同的并发控制思想悲观锁与乐观锁。今天我们就来彻底扒下它们的外衣看看它们到底是怎么玩的一、 悲观锁Pessimistic Lock—— “总有刁民想害朕”1. 核心思想悲观锁人如其名极其悲观。它认为在并发环境下只要我不把门锁死别人就一定会来捣乱修改数据。因此它的策略是先上锁再操作。任何想要访问受保护数据的线程都必须先获取锁。拿不到锁的就在外面排队挂起/阻塞直到上一个线程操作完毕释放锁。2. 典型代表Java中的synchronized关键字。Java中的ReentrantLock。数据库里的表锁、行锁如SELECT ... FOR UPDATE。3. 代码实战与底层解析我们来看一个经典的转账或者扣库存的例子importjava.util.concurrent.locks.ReentrantLock;publicclassPessimisticLockDemo{privateintticketCount10;// 定义一个典型的悲观锁privatefinalReentrantLocklocknewReentrantLock();publicvoidbuyTicket(StringuserName){// 第一步一上来就加锁不给别人任何机会lock.lock();try{// 此时其他线程执行到 lock.lock() 会被全部阻塞挂起排队System.out.println(userName 准备买票当前余票ticketCount);if(ticketCount0){// 模拟业务处理耗时Thread.sleep(100);ticketCount--;System.out.println(userName 买票成功剩余ticketCount);}else{System.out.println(userName 买票失败已经被抢光了...);}}catch(InterruptedExceptione){e.printStackTrace();}finally{// 最后一步无论是正常结束还是发生异常必须要释放锁lock.unlock();}}}【优缺点剖析】优点绝对安全。不管并发多高数据绝对不会错。缺点太重了线程一直加锁释放锁、阻塞唤醒需要请求操作系统从用户态切换到内核态开销极大。这就好比上公共厕所进去一个人就把大门锁了外面的人只能干等。二、 乐观锁Optimistic Lock—— “阳光大男孩的自信”1. 核心思想乐观锁非常开朗它认为并发冲突是小概率事件平时大家各凭本事去拿数据不会有人来捣乱。因此它的策略是不上锁只在最后更新的时刻去检查一下数据有没有被人动过。如果没被动过更新成功如果被动过了别人捷足先登更新失败然后选择重试自旋或者放弃。2. 核心技术CAS (Compare And Swap - 比较并交换)CAS 是乐观锁的灵魂。它包含三个参数V内存里的当前值E我期望的原本旧值N我想要修改成的新值执行逻辑当我要更新时只有发现内存当前值 V 等于我期望的旧值 E 时我才把值修改为 N。如果不等于说明被别人改过了我就放弃或者重新拿最新的 V 值再试一次。3. 代码实战与底层解析Java并发包java.util.concurrent.atomic下的原子类全是乐观锁的实现。importjava.util.concurrent.atomic.AtomicInteger;publicclassOptimisticLockDemo{// 这是一个利用 CAS 实现的乐观锁整数类底层无 synchronizedprivateAtomicIntegerticketCountnewAtomicInteger(10);publicvoidbuyTicket(StringuserName){intexpect;// 我期望拿到的旧票数intupdate;// 我抢购完后的新票数// 核心套路自旋死循环 CAS检查do{// 首先看一眼当前的余票是多少expectticketCount.get();if(expect0){System.out.println(userName 发现没票了放弃挣扎。);return;}// 假设我买走一张期望新票数updateexpect-1;// 下面这行就是 CAS 操作// 如果当前内存里的票数和我看的时候(expect)还是一致的那就把它变成 update。// 如果在这期间有人买走了ticketCount.compareAndSet 会返回 false循环继续这就叫自旋。}while(!ticketCount.compareAndSet(expect,update));System.out.println(userName 买票成功剩余update (使用了乐观理念));}}4. 致命漏洞ABA问题乐观锁有一个经典 BugABA 问题。老王看一眼桌上的钱是 100 块这是 A准备闭眼拿走。这时小偷把 100 块偷走了变成 B过了几秒觉得心虚又放了 100块假钞回去又变回 A。老王睁眼一看还是 100 块满足CAS条件开开心心拿走了。但他不知道这中间钱已经被掉包了解决方案加个版本号Stamp。每次修改数据不仅对比值还要看版本号递增了没。Java 中提供了AtomicStampedReference专门用来解决 ABA 问题总结到底谁更胜一筹没有绝对的王者只有最适合的兵器。读多写少的场景冲突较小使用乐观锁CAS。省去了线程挂起的时间性能极高。写多读少的场景冲突极其激烈。如果还用乐观锁会导致大量线程在死循环自旋把CPU直接打满耗爆这时候必须用悲观锁synchronized 或 Lock乖乖排队才是王道。
悲观锁与乐观锁:思想决定命运,到底谁更胜一筹?
前言一场由于“抢票”引发的血案在多线程和高并发的场景下最怕的就是共享数据被改乱了。比如两个人同时买最后一张火车票如果不加控制两人都以为自己买到了结果系统扣了两次钱票却只有一张。为了解决这个问题我们在程序设计中引入了“锁”的概念。针对不同的业务场景大佬们发明了两种截然不同的并发控制思想悲观锁与乐观锁。今天我们就来彻底扒下它们的外衣看看它们到底是怎么玩的一、 悲观锁Pessimistic Lock—— “总有刁民想害朕”1. 核心思想悲观锁人如其名极其悲观。它认为在并发环境下只要我不把门锁死别人就一定会来捣乱修改数据。因此它的策略是先上锁再操作。任何想要访问受保护数据的线程都必须先获取锁。拿不到锁的就在外面排队挂起/阻塞直到上一个线程操作完毕释放锁。2. 典型代表Java中的synchronized关键字。Java中的ReentrantLock。数据库里的表锁、行锁如SELECT ... FOR UPDATE。3. 代码实战与底层解析我们来看一个经典的转账或者扣库存的例子importjava.util.concurrent.locks.ReentrantLock;publicclassPessimisticLockDemo{privateintticketCount10;// 定义一个典型的悲观锁privatefinalReentrantLocklocknewReentrantLock();publicvoidbuyTicket(StringuserName){// 第一步一上来就加锁不给别人任何机会lock.lock();try{// 此时其他线程执行到 lock.lock() 会被全部阻塞挂起排队System.out.println(userName 准备买票当前余票ticketCount);if(ticketCount0){// 模拟业务处理耗时Thread.sleep(100);ticketCount--;System.out.println(userName 买票成功剩余ticketCount);}else{System.out.println(userName 买票失败已经被抢光了...);}}catch(InterruptedExceptione){e.printStackTrace();}finally{// 最后一步无论是正常结束还是发生异常必须要释放锁lock.unlock();}}}【优缺点剖析】优点绝对安全。不管并发多高数据绝对不会错。缺点太重了线程一直加锁释放锁、阻塞唤醒需要请求操作系统从用户态切换到内核态开销极大。这就好比上公共厕所进去一个人就把大门锁了外面的人只能干等。二、 乐观锁Optimistic Lock—— “阳光大男孩的自信”1. 核心思想乐观锁非常开朗它认为并发冲突是小概率事件平时大家各凭本事去拿数据不会有人来捣乱。因此它的策略是不上锁只在最后更新的时刻去检查一下数据有没有被人动过。如果没被动过更新成功如果被动过了别人捷足先登更新失败然后选择重试自旋或者放弃。2. 核心技术CAS (Compare And Swap - 比较并交换)CAS 是乐观锁的灵魂。它包含三个参数V内存里的当前值E我期望的原本旧值N我想要修改成的新值执行逻辑当我要更新时只有发现内存当前值 V 等于我期望的旧值 E 时我才把值修改为 N。如果不等于说明被别人改过了我就放弃或者重新拿最新的 V 值再试一次。3. 代码实战与底层解析Java并发包java.util.concurrent.atomic下的原子类全是乐观锁的实现。importjava.util.concurrent.atomic.AtomicInteger;publicclassOptimisticLockDemo{// 这是一个利用 CAS 实现的乐观锁整数类底层无 synchronizedprivateAtomicIntegerticketCountnewAtomicInteger(10);publicvoidbuyTicket(StringuserName){intexpect;// 我期望拿到的旧票数intupdate;// 我抢购完后的新票数// 核心套路自旋死循环 CAS检查do{// 首先看一眼当前的余票是多少expectticketCount.get();if(expect0){System.out.println(userName 发现没票了放弃挣扎。);return;}// 假设我买走一张期望新票数updateexpect-1;// 下面这行就是 CAS 操作// 如果当前内存里的票数和我看的时候(expect)还是一致的那就把它变成 update。// 如果在这期间有人买走了ticketCount.compareAndSet 会返回 false循环继续这就叫自旋。}while(!ticketCount.compareAndSet(expect,update));System.out.println(userName 买票成功剩余update (使用了乐观理念));}}4. 致命漏洞ABA问题乐观锁有一个经典 BugABA 问题。老王看一眼桌上的钱是 100 块这是 A准备闭眼拿走。这时小偷把 100 块偷走了变成 B过了几秒觉得心虚又放了 100块假钞回去又变回 A。老王睁眼一看还是 100 块满足CAS条件开开心心拿走了。但他不知道这中间钱已经被掉包了解决方案加个版本号Stamp。每次修改数据不仅对比值还要看版本号递增了没。Java 中提供了AtomicStampedReference专门用来解决 ABA 问题总结到底谁更胜一筹没有绝对的王者只有最适合的兵器。读多写少的场景冲突较小使用乐观锁CAS。省去了线程挂起的时间性能极高。写多读少的场景冲突极其激烈。如果还用乐观锁会导致大量线程在死循环自旋把CPU直接打满耗爆这时候必须用悲观锁synchronized 或 Lock乖乖排队才是王道。