何谓悲观,就是假象所有的事情都是向坏的方面发展。
当线程去取用数据的时候都认为别人有可能会去修改它,因此取数据时会进行上锁操作。
数据上锁后,如果其他人向拿这部分数据,就需要阻塞等待,直到锁被解除。
比如:共享数据每次只给一个线程使用,其它线程阻塞,用完后再转让给其它线程。
传统的关系型数据库里就用到这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。(当然JDK1.8后 synchronized 做了一系列的优化,加入了自旋锁和偏向锁)
所谓乐观,就是假象所有的事情都是向好的方面发展。
当线程去取用数据的时候都认为别人不会动这部分数据,因此不会主动进行上锁行为。
而在更新数据之前,会判定一下数据是不是产生过变化。
乐观锁其实并不是锁,也不会产生锁的行为,但是会循环执行与重试,从而保证数据的一致性。
据库提供的write_condition 机制,其实都是提供的乐观锁。
常用的实现形式是版本号机制和 CAS 算法实现。
Java 中 java.util.concurrent.atomic 包下面的原子变量类,使用的 CAS 实现乐观锁。
悲观锁与乐观锁本质上没有好坏区分,各有优缺点,所应对的业务场景有所区别。
乐观锁适用于写比较少的情况下(多读场景),即很少发生冲突,省去了锁的开销,加大系统吞吐量。
悲观锁适用于多写的情况,经常产生冲突,如果使用乐观锁会导致应用会不断的进行重试,降低性能。
悲观锁就是硬性对数据锁定,因此没有什么特别的实现。
乐观锁一般会使用版本号机制或 CAS 算法实现。
数据中增加一个版本号字段,每次修改数据的时候会自增这个版本号。
线程要更新数据操作时,先读取数据也会读取版本号,正式提交更新,需要用读取的版本号和数据库中最新数据的版本号相当方执行操作。
如果发现版本号产生变化,则需要重新读取数据再次执行刚刚的处理操作,直到更新成功。
举例:
Compare And Swap(比较与交换)是一种有名的无锁算法。
不使用锁的情况下实现多线程之间的变量同步,在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blockingSynchronization)。
CAS 算法涉及到三个操作:
仅当 V == A 时,CAS 通过原子方式用新值 B 来更新 V 的值。
否则不会执行任何操作(比较和替换是一个原子操作)。
一般情况下是一个自旋操作,即不断的重试。
ABA问题是实现最常见的问题。
因为版本号或者变量是可变的,如果某个线程将变量有A改为B,然后又改成了A。
在同一时间的其他线程会误以为没有产生更新,就会导致覆写的问题。
ABA问题的处理方案就是在确认修改前,我们要知道变量有没有被修改过或者修改了几次,从而判断是否需要执行重试。
JDK1.5 以后的 AtomicStampedReference 类就提供了此种能力,compareAndSet 方法就是首先检查当前引用是否等于预期引用。
其主要用于确认变量是否被修改过,当前标志是否等于预期标志,全部相等,则以原子方式将修改值。
CAS 的本质就是不断重试(不成功就一直循环执行直到成功)。
长时间不成功,会给CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用。
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
比如JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性。
可以把多个变量放在一个对象里来进行 CAS 操作。
当前还没有观点发布,欢迎您留下足迹!