Java并发问题之乐观锁与悲观锁

悲观锁和乐观锁时两种不同思路的锁,这两种锁机制是在多用户环境下并发控制的锁机制。

悲观锁

悲观锁又可以称之为互斥同步,是一种常见并发正确性保障手段。该思想总是设想最坏的情况,每当有一个线程去取数据的时候,总会认为这时候会有其他线程来修改数据,因此每当有一个线程去取数据的时候都会对该数据上锁,此时其他想对该数据进行操作的线程都会被阻塞挂起,直到该数据的锁被释放。在数据库中经常会用到这种锁机制,比如行锁,表锁等,读锁,写锁等;在java中同步原语关键字synchronized的实现就是悲观锁。

优点

  • 可以保证共享数据在同一个时刻只被一个线程使用

缺点

  • 进行线程阻塞和唤醒会带来性能问题,java的线程是映射到操作系统的原生线程之上的,如果阻塞或者唤醒一个线程,都需要操作系统的帮忙,这就需要从用户态转换到核心态,而状态转换是要耗费很多处理器的时间。
  • 如果一个高优先级的线程要等低优先级的线程释放锁才能执行会导致优先级倒置,这会引起性能风险。

乐观锁

乐观锁又可以称之为非阻塞同步。该思想总是很乐观,总认为在一个线程取共享数据的时候,其他线程不会对该共享数据进行修改,因此不会上锁。但是在更新的时候会判断一下在此期间其他线程对共享数据有没有进行改动。乐观锁的实现方式一般有两种:版本号机制CAS(Compare and Swap比较并交换)操作。

版本号机制

一般在数据表中加一个数据版本号version字段,表示当前数据修改的次数,每次对数据修改时,version加一;当有线程要更新数据时,在读取数据的同时还会读取version值,再提交更新的时候会再次读取version值,然后判断当前version值和之前的version值是否相同,如果相同则提交成功,如果失败则重复上述操作直到成功为止。

CAS

CAS是乐观锁技术,当多个线程同时对共享数据进行修改时,只有一个线程才能成功修改共享数据,其他线程都会失败但并不会被阻塞挂起,而是继续尝试修改。
CAS操作需要有三个操作数,分别是需要读写的内存位置(在Java中可以简单理解为变量的内存地址用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS操作执行的时候当且内存位置V和预期原值A相匹配时,处理器就会用新值B更新V的值,否则处理器将不执行更新操作。但是无论是否更新了V值,都会返回V的值,上述操作为原子操作。简单来说就是,我们认为位置V应该是A的值,如果实际上真的是A的值,我们将B的值放在V的位置上

CAS在Java中的实现示例

JDK1.5之后新增的java.util.concurent(J.U.C)提供了CAS操作。
以java.util.concurent中的AtomicInteger为例看一下在不用锁的情况下如何保证线程安全的。

public class AtomicInteger extends Number implements java.io.Serializable {  
    private volatile int value; 

    public final int get() {  
        return value;  
    }  

    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  

    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}

在没有锁的情况下,用volatitle来保证value在线程间的可见性(当一个线程修改了共享数据后其他线程可以读到共享数据修改后的值),在上面最重要的是理解getAndIncrement这个方法,它采用了CAS操作,每次从内存中读取共享数据的值然后将该数据和该数据加一后的值进行CAS操作,如果成功返回读取的结果,否则一直重复上述操作直到成功为止。
其中 compareAndSet 利用JNI(Java Native Interface)来完成CPU指令的操作:

public final boolean compareAndSet(int expect, int update) {   
     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 }  

其中unsafe.compareAndSwapInt(this, valueOffset, expect, update);类似如下逻辑:

1 if (this == expect) {
2     this = update
3     return true;
4 } else {
5     return false;
6 }

是不是和CAS的原理很类似。

缺点

  • ABA问题:如果一个变量V初次读取时是A的值,在再次被读取的期间,先被其他线程修改成B又被修改成A,则CAS操作会认为它从来没有被修改过,显示这是错误的。J.U.C为了解决这个问题提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。
  • 循环时间开销大 自旋CAS(不成功则一直循环执行直到成功);但是如果长时间不成功则会浪费大量的cpu占用时间
  • 只能保证一个共享变量的原子操作 对于一个共享变量执行操作时,我们可以使用循环CAS操作来保证原子操作;但是对于多个共享变量,循环cas操作无法保证原子性;这个时候我们可以选择锁或者将多个变量变成一个变量;在JDK1.5引入了AutomicReference类保证引用对象的原子性,即我们可以将多个变量放在一个对象里面进行CAS操作。

synchronized和CAS性能比较

  • 对于资源竞争较少的情况,即一个线程在更新共享数据时其他线程在此时操作共享数据的概率较小的情况下,如果使用synchronized同步锁要进行线程阻塞和唤醒以及用户态的切换,这样会浪费cpu的资源,此时使用CAS操作的话,自旋概率较低,花费的资源较少,因此性能较高。
  • 对于资源竞争严重的情况下,CAS自旋概率较大,因此开销较大,所以适合选用synchronized。

参考文章

Java并发问题–乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
《深入理解JAVA虚拟机》第二版周志明第十三章

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值