Atomic

1. 同步问题的提出

  假设我们使用一个双核处理器执行A和B两个线程,核1执行A线程,而核2执行B线
程,这两个线程现在都要对名为obj的对象的成员变量i进行加1操作,假设i的初
始值为0,理论上两个线程运行后i的值应该变成2,但实际上很有可能结果为1。
    
  我们现在来分析原因,这里为了分析的简单,我们不考虑缓存的情况,实际上有缓存
会使结果为1的可能性增大。A线程将内存中的变量i读取到核1算数运算单元中,然
后进行加1操作,再将这个计算结果写回到内存中,因为上述操作不是原子操作,只
要B线程在A线程将i增加1的值写回到内存之前,读取了内存中i的值(此时i值为
0),那么一定就会出现i的结果为1。因为A和B线程读取的i的值都为0,两个线程
对它加1后的值都为1,两个线程先后将1写入到变量i中,也就是说i被两次写入的值
都为1。
   
  最通常的解决方法是两个线程中对i加1的代码用synchronize关键字对obj对象加
锁。今天我们介绍一种新的解决方案,即使用Atomic包中的相关类来解决。

2. Atomic在硬件上的支持

   在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是"原
子操作",因为中断只能发生于指令之间(因为线程的调度需要通过中断完成)。这也是
某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互
斥的原因。在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于
系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干
扰。

   在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线
#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后
的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令
结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存
了,保证了这条指令在多处理器环境中的原子性。 当然,并不是所有的指令前面都可以
加lock前缀的,只有ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC,
 NEG, NOT, OR, SBB, SUB, XOR, XADD, 和 XCHG指令前面可以加"LOCK"指
 令,实现原子操作。

   Atomic的核心操作就是CAS(compare and set,利用CMPXCHG指令实现,它是一个
原子指令),该指令有三个操作数,变量的内存值V(value的缩写),变量的当前预期
值E(exception的缩写),变量想要更新的值U(update的缩写),当内存值和当前
预期值相同时,将变量的更新值覆盖内存值,执行伪代码如下。

if(V == E){ 
   V = U 
   return true 
}else{ 
   return false 
}

  现在我们就用CAS操作来解决上述问题。B线程将内存中的变量i读取一个临时变量中
(假设此时读取的值为0),然后再将i的值读取到core1的算数运算单元中,接下来进
行加1操作,比较临时变量中的值和i当前的值是否相同,如果相同用运算单元中的结果
(即i+1)的值覆盖内存中i的值(注意这一部分就是CAS操作,它是个原子操作,不能
被中断且其它线程中的CAS操作不能同时执行),否则指令执行失败。如果指令失败,说
明A线程已经将i的值加1。由此可知如果两个线程一开始读取的i的值为都为0,那么必
然只有一个线程的CAS操作能够成功,因为CAS操作不能并发执行。对于CAS操作执行失
败的线程,只要循环执行CAS操作,那么一定能够成功。可以看到并没有线程阻塞,这和
synchronize的原理有着本质的不同。

3. Atomic包简介及源码分析

  Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本
类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行
更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等
到执行成功。

  Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道
一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码
的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是
告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的
后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似
C++一样的指针越界到其他进程的问题。

Atomic包中的类按照操作的数据类型可以分成4组

   AtomicBoolean,AtomicInteger,AtomicLong
   线程安全的基本类型的原子性操作

   AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
   线程安全的数组类型的原子性操作,它操作的不是整个数组,而是数组中的
   单个元素

   AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,
   AtomicReferenceFieldUpdater
   基于反射原理对象中的基本类型(长整型、整型和引用类型)进行线程
   安全的操作

   AtomicReference ,AtomicMarkableReference,
   AtomicStampedReference
   线程安全的引用类型及防止ABA问题的引用类型的原子操作

我们一般常用的AtomicInteger、AtomicReference和
AtomicStampedReference。

现在我们来分析一下Atomic包中AtomicInteger的源代码,
其它类的源代码在原理上都比较类似。

1. 有参构造函数

public AtomicInteger(int initialValue) { 
   value = initialValue;
}


从构造函数函数可以看出,数值存放在成员变量value中

private volatile int value;

成员变量value声明为volatile类型,说明了多线程下的可见性,
即任何一个线程的修改,在其它线程中都会被立刻看到

