JAVA锁原理之 CAS原子操做篇

原子操做是什么?


原子操做(atomic operation)指的是由多步操做组成的一个操做。若是该操做不能原子地执行,则要么执行完全部步骤,要么一步也不执行,不可能只执行全部步骤的一个子集。java

现代操做系统中,通常都提供了原子操做来实现一些同步操做,所谓原子操做,也就是一个独立而不可分割的操做。在单核环境中,通常的意义下原子操做中线程不会被切换,线程切换要么在原子操做以前,要么在原子操做完成以后。更普遍的意义下原子操做是指一系列必须总体完成的操做步骤,若是任何一步操做没有完成,那么全部完成的步骤都必须回滚,这样就能够保证要么全部操做步骤都未完成,要么全部操做步骤都被完成。mysql

例如在单核系统里,单个的机器指令能够当作是原子操做(若是有编译器优化、乱序执行等状况除外);在多核系统中,单个的机器指令就不是原子操做,由于多核系统里是多指令流并行运行的,一个核在执行一个指令时,其余核同时执行的指令有可能操做同一块内存区域,从而出现数据竞争现象。多核系统中的原子操做一般使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操做时,其余CPU核必须中止对内存操做或者不对指定的内存进行操做,这样才能避免数据竞争问题算法

CAS是什么?


CAS全程为Compare and Swap即比较再交换sql

jdk5增长了并发包java.util.concurrent简称JUC,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。JDK 5以前Java语言是靠synchronized关键字保证同步的,这是一种排斥锁也是悲观锁安全

线程独享和线程共享及java的锁种类


某个资源只给一个线程享用称之为线程独享反之为线程共享多线程

在平常开发时,须要肯定好哪些资源是线程共享的,共享的场景是什么.才能更好的去使用不一样的锁策略,保证对资源操做的原子性.并发

java的锁种类分为8种高并发

  • 公平锁/非公平锁
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

不具有原子性操做的例子


资源类Resources性能

class Resources {
    
    private volatile int i = 0;
    
    public void add() {
        i++;
    }
    
    public int getI() {
        return i;
    }
}
复制代码

测试类Demo测试

public class Demo {

    public static void main(String[] args) {
        // 资源类实例
        final Resources resources = new Resources();

        // 定长线程池-线程数10个
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 循环打印-启动10个线程
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                public void run() {
                    // 每一个线程对资源类的i进行+1
                    for (int i1 = 0; i1 < 1000; i1++) {
                        resources.add();
                    }
                }
            });
        }

        // 阻塞主线程 - 等待线程池执行完毕
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 打印i的值,预期值应该是 10*1000*1 = 10000 才算合理
        System.out.println("I的值打印为:"+resources.getI());
    }
}
复制代码

运行结果为:I的值打印为:8939

运行结果中获得一个结论,多线程对Resources资源类中的add()方法进行i++,是线程不安全的。

有的小伙伴就很奇怪了,只是一行i++代码为何会是不安全的呢?不是说多步操做因为CPU多核同时运行才会不安全吗?可i++明明就一行代码啊

其实我一开始也是这么认为的,为何一行代码会出现线程不安全的问题,因而带着疑问反编译了java代码后发现i++是一行代码可是却不是一步操做.反编译命令:javap -c Resources.class

反编译后的Resources代码以下:

class com.cas.Resources {
  com.cas.Resources();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field i:I
       9: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field i:I
      10: return

  public int getI();
    Code:
       0: aload_0
       1: getfield      #2                  // Field i:I
       4: ireturn
}
复制代码

仔细观察add方法翻译JVM指令以下

  • aload_0 : 从局部变量表的相应位置装载一个对象引用到操做数栈的栈顶
  • dup : 表示数据重复定义,也就是复制操做数
  • getfield : 获取I存放在堆的值
  • iconst_1 : 当int取值1~5时,JVM采用iconst指令将常量压入栈中
  • iadd : 计算i的值,说白了就是i+1
  • putfield : 将计算I的结果写到堆内存中
  • return : 结束方法

从字节码指令看出,i++其实是通过3个主要步骤

  • 从堆内存中获取到i的值而且复制一份到当前线程的操做数栈中
  • 从当前线程的操做数栈中去计算i的值
  • 计算后结果写回到堆内存中的i去

那么基本上能够肯定不安全的点在于每一个线程预先保留好从堆内存中获取到的i值到操做数栈(线程临时存储区),而后将操做数栈的值计算后再写回到堆内存中。这样的过程就会发生数据脏读的问题了

以下图:

image-20200113161836693

从图中展现若是两个线程都对当前线程的操做数栈中的变量i进行+1,致使明明两个线程共加了两次结果却只加了1

JUC中的原子性操做类

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference

这里就举例一些经常使用的几个类,想要了解更多的朋友们可查看JDK中java.util.concurrent.atomic包下的类,此包下全部的类都是原子性操做的

image-20200113163221637

本文将使用AtomicInteger这个号称保证线程安全的int类型原子包装类进行一次测试

资源类稍微作下修改,将int i 声明为AtomicInteger i

资源类

class Resources {

    private volatile AtomicInteger i = new AtomicInteger(0);

    public void add() {
        i.getAndAdd(1);
    }

    public int getI() {
        return i.get();
    }
}
复制代码

运行结果为:I的值打印为:10000

