原子变量类Atomic详解(java.util.Concurrent)

    Java提供了12个原子变量类,可以分为4组:标量类、更新器类、数组类以及复合变量类。

为什么需要Atomic原子变量类?

    对于全局变量的数值类型操作 num++,若没有加synchronized关键字则是线程不安全的,num++解析为num=num+1,明显,这个操作不具备原子性,多线程时必然会出现问题。测试下:

public class AtomicIntegerTest1 {
    public static int count = 0;
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread() {
                public void run() {
                    count++;
                }
            }.start();
        }
        System.out.println("count: " + count);
    }
}

    输出的结果为count: 9992,这个值不定,每次测试都可能不一样,很显然,100个线程跑++操作,结果并没有像预期的那样count: 10000。

要是换成volatile修饰count变量呢?

    volatile修饰的变量能够在线程间保持可见性,能被多个线程同时读但是又能保证只被单线程写,并且不会读取到过期值(由java内存模型中的happen-before原则决定的)volatile修饰字段的写入操作总是优先于读操作,即使多个线程同时修改volatile变量字段,总能保证获取到最新的值。

public class AtomicIntegerTest3 {
    static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        count++;
                    }
                }
            }.start();
        }
        Thread.sleep(1000);
        System.out.println("volatile count: " + count);
    }
}

    结果似乎又失望了,试了大约10次后出现volatile count: 9984,果然还是出现问题了,volatile仅仅保证变量在线程间保持可见性,却依然不能保证非原子性的操作。

用了Atomic类后会变成什么样子呢?

把上面的代码改造成AtomicInteger原子类型:
public class AtomicIntegerTest2 {
    public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        count.getAndIncrement();
                    }
                }
            }.start();
        }
        Thread.sleep(1000);
        System.out.println("AtomicInteger count: " + count);
    }
}

结果每次都输出"AtomicInteger count: 10000",没毛病。concurrent(我这里是jdk1.7)包下提供了12种原子操作类型,如下:

原子变量类分组:

java.util.concurrent.atomic中的类可以分成4组:

  • 标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

  • 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

  • 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

  • 复合变量类:AtomicMarkableReference,AtomicStampedReference

   第一组AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据。其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。如AtomicInteger的实现片断为:

private static final Unsafe unsafe = Unsafe.getUnsafe();  
private volatile int value;

public final int get() {  
        return value;  
}

public final void set(int newValue) {  
        value = newValue;  
}

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

public final int getAndIncrement(){
        for(;;){
        //先取出AtomicInteger的当前值
        int current=get();
        //对当前值加1操作
        int next=current+1;
        //通过compareAndSet方法比较当前值有没有被其它线程修改过,若修改过返回false则再次进入compareAndSet方法判断
        if(compareAndSet(current,next))
        return current;
        }
}

public final boolean weakCompareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}
  • 构造函数(两个构造函数)

    • 默认的构造函数:初始化的数据分别是false,0,0,null
    • 带参构造函数:参数为初始化的数据
  • set()和get()方法:可以原子地设定和获取atomic的数据。类似于volatile,保证数据会在主存中设置或读取

  • void set()和void lazySet():set设置为给定值,直接修改原始值;lazySet延时设置变量值,这个等价于set()方法,但是由于字段是volatile类型的,因此次字段的修改会比普通字段(非volatile字段)有稍微的性能延时(尽管可以忽略),所以如果不是想立即读取设置的新值,允许在“后台”修改值,那么此方法就很有用。

  • compareAndSet()和weakCompareAndSet()方法:

    • 这两个方法都是conditional modifier方法。这2个方法接受2个参数,一个是期望数据(expected),一个是新数据(new);如果atomic里面的数据和期望数据一 致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。JSR规范中说:以原子方式读取和有条件地写入变量但不创建任何 happen-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。大意就是说调用weakCompareAndSet时并不能保证不存在happen- before的发生(也就是可能存在指令重排序导致此操作失败)。但是从Java源码来看,其实此方法并没有实现JSR规范的要求,最后效果和 compareAndSet是等效的,都调用了unsafe.compareAndSwapInt()完成操作。
  • getAndSet()方法

    • 原子的将变量设定为新数据,同时返回先前的旧数据

    • 其本质是get()操作,然后做set()操作。尽管这2个操作都是atomic,但是他们合并在一起的时候,就不是atomic。在Java的源程序的级别上,如果不依赖synchronized的机制来完成这个工作,是不可能的。只有依靠native方法才可以。

      对于 AtomicInteger、AtomicLong还提供了一些特别的方法。

