Java并发编程系列(四)----CAS与原子类

为了实现线程安全,我们前面都是用锁的方式来保证原子性,那么有不加锁能不能实现线程安全呢?这要从乐观锁和悲观锁说起。
1. 悲观锁。所谓的悲观锁,就是对资源的访问,默认情况下是认为会存在资源抢占,所以每次都要加锁,只能有一个线程执行。

  1. 乐观锁。另外一种锁的策略,默认资源不存在竞争,多个线程可以同时操作,在最后进行更新数据的时候,查看该资源是否被其他线程修改过,没有则执行更新,有则放弃本次操作。

CAS

CAS(Compare And Swap)比较替换,意思是
用一个期望值与当前值比较,如果相等,则用一个新的值替换当前变量值;如果不相等则放弃操作。

CAS 的ABA问题

假设线程1读取的时候变量值为A,此时线程2改变了变量值为B,然后又改回A。当线程1对比的时候发现变量值还是为A,则认为变量没有被其他线程修改过(事实上已经修改了A->B->A)。解决办法为在变量前面加上版本号,那么A->B->A就会变为1A->2B->3A。

CAS操作必须是原子操作,也就是比较-交换这整个过程是一个原子操作,不可分割,这需要处理器提供对应的指令集来实现。JDK中提供了一个Unsafe类,该类中的compareAndSwapXXX方法负责调用本地方法来实现CAS的原子操作。

原子类

在前面Java并发编程系列(一)—-深入剖析volatile关键字中分析了,下面的increaseAndGet()是没有无法实现原子操作的。

package com.rancho945.concurrent;

public class Counter {
    public int count = 0;

    public int increaseAndGet() {
        return count++;
    }
}

如果要实现原子操作,那么我么就必须对其进行加锁。JDK提供了一些原子类,可以实现上述功能的原子操作,并且没有加锁,先以AtomicInteger为例子,与上面的Counter类进行对比

package com.rancho945.concurrent;

import java.util.concurrent.atomic.AtomicInteger;


public class Test {
    public static void main(String[] args) {
        final AtomicInteger integer = new AtomicInteger();
        final Counter counter = new Counter();
        for (int i = 0; i < 20; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    for (int j = 0; j < 100000; j++) {
                        integer.incrementAndGet();
                        counter.increaseAndGet();
                    }
                }
            }).start();
        }
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println("AtomicInteger---"+integer.get());
        System.out.println("Counter---"+counter.count);
    }
}

执行结果

AtomicInteger---2000000
Counter---1389131

可以看到原子类的实现了线程安全,而我们自己没有加锁的却没有实现线程安全。

AtomicInteger源码分析

那么AtomicInteger是怎么实现线程安全的呢?我们看看AtomicInteger的源码

//这个是JDK提供的CAS操作工具类
private static final Unsafe unsafe = Unsafe.getUnsafe();
//这个用于标记变量的偏移量
private static final long valueOffset;
//这个是int值
private volatile int value;

在类加载的时候执行的代码块:

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

这里的意思是获取value成员变量在对象内存地址的偏移量,使用valueOffset标记value的位置。在CAS中会使用到。

然后看看incrementAndGet方法:

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

看看get方法:

public final int get() {
        return value;
    }

没什么好说的,再看看compareAndSet()

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

这里我们看到,使用的是unsafe.compareAndSwapInt,改方法负责调用jni本地方法实现CAS原子操作,这里的第一个参数传入的是当前对象,第二个参数就是前面静态代码块获取到的偏移量,第三个是期望值,如果期望值和valueOffset偏移量地址里内容一致,这把valueOffset地址(注意这里是valueOffset偏移量地址里的值而不是valueOffset本身的值,因为valueOffset是final的)里的内容更新成update的值,返回true;否则不更新,返回false。

过程就是先获取value的值,再加1,然后进行CAS操作,如果在读取value值和加1的过程中value的值被其他线程改变了,那么CAS失败,一直循环到成功为止。至于其他getAndAdd之类的方法也都差不多,读着可以自行分析。

synchronized 与CAS的对比

synchronized加锁解锁是一个相对比较耗时间的过程,在单线程或者比较低或者说一般的并发环境下,CAS性能要优于synchronized。但是在非常高的并发环境下,如果对同一个资源竞争很激烈,CAS失败的情况就会很多。比如原子类的原子操作方法的for(;;)循环重试次数增多,消耗的CPU时间片也会相应的增加。

值得注意的是高并发环境不意味着对同一个资源竞争激烈,比如有100个线程,竞争同一个资源的线程只有几个,所以不意味着线程多使用synchronized就一定有优势,在通常情况下CAS都要优于synchronized。

另外,synchronized加锁会使等待锁的其他线程挂起,如果持有锁的线程阻塞,那么其他线程只能干巴巴地等待,CAS可以避免这个问题。

参考资料

  1. 程序中的乐观锁与悲观锁,以及动手实现乐观锁 http://www.cnblogs.com/qinggege/p/5284750.html
  2. Java并发编程之CAS http://ifeve.com/compare-and-swap/
  3. 聊聊并发(五)原子操作的实现原理 http://ifeve.com/atomic-operation/
  4. unsafe.objectFieldOffset是什么? http://hllvm.group.iteye.com/group/topic/37940
  5. 《Java并发编程实战》 Brian Goetz等著 童云兰等译

你可能感兴趣的:(Java多线程)