悲观锁和乐观锁以及CAS机制

讨厌什么变成什么 2019-06-27

悲观锁:

认为每次获取数据的时候数据一定会被人修改,所以它在获取数据的时候会把操作的数据给锁住,这样一来就只有它自己能够操作,其他人都堵塞在那里。

乐观锁:

认为每次获取数据的时候数据不会被别人修改,所以获取数据的时候并没有锁住整个数据,但是在更新的时候它会去判断一下要更新的数据有没有被别人修改过,例如更新前查询该数据的版本号,更新的时候看看该版本号有没有被人修改过,如果被人修改过了,那就不会去更新。

应用场景:

悲观锁:
因为悲观锁会锁住数据,让其他人都等待,所以当一个系统并发量不大,而且可以接收一定延迟的时候可以选择悲观锁。
乐观锁:
因为乐观锁会在更新前去查数据,所以比较适合读多少写的场景,因为写操作多的话会造成大量的查询操作,给系统带来压力。例如SVN、Git等版本控制管理器就是应用的乐观锁,当你提交数据的时候对比下版本号,如果远程仓库的版本号和本地的不一样就表示有人已经提交过代码了,你需要先更新代码到本地处理一下版本冲突问题,不然是没有办法提交的。

CAS:

CAS是Compare And Set的缩写,中文意思就是比较和操作,是一个非阻塞算法。它其实是一个CPU的指令,它会拿内存值和一个给定的值进行比较,如果相等的话就会把内存值更新为另一个给定的值。其实CAS就是使用一个乐观锁的机制。

Java中CAS机制的应用:

从JDK1.5开始java.util.concurrent.atomic包中新增了一些原子类,AtomicInteger、AtomicLong等等,就是专门解决高并发下的同步问题。因为类似i++、++i的操作不是线程安全的,以前我们都会使用Synchronized关键字,但是现在我们直接使用这些原子类就可以解决线程安全的问题。下面用代码来看看有什么变化。

class Test1 {
    private volatile int count = 0;

    public synchronized void increment() {
    //加锁才能保证线程安全
        count++; 
    }

    public int getCount() {
        return count;
    }
}

class Test2 {
    private AtomicInteger count = new AtomicInteger();

    //使用AtomicInteger之后,不加锁,也是线程安全的。
    public void increment() {
        count.incrementAndGet();
    }
   
    public int getCount() {
        return count.get();
    }
}

下面这些是AtomicInteger提供的别的方法。

//获取当前的值
public final int get() 
//获取当前的值,并设置新的值
public final int getAndSet(int newValue)
//获取当前的值,并自增
public final int getAndIncrement()
//获取当前的值,并自减
public final int getAndDecrement() 
//获取当前的值,并加上预期的值
public final int getAndAdd(int delta)

我们从源码的角度看看AtomicInteger是怎么实现CAS机制的。unsafe是java提供的用来获取对象内存地址的类,作用是在更新操作时提供“比较并替换”的作用。valueOffset是记录value本身在内存的地址,value被声明为volatile是保证在更新操作时,当前线程可以拿到value最新的值。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

比如incrementAndGet方法,是获取当前的值并自增。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

我们进getAndAddInt方法看看,getIntVolatile和compareAndSwapInt都是本地方法,就是通过本地方法来实现CAS机制。确保不出现线程安全问题。

public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var4));
        return var5;
    }
    
    
 public native int getIntVolatile(Object var1, long var2);
 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS可能会引起的问题

  1. 因为CAS并不会锁住数据让其他线程阻塞,所以实际上是自旋锁的原理。自旋锁就是当线程获取锁的时候发现这个锁已经被别的线程抢了,它不是阻塞自己,而是一直循环查看这个锁有没有被释放,这就叫自旋锁。因为一直循环查看所以可以能会造成CPU负担过重,最好设置参数限制查看锁的次数。
  2. 死锁问题,有一个线程拿到自旋锁之后,又去拿锁,例如递归的时候会出现这样的情况,自己等待自己释放缩,卡在那里不动。
  3. ABA问题,这个问题就是说当线程1读到内存值为A,然后线程2进来了把内存值改为B,然后又改为了A,这个时候线程1觉得没有问题,就更新了。一般在数据库中使用乐观锁都会拿版本号作为对比值,因为版本号会一直增加,没有重复的,所以不会出现这个问题。Java中也提供了AtomicStampedReference这个类,大致原理也是提供一个版本号来对比。

相关推荐

xusong / 0评论 2012-05-29