2. compareAndSet方法(value的值通过内部this和valueOffset传递)

public final boolean compareAndSet(int expect, int update) {
 return unsafe.compareAndSwapInt(this, valueOffset, expect,
  update);
}
这个方法就是最核心的CAS操作    

 3. getAndSet方法 , 在该方法中调用了compareAndSet方法

public final int getAndSet(int newValue) {
        for (;;) {
            int current = get();
            if (compareAndSet(current, newValue))
                return current;
        }
}

如果在执行if (compareAndSet(current, newValue) 之前其它线程更改了
value的值,那么导致   value 的值必定和current的值不同,compareAndSet执
行失败,只能重新获取value的值,然后继续比较,直到成功。  

4. i++的实现

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

5. ++i的实现

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

4. 使用AtomicInteger例子

下面的程序,利用AtomicInteger模拟卖票程序,运行结果中
不会出现两个程序卖了同一张票,也不会卖到票为负数

package javaleanning;
import java.util.concurrent.atomic.AtomicInteger;

public class SellTickets {
    AtomicInteger tickets = new AtomicInteger(100);
    
    class Seller implements Runnable{
        @Override
        public void run() {
            while(tickets.get() > 0){
                int tmp = tickets.get();
                if(tickets.compareAndSet(tmp, tmp-1)){
                    System.out.println(Thread.currentThread().getName()+"  "+tmp);
                }
            }
        }
        
    }
    
    public static void main(String[] args) {
        SellTickets st = new SellTickets();
        new Thread(st.new Seller(), "SellerA").start();
        new Thread(st.new Seller(), "SellerB").start();
    }
}

5. ABA问题

上述的例子运行结果完全正确,这是基于两个(或多个)线程都是向同一个方向对数据
进行操作,上面的例子中两个线程都是是对tickets进行递减操作。再比如,多个线程
对一个共享队列都进行对象的入列操作,那么通过AtomicReference类也可以得到正
确的结果(AQS中维护的队列其实就是这个情况),但是多个线程即可以入列也可以出
列,也就是数据的操作方向不一致,那么可能出现ABA的情况。

6. ABA问题的解决方法

使用AtomicMarkableReference,AtomicStampedReference。使用上述两个
Atomic类进行操作。他们在实现compareAndSet指令的时候除了要比较当对象的前值
和预期值以外,还要比较当前(操作的)戳值和预期(操作的)戳值,当全部相同时,
compareAndSet方法才能成功。每次更新成功,戳值都会发生变化,戳值的设置是由编
程人员自己控制的。

public boolean compareAndSet(V  expectedReference, V  newReference, int expectedStamp,int newStamp) {

    Pair<V> current = pair;
    return  expectedReference == current.reference && 
            expectedStamp == current.stamp &&
            ((newReference == current.reference && newStamp == 
            current.stamp) || 
            casPair(current, Pair.of(newReference, newStamp)));
}

这时的compareAndSet 方法需要四个参数expectedReference, newReference,
 expectedStamp, newStamp,我们在使用这个方法时要保证期望的戳值和要更新戳
 值不能一样,通常 newStamp = expectedStamp + 1

还拿上述的例子

假设线程T1在弹栈之前:     sp指向A,戳值为100。

线程T2执行: 将A出栈后,sp指向B,戳值变为101,

B出栈后,sp指向C,戳值变为102,

 A入栈后,sp指向A,戳值变为103,

线程T1继续执行compareAndSet语句,发现sp虽然还是指向A,但是戳值的预期值100
和当前值103不同,所以compareAndSet失败,需要从新获取newSP的值(此时newSP
就会指向C),以及戳的预期值103,然后再次进行compareAndSet操作,这样A成功
出栈,sp会指向C。

注意,由于compareAndSet只能一次改变一个值,无法同时改变newReference和
newStamp,所以在实现的时候,在内部定义了一个类Pair类将newReference和
newStamp变成一个对象,进行CAS操作的时候,实际上是对Pair对象的操作

private static class Pair<T> {
    final T reference;
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

对于AtomicMarkableReference而言,戳值是一个布尔类型的变量,而
AtomicStampedReference中戳值是一个整型变量。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值