面试题系列--乐观锁和悲观锁

一,基本概念html

  • 乐观锁:乐观锁在操做数据时很是乐观,认为别人不会同时修改数据。所以乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:若是别人修改了数据则放弃操做,不然执行操做。
  • 悲观锁:悲观锁在操做数据时比较悲观,认为别人会同时修改数据。所以操做数据时直接把数据锁住,直到操做完成后才会释放锁;上锁期间其余人不能修改数据。

注意:乐观锁本质没有锁,所以使用它能够提升代码执行效率,不会阻塞,不会等待,会重试。java

二,锁的实例算法

乐观锁:1.在Java中java.util.concurrent.atomic包下面的原子变量类(采用CAS机制)。数据库

    2.版本号机制。   编程

悲观锁:悲观锁的实现方式就是加锁,经过给代码块加锁,或数据加锁。数组

    1.给数据加锁--传统的关系型数据库中的锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。安全

    2.给代码块加锁--好比Java里面的同步原语synchronized关键字的实现也是悲观锁。多线程

1,CAS机制并发

  CAS操做方式:即compare and swap 或者compare and set ,涉及到三个操做数,数据所在的内存地址(V),预期值(A),新值(B)。当须要更新时,判断当前内存地址的值与以前取到的值是否相等,若相等,则用新值更新,若不等则重试,通常状况下是一个自旋操做,即不断的重试。源码分析

  CAS 操做中包含三个操做数 —— 须要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。若是内存位置V的现值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,不然处理器将重复匹配。不管哪一种状况,它都会在 CAS 指令以前返回该位置的值。(在 CAS 的一些特殊状况下将仅返回 CAS 是否成功,而不提取当前值。)

  CAS是乐观锁技术,当多个线程同时尝试使用CAS更新同一个变量时,只有其中一个线程能成功更新变量的值,其它线程均会失败,但失败的线程并不会被挂起,只是被告知此次竞争失败,而且容许失败的线程再次尝试,固然也容许失败的线程放弃操做。这里再强调一下,乐观锁是一种思想,CAS是这种思想的一种实现方式。虽然与加锁相比,CAS比较交换会使程序看起来更加复杂一些。但因为其非阻塞性,对死锁问题天生免疫,更重要的是,使用无锁的方式彻底没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以,它要比基于锁方式的实现拥有更优越的性能。  

  在Java1.5以前是靠synchronized关键字保证同步的,这是一种独占锁,也是一种悲观锁,是一个重量级的操做,由于加锁须要消耗额外的资源,还由于线程状态的切换会涉及操做系统核心态和用户态的转换;因此在1.5以后Java增长了并发包Java.util.concurrent.*(j.u.c)。J.U.C就是创建在CAS之上的,相对于对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。因此J.U.C在性能上有了很大的提高。不过随着JVM对锁进行的一系列优化(如自旋锁、轻量级锁、锁粗化等),synchronized的性能表现也已经愈来愈好。

  如今来介绍AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操做来保证原子性;除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。原子操做类大体能够分为4类:原子更新基本类型,原子更新数组类型,原子更新引用类型,原子更新属性类型。这些原子类中都是用了无锁的概念,有的地方直接使用了CAS机制。

  下面以 java.util.concurrent 中的 AtomicInteger 为例,看一下在不使用锁的状况下是如何保证线程安全的。主要理解 getAndIncrement 方法,该方法的做用至关于 ++i 操做。

public class AtomicInteger extends Number implements java.io.Serializable {
    //在没有锁的机制下,字段value要借助volatile原语,保证线程间的数据是可见性。
    private volatile int value;
    //Unsafe用于实现对底层资源的访问
    private static final Unsafe unsafe = Unsafe.getUnsafe();
 
    //valueOffset是value在内存中的偏移量
    private static final long valueOffset;
    //经过Unsafe得到valueOffset
    static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
 
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 
    public final int getAndIncrement() {//至关于++i操做
        for (;;) {
            int current = get();//获取值
            int next = current + 1;//+1操做
            if (compareAndSet(current, next))//current是预期值,即从主存中取来还未操做过的值,next更新后的值
                return current;
        }
    }
}

源码分析说明以下:

(1)getAndIncrement()实现的自增操做是自旋CAS操做:每次获取数据时,都要循环进行compareAndSet判断,若是执行成功则退出,不然一直执行。

(2)其中compareAndSet是CAS操做的核心,它是利用Unsafe对象实现的。其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);是借助C来调用CPU底层指令实现的。

1 public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

相似以下逻辑:

