原子操作CAS

Posted by Kaka Blog on March 14, 2019

前言

1、引入例子,可以看出使用并发产生的问题。

public class WithoutCasDemo {
    int i = 0;
    void increase() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        WithoutCasDemo demo = new WithoutCasDemo();
        int THREAD_NUM = 20000;
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(()->{
                demo.increase();
            });
            threads[i].start();
        }
        for (int j = 0; j < THREAD_NUM; j++) {
            threads[j].join();
        }
        System.out.println("WithoutCasDemo.i = " + demo.i);
    }
}

使用javap -c命令查看生成的字节码,其中主要看i++部分:

2: getfield      #2                  // Field i:I
5: iconst_1
6: iadd
7: putfield      #2                  // Field i:I

可以看出i++不是一条指令就可以完成。这就导致并发操作时最终的结果小于20000。

2、使用锁可以解决这个问题,如下:

public class WithoutCasDemo {
    int i = 0;
    Lock lock = new ReentrantLock();
    void increase() {
        lock.lock();
        i++;
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        WithoutCasDemo demo = new WithoutCasDemo();
        int THREAD_NUM = 20000;
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(()->{
                demo.increase();
            });
            threads[i].start();
        }
        for (int j = 0; j < THREAD_NUM; j++) {
            threads[j].join();
        }
        System.out.println("WithoutCasDemo.i = " + demo.i);
    }
}

查看字节码文件可以看到加了锁:

1: getfield      #5                  // Field lock:Ljava/util/concurrent/locks/Lock;
4: invokeinterface #6,  1            // InterfaceMethod java/util/concurrent/locks/Lock.lock:()V
9: aload_0
10: dup
11: getfield      #2                  // Field i:I
14: iconst_1
15: iadd
16: putfield      #2                  // Field i:I
19: aload_0
20: getfield      #5                  // Field lock:Ljava/util/concurrent/locks/Lock;

什么是CAS

CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。

手写实现Lock

public class FangLock implements Lock {
    // 保存当前线程
    private AtomicReference<Thread> threadReference = new AtomicReference<>(null);
    // 等待队列
    private LinkedBlockingQueue<Thread> waiter = new LinkedBlockingQueue<>();
    @Override
    public void lock() {
        while (!threadReference.compareAndSet(null, Thread.currentThread())) {
            waiter.add(Thread.currentThread());
            LockSupport.park();
            waiter.remove(Thread.currentThread());
        }
    }

    @Override
    public void unlock() {
        if (threadReference.compareAndSet(Thread.currentThread(), null)) {
            for (Thread thread : waiter) {
                LockSupport.unpark(thread);
            }
        }
    }
}

加锁:

  • 比较原子线程是否为空,为空则可竞争获取线程。
  • 如果原子线程不为空,说明已经有线程在操作,则进入等待队列,并LockSupport.park()禁用当前线程。
  • threadReference.compareAndSet(null, Thread.currentThread()):期望AtomicReference对象的值为null,并设置为当前线程。

解锁:

  • 如果原子线程为当前线程,则可以释放锁,让其它线程竞争锁。

公平锁与非公平锁

锁可以分为公平锁和不公平锁,重入锁和非重入锁,以上过程实际上是非公平锁的获取和释放过程。

公平锁严格按照先来后到的顺去获取锁,而非公平锁允许插队获取锁。

Synchronized与Lock锁有什么不同

  1. Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的

  2. 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的条件下提供一种退出的机制。

  3. 一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以及设置等待时限等方式退出条件队列。

  4. synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式。

参考

Java并发包中Lock的实现原理

并发:重入锁(ReentrantLock)