悲观锁和乐观锁

乐观锁和悲观锁介绍

乐观锁:乐观锁是一种思想,每次取数据的时候都默认其他人不会去修改这个数据,不去上锁。但是在更新这个数据的时候会去判断此数据是否被修改,一般我们会使用类似版本号的机制去确认数据是否被修改。乐观锁一般适用于多读的应用场景以此来提高吞吐量,数据库提供的write_condition机制,其实就是提供的乐观锁。atomic包下面的原子类就是使用了乐观锁的一种实现方式CAS(Conmpare And Swap)实现的。
悲观锁:总是做最坏的打算,每次去获取数据的时候都会加锁,以阻塞他人。在关系数据库里面就用到了很多这种锁的机制,如行锁,表锁等,都是在操作前先上锁。

CAS(Compare and Swap 比较并交换)

CAS是乐观锁的一种实现方式,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败被告知这次修改失败。需要重新尝试。
CAS操作中包含三个操作数,读取内存位置,进行预期值比较,写入新值。如果内存位置的值和预期值一样,那么处理去就会自动将该位置更新为新值,否则不做任何操作。
CAS的缺点:
1、ABA问题
比如说一个线程R1从内存中取出A,这个时候另一个线程R2也从内存中取出这个A,并且R2线程对A进行了一些列的操作编程了B,之后又做了其他操作变成了A,这个时候R1线程进行CAS操作的时候会发现内存中的A值一样的,R1操作成功。比如链表的头在变化了两次后恢复到了原值,但是不代表整个链表没有变化。
在JDK的atomic包里面提供了一个类AtomicStampedReference / AtomicMarkableReference来解决ABA问题。主要是在对象中额外的增加一个标记来识别对象是否有过变更。
2、自旋CAS
自旋CAS就是如果不成功,就会一直循环执行,直到成功为止。然而如果长时间不成功,循环时间过长,会给CPU带来非常大的执行开销。(如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候后因内存顺序冲突(memiry order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率)
3、只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁,或者将多个共享变量合并成一个共享变量来操作。JDK也提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里面来惊喜CAS操作。

CAS与Synchronized的使用情景;
1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,单获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能。
2、对于资源竞争严重(线程冲突严重)的情况。CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。这种情况下,synchronized性能远高于CAS。

插入一下concurrent包的实现

由于java的CAS同时具有volatile读写的内存语义,因为Java线程之间的通信现在有了下面四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量

Java的CAS会使用现代处理器上提供的高效机器级别原子指令(这些原子指令以原子方式对内存执行读-写操作,这是在处理器中实现同步的关键)。同时,volatile变量的读/写和CAS可实现线程之间的通信。这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  • 首先,声明共享变量为volatile;
  • 然后,使用CAS的原子条件更新来实现线程之间的同步;
  • 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现多线程之间的通信。

AQS(AbstractQueuedSynchronized),非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。
JVM中的CAS(堆中对象的分配):
Java调用new object()会创建一个对象,这个对象会被分配到JVM的堆中。那么这个对象到底是怎么在堆中保存的呢?首先,new Objec()执行的时候,这个对象需要多大的空间,其实是已经确定的,因为java中的各种数据类型,占用多大的空间都是固定的。接下来就是在堆中找出那么一块空间用于存放这个对象。
在单线程的情况下,一般有两种分配策略:

  1. 指针碰撞:这种一般适用于内存是绝对规整的(内存是否规整取决于内存回收策略),分配空间的工作只是将指针像空闲内存一侧移动对象大小的距离即可。
  2. 空闲列表:这种适用于内存非规整的情况,这种情况下JVM会为维护一个内存列表,记录那些内存区域是空闲的,大小是多少。给对象分配空间的时候去空闲列表里面查询到合适的区域然后进行分配即可。

但是JVM不可能一直在单线程状态下运行,那样效率太差了。由于再给一个对象分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。解决并发时的安全问题也有两钟策略:

  1. CAS:实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原来和上讲的一样。
  2. TLAB:如果使用CAS其实对性能还是会有影响的,所以JVM又提出了一种更高级的优化策略;每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TL:AB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光,需要重新分配内存的时候才会进行CAS操作分配更大的内存空间。

虚拟机是否使用TLAB,可以通过-XX:+/-UseTL:AB参数来进行配置(JDK5以后的版本默认是启动TLAB)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值