张志学 發表於 2025-11-6 15:17:00

浅谈java中的悲观锁,乐观锁以及CAS操作

<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">了解volatile的同学一定知道,volatile 可以保证可见性,但是它无法保证原子性。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">所谓原子性,就是一个(一系列)操作,要么全都执行,要么全都不执行,不能执行到中间某种状态就结束,同时对于外界(其它)来看,要么就是看到执行前的结果,要么就是执行后的结果,不能看到中间状态。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">举一个经典的例子:多线程对于全局volatile 变量的累加,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )代码如下:</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> Main {
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span>   <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">volatile</span> <span style="color: rgba(0, 0, 255, 1)">int</span> count = 0<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span>   <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">int</span> TOTAL = 10000<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span>
<span style="color: rgba(0, 128, 128, 1)"> 5</span>   <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span> main(String[] args) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> InterruptedException {
</span><span style="color: rgba(0, 128, 128, 1)"> 6</span>         Runnable r = () -&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span>             <span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = 0; i &lt; TOTAL; i++<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span>               count++<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> <span style="color: rgba(0, 0, 0, 1)">            }
</span><span style="color: rgba(0, 128, 128, 1)">10</span> <span style="color: rgba(0, 0, 0, 1)">      };
</span><span style="color: rgba(0, 128, 128, 1)">11</span>
<span style="color: rgba(0, 128, 128, 1)">12</span>         Thread t1 = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Thread(r);
</span><span style="color: rgba(0, 128, 128, 1)">13</span>         Thread t2 = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Thread(r);
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 0, 0, 1)">      t1.start();
</span><span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 0, 0, 1)">      t2.start();
</span><span style="color: rgba(0, 128, 128, 1)">16</span>
<span style="color: rgba(0, 128, 128, 1)">17</span> <span style="color: rgba(0, 0, 0, 1)">      t1.join();
</span><span style="color: rgba(0, 128, 128, 1)">18</span> <span style="color: rgba(0, 0, 0, 1)">      t2.join();
</span><span style="color: rgba(0, 128, 128, 1)">19</span>
<span style="color: rgba(0, 128, 128, 1)">20</span>         System.out.println("echo :" +<span style="color: rgba(0, 0, 0, 1)"> count);
</span><span style="color: rgba(0, 128, 128, 1)">21</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">22</span> }</pre>
</div>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">这个代码的执行结果如下,多次执行也基本不会达到目标值20000</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> Connected to the target VM, address: '127.0.0.1:54088', transport: 'socket'
<span style="color: rgba(0, 128, 128, 1)">2</span> echo :13533
<span style="color: rgba(0, 128, 128, 1)">3</span> Disconnected from the target VM, address: '127.0.0.1:54088', transport: 'socket'</pre>
</div>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">产生这个问题的原因是,我们在处理自增操作时,它不是原子性的。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">虽然两个线程对于这个变量的操作变化都是实时感知的,读的都是是实时值,但是计算和回写时可能就会出问题了。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">详细说下</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">A 线程 将变量x自增为1 </span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">B线程读取1 ,B线程计算+1时,得到结果是2(注意此时2存在临时变量中),在计算+1时,A线程已经继续自增变量x到2甚至3,4,5,6...</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">B线程回写临时结果到变量x ,此时覆盖了A的操作,x 又变为了2。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">此时B线程的操作就是中间状态执行期间,被其它线程并发操作了。导致回写失败。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">那怎么解决呢?最常规的办法就是加并发锁,将并发的片段同步成一个整体,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )执行期间,不允许其它线程同步操作。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">这种常规的办法就是加锁,这种锁通常是指无论并发是否发生,我先加锁,保证我在执行期间肯定不受到干扰。<span style="color: rgba(255, 0, 0, 1)"><strong><span style="color: rgba(0, 0, 0, 1)">我们将这种时刻防护并发保护数据安全的锁称之为<span style="color: rgba(255, 0, 0, 1)">悲观锁</span>。</span></strong></span></span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">这就像是游客进入地铁闸机,不管有没有其他游客准备并行进入。闸机通道,每次只限一个人操作。</span></p>
<p><img src="https://img2024.cnblogs.com/blog/704073/202511/704073-20251106150258346-1084396295.png" alt="bgszj" loading="lazy"></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">闸机的旋转门旋转 (加锁)</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">进入人 (数据操作)</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">离开闸机,进入景区 (解锁)</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">像传统的 synchronized ReentrantLock 等锁,都是悲观锁。都有典型的加锁解锁操作。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px"><img src="https://img2024.cnblogs.com/blog/704073/202511/704073-20251106150457450-1948859756.png" alt="bgs2" loading="lazy"></span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">悲观锁锁常用于竞争激烈的并发场景下。</span></p>
<p>&nbsp;</p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">除了加并发锁还有啥办法呢?</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">还可以通过状态的变化来控制。就以我们这个例子来说。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">因为出现问题的本质时因为发生了并发,我们只要判断并发有没有发生就可以。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">如果并发没发生,我直接操作有没有锁无所谓,如果并发发生了,我看下对我的影响,如果对我有影响,我就认为这次操作失败了,重新操作试下。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">我们观察有没有发生并发有两个点,开始和结束点,</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">&lt;1&gt;如果在开始点观察:其它线程有没有也同步读取数据。细想就发现这太难了,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )首先你要并发的观察所有cpu核的线程有没有读数据,这个挑战太大。而且别人可能只是简单的读取一下不操作,或者即使你能观察到,别的线程也可能先于你观察就已经读到数据了。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">这显然不可行的。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">&lt;2&gt;其次就是观察结束点,数据有没有改变。别人怎么读无所谓。这种显然是可以的。我们只要监控变量的值发生变法了没有即可判断是否发生了并发,从而判断是否可以继续写。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">如果线程读取的是1,操作回写的结果是2(新值),它就可以在回写时,判断下回写要覆盖的值是不是1(旧值)。如果是则覆盖写入,如果不是,则认为并发失败,重新尝试写入(或进行其它失败策略)。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px"><strong>这种通过判断并发是否发生才进行操作的方式,我们称之为<span style="color: rgba(255, 0, 0, 1)">乐观锁</span>。</strong></span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">生活中像各种检票系统,就是一个典型的乐观锁控制,系统只要核实票是否有效,然后更改状态即可,并不涉及到锁定,处理,解锁的并发控制</span></p>
<p><img src="https://img2024.cnblogs.com/blog/704073/202511/704073-20251107095811111-1544714273.png" alt="leguansjianpiao" loading="lazy"></p>
<p>&nbsp;</p>
<p><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">乐观并发控制一般分为三个步骤:</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">(1)读取 read</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">(2)修改 modify (计算出目标值)</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">(3)校验并提交/写入 (Validate &amp; Commit)</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px"><img src="https://img2024.cnblogs.com/blog/704073/202511/704073-20251106150457450-1948859756.png" alt="bgs2" loading="lazy"></span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">乐观锁常常用于低并发的场景中。因为它避免了悲观锁的状态切换,因此它的性能在低并发时更高,高并发下由于冲突较多,会导致比较次数较多,从而导致性能下降。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">我们业务中最常见的乐观锁,一般是在数据库层面通过where 语句来实现,</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">比如下边这个语句</span></p>
<div class="cnblogs_code">
<pre>update status =<span style="color: rgba(0, 0, 0, 1)"> '待支付'
from order
where status </span>= '已下单'</pre>
</div>
<p><span style="font-family: &quot;Microsoft YaHei&quot;">当用户下单后,校验身份</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">订单状态从初始--&gt;(check用户身份)--&gt;已下单--&gt;(锁定库存)--&gt;待支付</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">每次订单状态机发生正常业务状态跳转时,都check状态是否是预定状态,但是此时用户又可以并发的去操作订单,如取消订单,这时状态机的正常业务状态跳转就要发现被并发修改了,进而失败退出。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">这就是乐观锁的一种典型应用场景。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">像例子中这种比较变量当前值是否是预期值,如果是,就将变量值赋为心值(预期值交换为新值),如果不是则不做操作的行为</span><br><strong><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">我们称之为compare and swap 比较并交换,也就是大家常说的<span style="color: rgba(255, 0, 0, 1)">CAS</span>.</span></strong><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">CAS 是一种思路,也是乐观锁的一种实现方式,除此之外,还可以通过数据库主键控制,数据库版本号等方式来实现,但是本质都差不多,就是在写入时进行原子级别的比较并写入</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">java中已经通过unsafe类结合c++代码实现了CAS的能力,但是操作不太方便,因此JUC中atomic包下提供了各种原子类,如:</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">AtomicBoolean、AtomicInteger、AtomicReference 等。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )这些类可以直接用于业务代码的各种类型的原子操作类。(Atomic原子类/unsafe类的实现和使用,我会在后边的文章中专门讲解)</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">但是CAS操作本身是无法直接的解决ABA问题的。</span><br><strong><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px; color: rgba(255, 0, 0, 1)">什么是ABA问题,就是指CAS 在比较预期值时,虽然值等于预期值,但是可能已经发生并发了,</span></strong><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">比如线程1发现变量值为A,</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">线程1准备将A调整为C,</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">此时发生了并发,其它线程将变量值调整为了B,因为某种原因调整回A</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">线程1CAS 写入变量时,A仍然等于预期值,但是已经不是原来的A了,此时再发生写入,可能会有异常或场景遗漏。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">这种情况往往发生在变量值可以发生循环变化时,对于不会循环时,这个问题就不会产生。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">常规的解决办法就是加入一个新的变量,如版本号,版本号和每次的变量值时一一映射关系。这样即使变量值循环回去,但是版本号只会递增不会循环。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">一般的数值操作,即使有ABA场景的发生也不用担心,大部分由于最终一致性的情况,并不会对业务有什么冲击。只有很少的场景需要结合业务或者是对象内部变化,才会引发新的问题。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">最后再说下很多人提到的CAS是无锁么?(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )并不是,CAS也是乐观锁的一种实现,也是锁,虽然我们没有显示的使用,但是内部在真正实现原子操作的那个时间段内还是需要通过各种状态、指令来控制住了并发。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">比如通过数据库实现的CAS 乐观锁,那么在update时,一般会有行锁或者表锁。</span><br><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">通过atomic包下的原子类进行cas,虽然没有直接使用锁,但是在底层调用C++进而调用cpu指令cmpxchg时,还是通过lock 指令来锁定内存指令或者缓存行来保证控制并发。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">因此CAS 肯定是用到了锁,但是对于应用层面的业务来说,感知不到锁。</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">&nbsp;</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">&nbsp;</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">&nbsp;</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">&nbsp;</span></p>
<p><span style="font-family: &quot;Microsoft YaHei&quot;; font-size: 14px">&nbsp;</span></p>

</div>
<div id="MySignature" role="contentinfo">
    <p style="background: #C0FFFF; padding: 10px; border: 1px dashed #E0E0E0; font: 100% 微软雅黑; color: #F00; text-align: center">
如果你觉得写的不错,欢迎转载和点赞。
转载时请保留作者署名jilodream/王若伊_恩赐解脱(博客链接:http://www.cnblogs.com/jilodream/</p><br><br>
来源:https://www.cnblogs.com/jilodream/p/19196698
頁: [1]
查看完整版本: 浅谈java中的悲观锁,乐观锁以及CAS操作