getAndIncrement():以原子方式将当前值加 1,相当于线程安全的i++操作.

incrementAndGet():以原子方式将当前值加 1, 相当于线程安全的++i操作.

getAndDecrement():以原子方式将当前值减 1, 相当于线程安全的i--操作.

decrementAndGet():以原子方式将当前值减 1,相当于线程安全的--i操作.

addAndGet():以原子方式将给定值与当前值相加, 实际上就是等于线程安全的i =i+delta操作.

getAndAdd():以原子方式将给定值与当前值相加, 相当于线程安全的t=i;i+=delta;return t;操作。 以实现一些加法,减法原子操作。(注意 --i、++i不是原子操作,其中包含有3个操作步骤:第一步,读取i;第二步,加1或减1;第三步:写回内存)

    java有8个基本类型,而这里只有3个,那么其他5个如果也想原子操作呢?其实可以参考AtomicBoolean的实现,它先将boolean类型值转换成int。

使用AtomicReference创建线程安全的堆栈

package thread;  
import java.util.concurrent.atomic.AtomicReference;  
public class ConcurrentStack<T> {  
    private AtomicReference<Node<T>>    stacks  = new AtomicReference<Node<T>>();  
    public T push(T e) {  
        Node<T> oldNode, newNode;  
        for (;;) { // 这里的处理非常的特别,也是必须如此的。  
            oldNode = stacks.get();  
            newNode = new Node<T>(e, oldNode);  
            if (stacks.compareAndSet(oldNode, newNode)) {  
                return e;  
            }  
        }  
    }     
    public T pop() {  
        Node<T> oldNode, newNode;  
        for (;;) {  
            oldNode = stacks.get();  
            newNode = oldNode.next;  
            if (stacks.compareAndSet(oldNode, newNode)) {  
                return oldNode.object;  
            }  
        }  
    }     
    private static final class Node<T> {  
        private T       object;       
        private Node<T>   next;         
        private Node(T object, Node<T> next) {  
            this.object = object;  
            this.next = next;  
        }  
    }     
}

    虽然原子的标量类扩展了Number类,但并没有扩展一些基本类型的包装类,如Integer或Long,事实上他们也不能扩展:基本类型的包装类是不可以修改的,而原子变量类是可以修改的。在原子变量类中没有重新定义hashCode或equals方法,每个实例都是不同的,他们也不宜用做基于散列容器中的键值.

 

  第二组AtomicIntegerArray,AtomicLongArray还有AtomicReferenceArray类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方面也引人注目,这对于普通数组来说是不受支持的。他们内部并不是像AtomicInteger一样维持一个valatile变量,而是全部由native方法实现,AtomicIntegerArray的实现片断,如下:

private static final Unsafe unsafe = Unsafe.getUnsafe();  
private static final int base = unsafe.arrayBaseOffset(int[].class);  
private static final int scale = unsafe.arrayIndexScale(int[].class);  
private final int[] array;  
public final int get(int i) {  
        return unsafe.getIntVolatile(array, rawIndex(i));  
}  
public final void set(int i, int newValue) {  
        unsafe.putIntVolatile(array, rawIndex(i), newValue);  
}  

 

    第三组AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater基于反射的实用工具,可以对指定类的指定volatile字段进行原子更新。API非常简单,但是也是有一些约束:

1.字段必须是volatile类型的

2.字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说 调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

3.只能是实例变量,不能是类变量,也就是说不能加static关键字。

4.只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。

5.对于AtomicIntegerFieldUpdater 和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater 。

AtomicIntegerFieldUpdater的实现片断:

private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER = AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");  
private volatile long totalPendingSize;

//使用
long oldValue = totalPendingSize;
long newWriteBufferSize = oldValue + size;
while (!TOTAL_PENDING_SIZE_UPDATER.compareAndSet(this, oldValue, newWriteBufferSize)) {
   oldValue = totalPendingSize;
   newWriteBufferSize = oldValue + size;
}  

转载于:https://my.oschina.net/langwanghuangshifu/blog/1942191

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值