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