一、悲观并发控制
1.1 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,
这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
1.2 悲观锁中包含的概念
`读写锁(又叫共享互斥锁)`
'共享锁、读锁是同一概念
互斥锁、排他锁、写锁是同一概念'
`写锁`
一个写锁会阻塞其他的写锁和读锁,处于安全,只有这样才能保证在给定的时间里只有一个用户能执行写入,
并防止其他用户读取正在写入的同一个资源
`锁粒度:`
锁粒度时一种提高共享资源并发性的方式,是让锁定对象更有选择性,尽量只锁定需要修改的部分数据,而不是所有的资源,
只对修改的数据片进行精确的锁定任何时候,在给定的资源上,锁定的数据量越少,则系统的并发成都越高,只要互相不发生冲突即可
锁的各种操作,包括获得锁,检查锁是否已经解除,释放锁等,都会增加系统的消耗,如果系统花大量的时间来管理锁,
而不是存取数据,那么系统的可能性就会受到影响
锁策略就是在锁的开销和数据的安全性之间寻求平衡,一般都是在表上施加行级锁(row-level lock) 并以复杂方式进行实现,
以便在锁比较多的情况下尽可能提供更好的性能
mysql存储引擎都可以实现自己的锁策略和锁粒度,在存储引擎的设计中锁管理是非常重要的决定,将锁粒度固定在某个级别,
可以为某些特定的应用场景提供更好的性能,同时也会失去另一些应用场景的良好支持
mysql有多个存储引擎,每个引擎都有不同的特点和应用场景
`表锁(table lock)`
表锁时开销最小的,表锁会锁定整张表,一个用户在对标进行写,插入,删除,更新等操作前,需要先获取到写锁,
这会阻塞其他用户对该表的所有读写操作,只有没有写锁时,其他用户才可以获得读锁,读锁之间时不相互阻塞的
表锁支持某些类型的并发写操作,另外写锁也比读锁有更高优先级,一个械所请求可能会插入到读锁队列的前面,
读锁不可以插到写锁的前面,服务器会为例如alter table之类的语句使用表锁,而忽略存储引擎锁机制
`行级锁(row lock)`
行级锁可以最大程度支持并发处理,同时开销也是最大的锁开销,在innoDB ,XtraDB 实现了行级锁,行级锁旨在存储引擎层实现,
而服务层没有实现,服务层完全不了解存储引擎中锁的实现,所有的存储引擎都以自己的方式实现了锁的机制
二、乐观并发控制结合上篇文章查看 四、乐观并发控制<方式二>
2.1 乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,
可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,
其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
2.2 乐观锁中包含的概念
基于时间戳的验证
基于版本号的验证
CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,
也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。
一般情况下是一个自旋操作,即不断的重试。
三、乐观锁的缺点
1 ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,
因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,
并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline,
使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush,
从而提高CPU的执行效率。
3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,
提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.
所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
四、使用场景
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,
所以一般多写的场景下用悲观锁就比较合适。
CAS与synchronized的使用情景
简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;
而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。
但是,在'JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下
并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,
但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS'。
参考文章:
https://blog.csdn.net/qq_36441659/article/details/108720577
https://blog.csdn.net/qq_34337272/article/details/81072874