下面我们重点讲一下Atomic变量,先举个例子:

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by hinus on 2017/5/31.
 */
public class TestAtomic {
    public AtomicInteger total;

    public static void main(String[] args) throws Exception {
        TestAtomic test = new TestAtomic();
        test.total = new AtomicInteger(0);

        Thread t1 = new Thread() {
            public void run() {
                for (int i = 0; i < 5_000; i++) {
                    test.total.incrementAndGet();
                }
            }
        };

        Thread t2 = new Thread() {
            public void run() {
                for (int i = 0; i < 5_000; i++) {
                    test.total.incrementAndGet();
                }
            }
        };

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(test.total.get());
    }
}

这个例子是两个线程各自执行对变量total的加一操作,每个线程5000次,共计一万次。这个例子的运行结果,一定是10000。

Atomic的原理

上面的例子,很典型,没有使用任何的同步机制。什么锁啦,信号量啦,synchronized关键字啦,统统没有。只是引入了一种新的类型AtomicInteger,这个类型中的所有操作都是原子操作。我们就来看一下,它是怎么做到的。首先,看一下 incrementAndGet 的实现,我们直接在IDE里查看就行(或者通过JDK源代码去查看)。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

好了,我们继续看,unsafe.getAndAddInt 的实现是什么样的。

 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;
    }

这是一个循环,先要搞清楚 var2 是要更新的变量在类中的offset。怎么理解呢?比如说,有一个类,A:

class A {
    int a;
    int b;
}

它在内存的布局有很大可能是这样的(与JVM的具体实现有关,具体数字在不同的实现可能有变化,但原理是相同的):

在这张图里,a 的偏移就是8,b的偏移就是16(都是64位系统,int 类型有时也会占用8字节,虽然实际上,只有4字节是有效的)。这就是偏移(offset)的意义。我们今天先不管它的具体实现,只要知道通过getIntVolatile是能取到相对应的 field 的值的。

我们今天重点来看 compareAndSwapInt 的实现。这个方法的实现是这样的:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

这个函数其实很简单,就是去看一下obj 的 offset 上的那个位置上的值是多少。如果是 e,那就把它更新为 x,返回true。如果不是 e,那就什么也不做,并且返回false。

好了。我们再回过头来看,getAndAddInt 的实现。这就比较清楚了,var5是我们要更新的那个值。如果我们使用CAS操作去更新这个值,发现这个值已经变化了(一定是其他线程干的,想想为什么?),那就再进入下一次循环。如果这个值没有变化,那就使用原子操作,把这个值更新上去。这样,我们就实现了无锁的变量加法。

自旋锁

在上面的实现中,虽然没有显式地加锁解锁的操作。但实际上,这种操作也是一种锁:Spin Lock,也就是自旋锁。

我来举个现实中的例子。比如说,读大学的时候,我们要去借教室,假如我们要借的教室是2221(想起我的班长,通知大家周日到2221开班会,他是个福建人,读作饿饿饿幺)。那我去借的时候,发现教室里有人上课。然后我就每隔1分钟去看一趟,直到教室里没有人上课我才过去占住教室。这种策略就是一种自旋的策略,是一种像陀螺一样一直在转的策略。这种策略在借教室的例子显得有点笨,但再换一个例子,在12306上抢火车票,我们使用工具一直刷,其目的就是能在一旦有余票可以第一时间抢到。这也是一种自旋锁。

这里为什么使用自旋锁呢?这就涉及到线程切换的知识了。线程切换的知识我们后边会讲,这里只要知道线程切换的开销相比起执行一次加法操作是非常巨大的(通常在三个数量级以上)。为了避免线程切换,我们可以让一个线程始终活跃着尝试去抢这个变量的控制权。因为其他线程更新这个值也是一个非常快的操作,就可以在很短的时间之内就可以抢得这个变量了。所以这里就选择了使用CAS来实现。

可见,自旋锁适合的场景是关键区中逻辑很短,等待时间很短的情况。