目录html
Java 按照锁的实现分为乐观锁和悲观锁,乐观锁和悲观锁并非一种真实存在的锁,而是一种设计思想,乐观锁和悲观锁对于理解 Java 多线程和数据库来讲相当重要,那么本篇文章就来详细探讨一下这两种锁的概念以及实现方式。java
悲观锁
是一种悲观思想,它总认为最坏的状况可能会出现,它认为数据极可能会被其余人所修改,因此悲观锁在持有数据的时候总会把资源
或者 数据
锁住,这样其余线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。悲观锁的实现每每依靠数据库自己的锁功能实现。mysql
Java 中的 Synchronized
和 ReentrantLock
等独占锁(排他锁)也是一种悲观锁思想的实现,由于 Synchronzied 和 ReetrantLock 不论是否持有资源,它都会尝试去加锁,生怕本身心爱的宝贝被别人拿走。算法
乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,因此读取不会上锁,可是乐观锁在进行写入操做的时候会判断当前数据是否被修改过(具体如何判断咱们下面再说)。乐观锁的实现方案通常来讲有两种: 版本号机制
和 CAS实现
。乐观锁多适用于多度的应用类型,这样能够提升吞吐量。sql
在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。数据库
上面介绍了两种锁的基本概念,并提到了两种锁的适用场景,通常来讲,悲观锁不只会对写操做加锁还会对读操做加锁,一个典型的悲观锁调用:编程
select * from student where name="cxuan" for update
这条 sql 语句从 Student 表中选取 name = "cxuan" 的记录并对其加锁,那么其余写操做再这个事务提交以前都不会对这条数据进行操做,起到了独占和排他的做用。安全
悲观锁由于对读写都加锁,因此它的性能比较低,对于如今互联网提倡的三高
(高性能、高可用、高并发)来讲,悲观锁的实现用的愈来愈少了,可是通常多读的状况下仍是须要使用悲观锁的,由于虽然加锁的性能比较低,可是也阻止了像乐观锁同样,遇到写不一致的状况下一直重试的时间。多线程
相对而言,乐观锁用于读多写少的状况,即不多发生冲突的场景,这样能够省去锁的开销,增长系统的吞吐量。并发
乐观锁的适用场景有不少,典型的好比说成本系统,柜员要对一笔金额作修改,为了保证数据的准确性和实效性,使用悲观锁锁住某个数据后,再遇到其余须要修改数据的操做,那么此操做就没法完成金额的修改,对产品来讲是灾难性的一刻,使用乐观锁的版本号机制可以解决这个问题,咱们下面说。
乐观锁通常有两种实现方式:采用版本号机制
和 CAS(Compare-and-Swap,即比较并替换)算法
实现。
版本号机制是在数据表中加上一个 version
字段来实现的,表示数据被修改的次数,当执行写操做而且写入成功后,version = version + 1,当线程A要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version值相等时才更新,不然重试更新操做,直到更新成功。
咱们以上面的金融系统为例,来简述一下这个过程。
金额
和 version
,金额的属性是可以实时变化,而 version 表示的是金额每次发生变化的版本,通常的策略是,当金额发生改变时,version 采用递增的策略每次都在上一个版本号的基础上 + 1。上面两种状况是最乐观的状况,上面的两个事务都是顺序执行的,也就是事务一和事务二互不干扰,那么事务要并行执行会如何呢?
事务一开启,男柜员先执行读操做,取出金额和版本号,执行写操做
begin update 表 set 金额 = 120,version = version + 1 where 金额 = 100 and version = 0
此时金额改成 120,版本号为1,事务尚未提交
事务二开启,女柜员先执行读操做,取出金额和版本号,执行写操做
begin update 表 set 金额 = 50,version = version + 1 where 金额 = 100 and version = 0
此时金额改成 50,版本号变为 1,事务未提交
如今提交事务一,金额改成 120,版本变为1,提交事务。理想状况下应该变为 金额 = 50,版本号 = 2,可是实际上事务二 的更新是创建在金额为 100 和 版本号为 0 的基础上的,因此事务二不会提交成功,应该从新读取金额和版本号,再次进行写操做。
这样,就避免了女柜员 用基于 version=0 的旧数据修改的结果覆盖男操做员操做结果的可能。
先来看一道经典的并发执行 1000次递增和递减后的问题:
public class Counter { int count = 0; public int getCount() { return count; } public void setCount(int count) { this.count = count; } public void add(){ count += 1; } public void dec(){ count -= 1; } }
public class Consumer extends Thread{ Counter counter; public Consumer(Counter counter){ this.counter = counter; } @Override public void run() { for(int j = 0;j < Test.LOOP;j++){ counter.dec(); } } } public class Producer extends Thread{ Counter counter; public Producer(Counter counter){ this.counter = counter; } @Override public void run() { for(int i = 0;i < Test.LOOP;++i){ counter.add(); } } } public class Test { final static int LOOP = 1000; public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Producer producer = new Producer(counter); Consumer consumer = new Consumer(counter); producer.start(); consumer.start(); producer.join(); consumer.join(); System.out.println(counter.getCount()); } }
屡次测试的结果都不为 0,也就是说出现了并发后数据不一致的问题,缘由是 count -= 1 和 count += 1 都是非原子性操做,它们的执行步骤分为三步:
若是要把证它们的原子性,必须进行加锁,使用 Synchronzied
或者 ReentrantLock
,咱们前面介绍它们是悲观锁的实现,咱们如今讨论的是乐观锁,那么用哪一种方式保证它们的原子性呢?请继续往下看
CAS 即 compare and swap(比较与交换)
,是一种有名的无锁算法。即不使用锁的状况下实现多线程之间的变量同步,也就是在没有线程被阻塞的状况下实现变量的同步,因此也叫非阻塞同步(Non-blocking Synchronization
CAS 中涉及三个要素:
当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。
JAVA对CAS的支持:在JDK1.5 中新添加 java.util.concurrent (J.U.C) 就是创建在 CAS 之上的。对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种实现。因此J.U.C在性能上有了很大的提高。
咱们以 java.util.concurrent 中的AtomicInteger
为例,看一下在不用锁的状况下是如何保证线程安全的
public class AtomicCounter { private AtomicInteger integer = new AtomicInteger(); public AtomicInteger getInteger() { return integer; } public void setInteger(AtomicInteger integer) { this.integer = integer; } public void increment(){ integer.incrementAndGet(); } public void decrement(){ integer.decrementAndGet(); } } public class AtomicProducer extends Thread{ private AtomicCounter atomicCounter; public AtomicProducer(AtomicCounter atomicCounter){ this.atomicCounter = atomicCounter; } @Override public void run() { for(int j = 0; j < AtomicTest.LOOP; j++) { System.out.println("producer : " + atomicCounter.getInteger()); atomicCounter.increment(); } } } public class AtomicConsumer extends Thread{ private AtomicCounter atomicCounter; public AtomicConsumer(AtomicCounter atomicCounter){ this.atomicCounter = atomicCounter; } @Override public void run() { for(int j = 0; j < AtomicTest.LOOP; j++) { System.out.println("consumer : " + atomicCounter.getInteger()); atomicCounter.decrement(); } } } public class AtomicTest { final static int LOOP = 10000; public static void main(String[] args) throws InterruptedException { AtomicCounter counter = new AtomicCounter(); AtomicProducer producer = new AtomicProducer(counter); AtomicConsumer consumer = new AtomicConsumer(counter); producer.start(); consumer.start(); producer.join(); consumer.join(); System.out.println(counter.getInteger()); } }
经测试可得,无论循环多少次最后的结果都是0,也就是多线程并行的状况下,使用 AtomicInteger 能够保证线程安全性。 incrementAndGet 和 decrementAndGet 都是原子性操做。本篇文章暂不探讨它们的实现方式。
任何事情都是有利也有弊,软件行业没有完美的解决方案只有最优的解决方案,因此乐观锁也有它的弱点和缺陷:
ABA 问题说的是,若是一个变量第一次读取的值是 A,准备好须要对 A 进行写操做的时候,发现值仍是 A,那么这种状况下,能认为 A 的值没有被改变过吗?能够是由 A -> B -> A 的这种状况,可是 AtomicInteger 却不会这么认为,它只相信它看到的,它看到的是什么就是什么。
JDK 1.5 之后的 AtomicStampedReference
类就提供了此种能力,其中的 compareAndSet 方法
就是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
也能够采用CAS的一个变种DCAS来解决这个问题。
DCAS,是对于每个V增长一个引用的表示修改次数的标记符。对于每一个V,若是引用修改了一次,这个计数器就加1。而后再这个变量须要update的时候,就同时检查变量的值和计数器的值。
咱们知道乐观锁在进行写操做的时候会判断是否可以写入成功,若是写入不成功将触发等待 -> 重试机制,这种状况是一个自旋锁,简单来讲就是适用于短时间内获取不到,进行等待重试的锁,它不适用于长期获取不到锁的状况,另外,自旋循环对于性能开销比较大。
简单的来讲 CAS 适用于写比较少的状况下(多读场景,冲突通常较少),synchronized 适用于写比较多的状况下(多写场景,冲突通常较多)
补充: Java并发编程这个领域中 synchronized 关键字一直都是元老级的角色,好久以前不少人都会称它为 “重量级锁” 。可是,在JavaSE 1.6以后进行了主要包括为了减小得到锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各类优化以后变得在某些状况下并非那么重了。synchronized 的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但得到了高吞吐量。在线程冲突较少的状况下,能够得到和 CAS 相似的性能;而线程冲突严重的状况下,性能远高于CAS。
欢迎关注我本人的公众号,公号回复002有你想要的一切
相关参考:
https://baike.baidu.com/item/悲观锁