1  if (o == expected) {//this=expect
2      o = x   //this=updata
3      return true;
4  } else {
5      return false;
6  }

(3)Unsafe是用来帮助Java访问操做系统底层资源的类(如能够分配内存、释放内存),经过Unsafe,Java具备了底层操做能力,能够提高运行效率;强大的底层资源操做能力也带来了安全隐患(类的名字Unsafe也在提醒咱们这一点),所以正常状况下用户没法使用。AtomicInteger在这里使用了Unsafe提供的CAS功能。

(4)valueOffset能够理解为value在内存中的偏移量,对应了CAS三个操做数(V/A/B)中的V;偏移量的得到也是经过Unsafe实现的。

(5)value域的volatile修饰符:Java并发编程要保证线程安全,须要保证原子性、可视性和有序性;CAS操做能够保证原子性,而volatile能够保证可视性和必定程度的有序性;在AtomicInteger中,volatile和CAS一块儿保证了线程安全性。关于volatile做用原理的说明涉及到Java内存模型(JMM),这里不详细展开。

 

2.版本号机制

  通常是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一,当线程A要更新数据时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前主内存中的version值相同,则提交更新,若不相同,则重复更新,直到更新成功。

eg:updata  table  set  x=x+1,version=version+1 where id=#{id}  and  version=#{version};

  对于这句代码来讲,若是可以使用id+version查到数据,说明该数据没有再被修改过。若是查询不到,则说明数据被修改过,则会重复查询。

  须要注意的是,这里使用了版本号做为判断数据变化的标记,实际上能够根据实际状况选用其余可以标记数据版本的字段,如时间戳等。

悲观锁机制存在如下问题:  

  1. 在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题。

  2. 一个线程持有锁会致使其它全部须要此锁的线程挂起。

  3. 若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。

  对比于悲观锁的这些问题,另外一个更加有效的锁就是乐观锁。其实乐观锁就是:每次不加锁而是假设没有并发冲突而去完成某项操做,若是由于并发冲突失败就重试,直到成功为止。

要注意乐观锁的实现本质是没有使用锁的:

(1)乐观锁自己是不加锁的,只是在更新时判断一下数据是否被其余线程更新了;AtomicInteger即是一个例子。

(2)有时乐观锁可能与加锁操做合做,例如,在前述updateCoins()的例子中,MySQL在执行update时会加排它锁。但这只是乐观锁与加锁操做合做的例子,不能改变“乐观锁自己不加锁”这一事实。

三,乐观锁和悲观锁的使用场景

一、功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,不管是CAS仍是版本号机制。

例如,CAS只能保证单个变量操做的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则能够经过对整个代码块加锁来处理。再好比版本号机制,若是query的时候是针对表1,而update的时候是针对表2,也很难经过简单的版本号来实现乐观锁。

二、竞争激烈程度

若是悲观锁和乐观锁均可以使用,那么选择就要考虑竞争的激烈程度:

  • 当竞争不激烈 (出现并发冲突的几率小)时,乐观锁更有优点,由于悲观锁会锁住代码块或数据,其余线程没法同时访问,影响并发,并且加锁和释放锁都须要消耗额外的资源。
  • 当竞争激烈(出现并发冲突的几率大)时,悲观锁更有优点,由于乐观锁在执行更新时频繁失败,须要不断重试,浪费CPU资源。

四,CAS机制的缺点

一、ABA问题

假设有两个线程——线程1和线程2,两个线程按照顺序进行如下操做:

(1)线程1读取内存中数据为A;

(2)线程2将该数据修改成B;

(3)线程2将该数据修改成A;

(4)线程1对数据进行CAS操做

在第(4)步中,因为内存中数据仍然为A,所以CAS操做成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

在AtomicInteger的例子中,ABA彷佛没有什么危害。可是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶通过两次(或屡次)变化又恢复了原值,可是栈可能已发生了变化。

对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操做时,不只比较内存中的值,也会比较版本号,只有当两者都没有变化时,CAS才能执行成功。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

二、高竞争下的开销问题

在并发冲突几率大的高竞争环境下,若是CAS一直失败,会一直重试,CPU开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过必定阈值后失败退出。固然,更重要的是避免在高竞争环境下使用乐观锁。

三、功能限制

CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操做的原子性,这意味着当涉及到多个变量(内存值)时,CAS也无能为力。

除此以外,CAS的实现须要硬件层面处理器的支持,在Java中普通用户没法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。

 

推荐:http://www.javashuo.com/article/p-qpuqpsfi-gk.html

相关文章
相关标签/搜索