Java并发编程原子类实现-AtomicInteger incrementAndGet方法实现

背景,多个生产者一个消费者,实现生产者生产数据按产生顺序加上编号
简单应用如下,子线程进行原子增

public class Producer implements Runnable {
    private static AtomicInteger  count= new AtomicInteger();
      public void run() {
         String data = null;
        count.incrementAndGet()
        data = "data:" + count.incrementAndGet();
        System.out.println("将数据:" + data + "放入队列...");
    }
    }

执行:

        // 声明一个容量为10的缓存队列
        BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);

        Producer producer1 = new Producer(queue);
        Producer producer2 = new Producer(queue);
        Producer producer3 = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        // 借助Executors
        ExecutorService service = Executors.newCachedThreadPool();
        // 启动线程
        service.execute(producer1);
        service.execute(producer2);
        service.execute(producer3);
        service.execute(consumer);

运行结果如下:

这里写图片描述

在多线程的场景即可实现原子加

打开incrementAndGet调用链,调用方法如下:



    public class AtomicInteger extends Number implements java.io.Serializable {
       /**
     * Atomically increments by one the current value.
     *自增
     * @return the updated value
     * 更新后的值
     */
     public final int incrementAndGet() {
     //第一个为AtomicInteger ,第二个为value所在属性偏移量,第三个为要增加的数值
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
       private static final Unsafe unsafe = Unsafe.getUnsafe();

         static {
        try {
       /***
   * 返回指定静态field的内存地址偏移量,在这个类的其他方法中这个值只是被用作一个访问
   * 特定field的一个方式。这个值对于 给定的field是唯一的,并且后续对该方法的调用都应该
   * 返回相同的值。
   * @param field the field whose offset should be returned.
   *         需要返回偏移量的field
   * @return the offset of the given field.
   *         指定field的偏移量,该过程实际上就是计算成员变量value的内存偏移地址,计算后,可以更直接的对内存进行操作。
   */
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
//value:保存着AtomicInteger基础数据,使用volatile修饰,可以保证该值对内存可见,也是原子类实现的理论保障。
  private volatile int value;
    }

Unsave:
Unsafe类是啥?

Java最初被设计为一种安全的受控环境。尽管如此,Java HotSpot还是包含了一个“后门”,提供了一些可以直接操控内存和线程的低层次操作。这个后门类——sun.misc.Unsafe——被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是丝毫不建议在生产环境中使用这个后门。因为这个API十分不安全、不轻便、而且不稳定。这个不安全的类提供了一个观察HotSpot JVM内部结构并且可以对其进行修改。有时它可以被用来在不适用C++调试的情况下学习虚拟机内部结构,有时也可以被拿来做性能监控和开发工具。

为什么叫Unsafe?

Java官方不推荐使用Unsafe类,因为官方认为,这个类别人很难正确使用,非正确使用会给JVM带来致命错误。而且未来Java可能封闭丢弃这个类。

Unsave类中操作:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        //如果compareAndSwapInt时取出的值和预期值一样,则可以进行相关值的修改,否则,值已经被修改,继续从对象中通过偏移量取出最新值进行操作
        do {
        //利用对象和value属性的偏移量取到在value内存中取到最新的值,作为预期值的结果
            var5 = this.getIntVolatile(var1, var2);//1
            // 1 2 4 5 对应值分别为,引用对象,value属性偏移量,增加值,预期值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//2

        return var5;
    }

两个相关方法调用如下:

 /**
    Retrieves the value of the integer field at the specified offset in the
    supplied object with volatile load semantics.
    获取obj对象中offset偏移地址对应的整型field的值,支持volatile load语义。

    @param obj the object containing the field to read.
       包含需要去读取的field的对象
    @param offset the offset of the integer field within <code>obj</code>.
        <code>obj</code>中整型field的偏移量
   */
   public native int getIntVolatile(Object obj, long offset);
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
* 
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

native方法实现:

//getIntVolatile方法native实现
jint sun::misc::Unsafe::getIntVolatile (jobject obj, jlong offset)    
{    
  volatile jint *addr = (jint *) ((char *) obj + offset);    //3
  jint result = *addr;    //4
  read_barrier ();    //5
  return result;    //6
}  
inline static void read_barrier(){
  __asm__ __volatile__("" : : : "memory");
}

1.通过volatile方法获取当前内存中该对象的value值。
2. 计算value的内存地址。
3. 将值赋值给中间变量result。
4.插入读屏障,保证该屏障之前的读操作后后续的操作可见。
5. 返回当前内存值
6. 通过compareAndSwapInt操作对value进行+1操作,如果再执行该操作过程中,内存数据发生变更,则执行失败,但循环操作直至成功。

//compareAndSwapInt
jboolean sun::misc::Unsafe::compareAndSwapInt (jobject obj, jlong offset,jint expect, jint update)  {  
  jint *addr = (jint *)((char *)obj + offset); //1
  return compareAndSwap (addr, expect, update);
}  

static inline bool compareAndSwap (volatile jlong *addr, jlong old, jlong new_val)    {    
  jboolean result = false;    
  spinlock lock;    //2
  if ((result = (*addr == old)))    //3
    *addr = new_val;    //4
  return result;  //5
}  

1 通过对象地址和value的偏移量地址,来计算value的内存地址。
2 使用自旋锁来处理并发问题。
3 比较内存中的值与调用方法时调用方所期待的值。
4 如果3中的比较符合预期,则重置内存中的值。
5 如果成功置换则返回true,否则返回false;

为什么volatile能保证可见性? (内存屏障)

我们都知道volatile能保证可见性,不能保证原子性,比如i++操作
也知道Happen-Before原则,那么是如何确保Happen-Before原则不被指令重排序影响呢?
例如你让一个volatile的integer自增(i++),其实要分成3步:
1)读取volatile变量值到local;
2)增加变量的值;
3)把local的值写回,让其它的线程可见。
这3步的jvm指令为:

mov   
0xc(%r10),%r8d
 ; Load
inc   
 %r8d           ; Increment
mov   
 %r8d,0xc(%r10)
 ; Store
lock
 addl $0x0,(%rsp)
 ; StoreLoad Barrier

StoreLoad Barrier就是内存屏障

内存屏障(memory barrier) 是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障, 相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会 把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

内存屏障和volatile什么关系?

上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障 指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将 会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
明白了内存屏障这个CPU指令,回到前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。
所以volatile不能保证i++操作的原子性

**

内存屏障(Memory barrier)

**

为什么会有内存屏障

每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。
用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障,内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令。
内存屏障是什么

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:
阻止屏障两侧的指令重排序;
强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

volatile语义中的内存屏障

volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
final语义中的内存屏障

对于final域,编译器和CPU会遵循两个排序规则:
新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;
初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(先赋值引用,再调用final值)
总之上面规则的意思可以这样理解,必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:
写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止重排序。
读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障。
X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。

链接:https://segmentfault.com/a/1190000012463882
链接:https://www.cnblogs.com/churao/p/8494160.html
链接:https://www.jianshu.com/p/2ab5e3d7e510

猜你喜欢

转载自blog.csdn.net/qq_31443653/article/details/81480684