很显然,当咱们使用AtomicInteger进行增长的时候,i在多线程的操做下准确的计算到了10000,这个值是正确的。可是为何AtomicInteger能让i线程安全呢?会不会是使用了synchronized关键字呢?让咱们深刻一探究竟

image-20200113164412989

image-20200113165044633

点进去发现只用了一行代码搞定,并且尚未锁相关代码,可是调用了一个方法,不会是那个方法加了锁吧,来猜想观察一下调用getAndAddInt方法的三个传参是什么意思

unsafe.getAndAddInt(this, valueOffset, delta)
// this 当前对象
// valueOffset 是什么鬼?
// delta 须要增长的值
// unsafe 这个又是什么鬼?这个相似乎没见过啊
复制代码

image-20200113170054551

那咱们再点进去看一下unsafe.getAndAddInt(this, valueOffset, delta)究竟作了什么,即便是直接操做内存那也不能保证线程安全啊

image-20200113170922327

看完吓一跳,写个where循环在里面调用compareAndSwapInt(var1, var2, var5, var5 + var4)方法,那能猜测到的应该是这个线程去尝试着抢锁,有可能抢不到,而后挂起当前线程不停去自旋抢锁直到抢到锁而且增长成功为止

先分析这段代码

// var1 当前对象
// var2 当前对象值的位移量
// var4 须要增长的值
// var5 声明他做甚?
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

// 再看看这行代码
// var5 = this.getIntVolatile(var1, var2); 根据当前对象和当前对象值的位移量 获取内存中最新的值
// 再往下看看这行代码
// compareAndSwapInt(var1, var2, var5, var5 + var4) 再想一想cas的全称即比较再交换

// var1 当前对象
// var2 当前对象值的位移量
// var5 当前对象在内存中最新的值
// (var5 + var4) 换成计算后的值
复制代码

原来如此,这像不像mysql中innodb的行锁特性

update stu set status=2 where id = 1 and status=1
// 经过ID肯定好索引
// 经过索引锁定数据再判断status=1 
// 才将id为1的行修改为status=2
复制代码

得出结论,这就是cas实现乐观锁的方式,先比较再进行赋值

CAS算法的好处


CAS是一种无锁算法,经过硬件层面上对前后操做内存的线程进行排队处理CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。

CAS(比较并交换)是CPU指令级的操做,只有一步原子操做,因此很是快。并且避免了请求操做系统来裁定锁的问题,不用麻烦操做系统,直接在CPU内部就搞定了

CAS算法的弊端


刚刚看到一个问题,若是增长不成功,那while会一直尝试着去增长,会不会产生死锁或致使线程耗时过久的一系列问题发生啊?

  • 高并发且自旋不成时

    自旋若是长时间不成功,会带来很大的性能开销。若是变动操做很耗时,同时变动很频繁,就可能致使自旋长时间不成功,带来大量的性能开销

手写一个简单的锁


public class MyLock implements Lock {

    /** * 锁的拥有者 */
    private AtomicReference<Thread> atomicReference = new AtomicReference();

    /** * 线程等待队列 */
    private LinkedBlockingQueue<Thread> linkedBlockingQueue = new LinkedBlockingQueue<Thread>();

    /** * 加锁 */
    public void lock() {
        if (!tryLock()) {
            // 若是抢锁失败,将线程进入等待队列,并挂起当前线程
            linkedBlockingQueue.offer(Thread.currentThread());

            // 挂起当前线程方式有 suspend park wait
            // suspend已被弃用了,wait必须配合synchronized才能使用,因此合适咱们的也只有park了

            // 线程之间的唤醒有多是伪唤醒,因此须要写死循环
            while (true) {
                // 只有队列头部的线程等于当前线程才进行抢锁,不然挂起
                if (linkedBlockingQueue.peek() == Thread.currentThread()) {
                    // 抢不到锁就挂起,抢到锁出队列而且退出lock方法
                    if (!tryLock()) {
                        LockSupport.park();
                    } else {
                        linkedBlockingQueue.poll();
                        return;
                    }
                } else {
                    LockSupport.park();
                }
            }
        }
    }

    /** * 尝试抢锁 */
    public boolean tryLock() {
        // 若是锁的拥有者为null,那就将他设为当前线程
        return atomicReference.compareAndSet(null, Thread.currentThread());
    }

    /** * 释放锁 */
    public void unlock() {
        // 尝试释放锁成功后-唤醒队列头部线程
        if (atomicReference.compareAndSet(Thread.currentThread(), null)) {
            Thread peek = linkedBlockingQueue.peek();
            if (peek != null) {
                LockSupport.unpark(peek);
            }
        }
    }


    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    public Condition newCondition() {
        return null;
    }
}
复制代码

测试:资源类稍微作下修改,将AtomicInteger i 声明为int i

class Resources {
    /** * 自定义锁 */
    private MyLock myLock = new MyLock();

    /** * 非原子操做的int类型 */
    private volatile int i = 0;

    public void add() {
        myLock.lock();
        try {
            i++;
        } finally {
            myLock.unlock();
        }
    }

    public int getI() {
        return i;
    }
}
复制代码

运行结果为:I的值打印为:10000

运行结果是对的,经过MyLock简单实现CAS的例子,应该对CAS算法的理解更加深入

相关文章
相关标签/搜索