原子操作
原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。在多处理器上实现原子操作就变得有点复杂。
处理器如何实现原子操作
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性
总线锁(性能太低)
cpu从主内存读取数据到告诉缓存,会在总线上对这个数据加锁,这样其他cpu没法去读写这个数据,直到被释放,所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
使用缓存锁保证原子性
最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化,频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性
MESI缓存锁一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效
但是有两种情况下处理器不会使用缓存锁定。
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定
以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
Java当中如何实现原子操作
在java中可以通过锁和循环CAS的方式来实现原子操作。JVM中的CAS操作正是利用了上文中提到的处理器提供的CMPXCHG指令实现的。CAS实现的基本思路就是循环进行CAS操作直到成功为止。
Atomic
在Atomic包里一共有12个类,四种原子更新方式,原子更新基本类型,原子更新数组,原子更新引用,原子更新字段, Atomic包里的类基本都是使用Unsafe实现的包装类。
- 基本类:AtomicInteger、AtomicLong、AtomicBoolean;
- 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference;
- 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
CAS(compare and swap)1.5+
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值
CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可
A-B-A问题
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicAbaProblemTest {
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
Thread main = new Thread(new Runnable() {
@Override
public void run() {
int a = atomicInteger.get();
System.out.println("操作线程"+Thread.currentThread().getName()+"--修改前操作数值:"+a);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
if(isCasSuccess){
System.out.println("操作线程"+Thread.currentThread().getName()+"--Cas修改后操作数值:"+atomicInteger.get());
}else{
System.out.println("CAS修改失败");
}
}
},"主线程");
Thread other = new Thread(new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();// 1+1 = 2;
System.out.println("操作线程"+Thread.currentThread().getName()+"--increase后值:"+atomicInteger.get());
atomicInteger.decrementAndGet();// atomic-1 = 2-1;
System.out.println("操作线程"+Thread.currentThread().getName()+"--decrease后值:"+atomicInteger.get());
}
},"干扰线程");
main.start();
other.start();
}
}
解决A-B-A问题(AtomicStampedReference很鸡肋)
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedRerenceTest {
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<>(1, 0);
public static void main(String[] args){
Thread main = new Thread(() -> {
int stamp = atomicStampedRef.getStamp(); //获取当前标识别
System.out.println("操作线程" + Thread.currentThread()+ "stamp="+stamp + ",初始值 a = " + atomicStampedRef.getReference());
try {
Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
System.out.println("操作线程" + Thread.currentThread() + "stamp="+stamp + ",CAS操作结果: " + isCASSuccess);
},"主操作线程");
Thread other = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
stamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
System.out.println("操作线程" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
},"干扰线程");
main.start();
other.start();
}
}
Unsafe
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常
public class Unsafe {
// 单例对象
private static final Unsafe theUnsafe;
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// 仅在引导类加载器`BootstrapClassLoader`加载时才合法
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
如何获取Unsafe
1、把调用Unsafe相关方法的类Demo所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载
java -Xbootclasspath/Demo:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
2、通过反射获取单例对象theUnsafe
public class UnsafeInstance {
public static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几
package util;
import bean.UnsafeTestBean;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* Unsafe类是"final"的,不允许继承。且构造函数是private的,
* 因此无法在外部对Unsafe进行实例化,只能通过反射来获取Unsafe:
*/
public class UnsafeInstance {
public static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
UnsafeTestBean unsafeTestBean = new UnsafeTestBean();
/**
* 内存屏障
* public native void loadFence(); 保证在这个屏障之前的所有读操作都已经完成。
* public native void storeFence(); 保证在这个屏障之前的所有写操作都已经完成。
* public native void fullFence(); 保证在这个屏障之前的所有读写操作都已经完成。
*/
/**
* 普通读写
* 通过Unsafe可以读写一个类的属性,即使这个属性是私有的,也可以对这个属性进行读写。
* public native int getInt(Object var1, long var2);
* public native void putInt(Object var1, long var2, int var4);
*/
UnsafeInstance.reflectGetUnsafe().putInt(unsafeTestBean,20L,10);
int temp = UnsafeInstance.reflectGetUnsafe().getInt(unsafeTestBean,20);
System.out.println(temp);;
/**
* Unsafe还可以直接在一个地址上读写
* public native byte getByte(long var1);
* public native void putByte(long var1, byte var3);
*/
/**
* volatile读写
* 普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。
* public native int getIntVolatile(Object var1, long var2);
* public native void putIntVolatile(Object var1, long var2, int var4);
*/
/**
* 有序写入
* 有序写入只保证写入的有序性,不保证可见性,就是说一个线程的写入不保证其他线程立马可见。
* public native void putOrderedObject(Object var1, long var2, Object var4);
* public native void putOrderedInt(Object var1, long var2, int var4);
* public native void putOrderedLong(Object var1, long var2, long var4);
*/
/**
* 直接内存操作
* // 分配内存
* public native long allocateMemory(long var1);
* // 重新分配内存
* public native long reallocateMemory(long var1, long var3);
* // 内存初始化
* public native void setMemory(long var1, long var3, byte var5);
* // 内存复制
* public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);
* // 清除内存
* public native void freeMemory(long var1);
*/
/**
* CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
* public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
* public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
* public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
*/
/**
* 偏移量相关
* public native long staticFieldOffset(Field var1);
* public native long objectFieldOffset(Field var1);
* public native Object staticFieldBase(Field var1);
* public native int arrayBaseOffset(Class<?> var1);
* public native int arrayIndexScale(Class<?> var1);
*/
/**
* 线程调度
* 取消阻塞线程
* public native void unpark(Object var1);
* 阻塞线程
* public native void park(boolean var1, long var2);
* 获得对象锁(可重入锁)
* public native void monitorEnter(Object var1);
* 释放对象锁
* public native void monitorExit(Object var1);
* 尝试获取对象锁
* public native boolean tryMonitorEnter(Object var1);
*/
/**
* 类加载
* public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
* public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);
* public native Object allocateInstance(Class<?> var1) throws InstantiationException;
* public native boolean shouldBeInitialized(Class<?> var1);
* public native void ensureClassInitialized(Class<?> var1);
*/
}
}
线程调度
Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现
public class LockSupport {
private static final Unsafe UNSAFE;
private static final long parkBlockerOffset;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
private LockSupport() {
}
private static void setBlocker(Thread var0, Object var1) {
UNSAFE.putObject(var0, parkBlockerOffset, var1);
}
public static void unpark(Thread var0) {
if (var0 != null) {
UNSAFE.unpark(var0);
}
}
public static void park(Object var0) {
Thread var1 = Thread.currentThread();
setBlocker(var1, var0);
UNSAFE.park(false, 0L);
setBlocker(var1, (Object)null);
}
内存屏障
在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。
public class StampedLock implements Serializable {
private static final Unsafe U;
.........
/**
* StampedLock的validate()方法使用内存屏障,防止指令重排
*/
public boolean validate(long var1) {
U.loadFence();
return (var1 & -128L) == (this.state & -128L);
}
.......
}
StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性。
堆外内存
通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。
使用堆外内存的原因
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
- 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存
典型应用
DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。
DirectByteBuffer(int var1) {
super(-1, 0, var1, var1);
boolean var2 = VM.isDirectMemoryPageAligned();
int var3 = Bits.pageSize();
long var4 = Math.max(1L, (long)var1 + (long)(var2 ? var3 : 0));
Bits.reserveMemory(var4, var1);
long var6 = 0L;
try {
//分配内存 并返回基地址
var6 = unsafe.allocateMemory(var4);
} catch (OutOfMemoryError var9) {
Bits.unreserveMemory(var4, var1);
throw var9;
}
//内存初始化
unsafe.setMemory(var6, var4, (byte)0);
if (var2 && var6 % (long)var3 != 0L) {
this.address = var6 + (long)var3 - (var6 & (long)(var3 - 1));
} else {
this.address = var6;
}
//跟踪DirectByteBuffer对象的垃圾回收,以实现堆外内存的释放
this.cleaner = Cleaner.create(this, new DirectByteBuffer.Deallocator(var6, var4, var1));
this.att = null;
}
创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。