并发编程07-Atomic&Unsafe魔法类详解及使用(处理器如何实现原子操作;ABA问题;CAS操作;线程挂起阻塞、唤醒、锁机制;内存屏障)

详见笔记及Git示例
 
1、什么是原子操作?
2、CPU原子操作的实现方式 
3、Atomic相关(ABA问题) 
4、Unsafe魔法类(CAS操作;线程挂起阻塞、唤醒、锁机制;内存屏障
 
一、原子操作
原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。本文让我们一起来聊一聊在Inter处理器和Java里是如何实现原子操作的。
术语名称
英文
解释
缓存行
Cache line
缓存的最小操作单位
比较并交换
Compare and Swap
CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CPU流水线
CPU pipeline
CPU流水线的工作方式就象工业生产上的装配流水线,在CPU中由5~6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5~6步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度。
内存顺序冲突
Memory order violation
内存顺序冲突一般是由假共享引起,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线。
二、处理器如何实现原子操作
         32位IA-32处理器使用基于对缓存加锁总线加锁的方式来实现多处理器之间的原子操作。
 
2.1、 处理器自动保证基本内存操作的原子性
         首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者
写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字
节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64
位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,
跨多个缓存行,跨页表的访问。但是处理器提供总线锁定缓存锁定两个机制来保证复杂内
存操作的原子性。
2.2、 使用总线锁保证原子性
         第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写
(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样
读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果
i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图
 
         原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分
别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读
改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
         处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个
LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该
处理器可以独占使用共享内存。
 
2.3、 使用缓存锁保证原子性
         第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操
作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器
不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使
缓存锁定代替总线锁定来进行优化。
         频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接
在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓
存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内
存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言
LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子
性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处
理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i
时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
         但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存
在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第
二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区
域在处理器的缓存行中也会调用总线锁定。
         以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测
试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指
令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理
器不能同时访问它。
 
2.4、Java当中如何实现原子操作
         在java中可以通过循环CAS的方式来实现原子操作。
         JVM中的CAS操作正是利用了上文中提到的处理器提供的CMPXCHG指令实现的。
旋CAS实现的基本思路就是循环进行CAS操作直到成功为止,具体的类可以参见juc下的
atomic包内的原子类。
 
三、Atomic
         在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更
新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装
类。
  • 基本类:AtomicInteger、AtomicLong、AtomicBoolean。
  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。
  • 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence(解决ABA问题)、AtomicMarkableReference。
  • 属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
 
3.1、原子更新基本类型类
用于通过原子的方式更新基本类型,Atomic包提供了以下三个类:
  • AtomicBoolean:原子更新布尔类型。
  • AtomicInteger:原子更新整型。
  • AtomicLong:原子更新长整型。
 
AtomicInteger的常用方法如下:
  1. int addAndGet(int delta) :以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
  2. boolean compareAndSet(int expect, int update) :如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
  3. int getAndIncrement():以原子方式将当前值加1,注意:这里返回的是自增前的值。
  4. void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  5. int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
package my.mark.mybaibaoxiang.juc.biji07.Atomic;
 
 
import java.util.concurrent.atomic.AtomicInteger;
 
 
/**
* @author wxming.
* @description: AtomicInteger基本使用。
*/
public class AtomicIntegerTest {
    static AtomicInteger atomicInteger = new AtomicInteger();
 
 
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    atomicInteger.incrementAndGet();
                }
            }).start();
        }
 
 
        try {
            Thread.sleep(1000);//此处不用睡眠,用上个笔记的知识也可以等线程都执行完再执行后续。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("自加10次数值:--->" + atomicInteger.get());//如果线程没睡眠没执行完就取,此处可能还不到10.
    }
}
//自加10次数值:--->10
 
         Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,float和
double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是
使用Unsafe实现的,Unsafe只提供了三种CAS方法,compareAndSwapObject,
compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现其是
先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新double
也可以用类似的思路来实现。
 
3.2、原子更新数组类
         通过原子的方式更新数组里的某个元素,Atomic包提供了以下三个类:
  1. AtomicIntegerArray:原子更新整型数组里的元素。
  2. AtomicLongArray:原子更新长整型数组里的元素。
  3. AtomicReferenceArray:原子更新引用类型数组里的元素。
AtomicIntegerArray类主要是提供原子的方式更新数组里的整型,其常用方法如下
  • int addAndGet(int i, int delta):以原子方式将输入值与数组中索引i的元素相加。
  • boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
package my.mark.mybaibaoxiang.juc.biji07.Atomic;
 
 
import java.util.concurrent.atomic.AtomicIntegerArray;
 
 
/**
* @author wxming.
* @description: AtomicIntegerArray基本使用。
*/
public class AtomicIntegerArrayTest {
    static int[] value = new int[]{1, 2};
    static AtomicIntegerArray aiArray = new AtomicIntegerArray(value);//里面是clone操作了一份副本,没对原数组做修改。
 
 
 
 
    public static void main(String[] args) {
        aiArray.getAndSet(0, 3);
        System.out.println(aiArray.get(0));
        System.out.println(value[0]);
        if (aiArray.get(0) != value[0]) {
            System.out.println("是否相等");
        }
    }
}
打印结果:
3
1
是否相等
 
3.3、原子更新引用类型
         原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子的更新多个变
量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下三个类:
  1. AtomicReference:原子更新引用类型。
  2. AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
  3. AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)
ABA问题
package my.mark.mybaibaoxiang.juc.biji07.Atomic;
 
 
import java.util.concurrent.atomic.AtomicInteger;
 
 
/**
* @author wxming.
* @description: TODO ABA问题
* 比方主线程由于睡眠了一下,不知道干扰线程将某个数加一又减一了,
* 主线程不知道干扰线程做过修改,便产生了ABA问题。
* 比方金融钱,被挪用了又还回去了。
* 解决:给数据加版本号。AtomicStampedRerence提供用于解决aba问题。
*/
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);//如果还是a没变,便修改成功。(此处由于睡眠了一下,不知道干扰线程将这个数加一又减一了,主线程不知道干扰线程做过修改,便产生了ABA问题。比方金融钱,被挪用了又还回去了。解决:给数据加版本号。AtomicStampedRerence提供用于解决aba问题。)
                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();
 
 
    }
}
 
//打印结果
操作线程干扰线程--increase后值:2
操作线程干扰线程--decrease后值:1
操作线程主线程--修改前操作数值:1
操作线程主线程--Cas修改后操作数值:2
AtomicStampedReference用于解决ABA问题:
package my.mark.mybaibaoxiang.juc.biji07.Atomic;
 
 
import java.util.concurrent.atomic.AtomicStampedReference;
 
 
/**
* @author wxming.
* @description: TODO AtomicStampedReference用于解决ABA问题。
*/
public class AtomicStampedRerenceTest {
    private static AtomicStampedReference<Integer> atomicStampedRef =//Integer处为要修改的值类型。
            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();
    }
}
 
//打印结果
操作线程Thread[主操作线程,5,main]stamp=0,初始值 a = 1
操作线程Thread[干扰线程,5,main]stamp=1,【increment】 ,值 = 2
操作线程Thread[干扰线程,5,main]stamp=2,【decrement】 ,值 = 1
操作线程Thread[主操作线程,5,main]stamp=0,CAS操作结果: false
 
3.4、原子更新字段类
         如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类,Atomic包提
供了以下三个类:
  1. AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  2. AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  3. AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。
         原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个
更新器。原子更新类的字段的必须使用public volatile修饰符。
package my.mark.mybaibaoxiang.juc.biji07.Atomic;
 
 
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
 
 
/**
* @author wxming.
* @description: AtomicIntegerFieldUpdater基本使用。
*/
public class AtomicIntegerFieldUpdateTest {
 
 
    //属性原子修改器。要修改的类和要修改的属性。
    static AtomicIntegerFieldUpdater aifu = AtomicIntegerFieldUpdater.newUpdater(Student.class, "old");
 
 
    public static void main(String[] args) {
        Student stu = new Student("杨过", 18);
        System.out.println(aifu.getAndIncrement(stu));
        System.out.println(aifu.get(stu));
    }
 
 
    static class Student {
        private String name;
        public volatile int old;//TODO 用volatile修饰。保证高并发下的可见性?因为此处一旦使用Atomic做修改,证明都是高并发下的场景。
 
 
        public Student(String name, int old) {
            this.name = name;
            this.old = old;
        }
 
 
        public String getName() {
            return name;
        }
 
 
        public int getOld() {
            return old;
        }
    }
 
 
}
 
打印结果:
18
19
 
模仿自定义一个Atomic类,对某个属性进行原子修改:
package my.mark.mybaibaoxiang.juc.biji07.Atomic;
 
 
import my.mark.mybaibaoxiang.juc.biji07.utils.UnsafeInstance;
import sun.misc.Unsafe;
 
 
/**
* @author wxming.
* @description: 模仿自定义一个Atomic类,对某个属性进行原子修改。
*/
public class AtomicStudentAgeUpdater {
    private String name;
    private volatile int age;
 
 
    private static final Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
    private static final long valueOffset;//偏移量:new出来的实例对象在内存中占了一块区域,起始到结束。偏移量就是age这个属性从起始位置开试找起找到age属性的内存距离,相对起始的内存地址。见图示意。
 
 
    static {
        try {//静态代码块里初始化
            valueOffset = unsafe.objectFieldOffset(AtomicStudentAgeUpdater.class.getDeclaredField("age"));
            System.out.println("valueOffset:--->" + valueOffset);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
 
 
    //比较并且交换。
    public void compareAndSwapAge(int old, int target) {
        unsafe.compareAndSwapInt(this, valueOffset, old, target);
    }
 
 
 
 
    public AtomicStudentAgeUpdater(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
 
    public int getAge() {
        return this.age;
    }
 
 
    public static void main(String[] args) {
        AtomicStudentAgeUpdater updater = new AtomicStudentAgeUpdater("杨过", 18);
        updater.compareAndSwapAge(18, 56);
 
 
        System.out.println("真实的杨过年龄---" + updater.getAge());
 
 
    }
}
 
打印结果:
valueOffset:--->12
真实的杨过年龄---56
 
四、Unsafe应用解析
         Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的
方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了
类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。
在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语
言变得不再“安全”,因此对Unsafe的使用一定要慎重。
         Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用
getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。
package my.mark.mybaibaoxiang.juc.biji07.unsafe;
 
 
import sun.misc.VM;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
 
 
/**
* @author wxming.
* @Description Unsafe类模拟介绍
* @date 2020-7-22.
*/
public class Unsafe {
    // 单例对象
    private static final Unsafe theUnsafe;
 
 
    static {
        Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
        theUnsafe = new Unsafe();
    }
 
 
    private Unsafe() {
    }
 
 
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        // 仅在引导类加载器`BootstrapClassLoader`加载时才合法
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
}
 
4.1、如何获取Unsafe实例?
1、getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a
调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被
引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java ­Xbootclasspath/a:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
2、通过反射获取单例对象theUnsafe。
package my.mark.mybaibaoxiang.concurrent.jmmAndVolatile.util;
 
 
import sun.misc.Unsafe;
 
 
import java.lang.reflect.Field;
 
 
/**
* Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,
* 一旦能够直接操作内存,这也就意味着(1)不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
* (2)Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,
* 一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
* (3)直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。
*/
public class UnsafeInstance {
 
 
    public static Unsafe reflectGetUnsafe() {
        try {
            //Unsafe类中定义了许多可以直接越过虚拟机操作底层的方法。
            /** 魔术类
             * Unsafe的初始化方法主要是通过getUnsafe方法的单例模式实现的,调用JVM本地方法registerNatives()和sun.reflect.Reflection,
             * 通过Reflection.getCallerClass()判断当前调用的类是否是主类加载器(BootStrap classLoader)加载的,否则的话抛出一个SecurityException。
             * 这也证明了一个问题,那就是只有由主类加载器(BootStrap classLoader)加载的类才能调用这个类中的方法。
             * 我们自己开发的类是不让直接调用的,要调用只能通过反射。
             */
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
 
4.2、Unsafe功能介绍
Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系
统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细
介绍。
 
4.2.1、内存操作
这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。
//分配内存, 相当于C++malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes,
byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,
Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt
getDoublegetLonggetChar
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有:
putInt,putDoubleputLongputChar
public native void putObject(Object o, long offset, Object x);
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为
allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);
package my.mark.mybaibaoxiang.juc.biji07.unsafe;
 
 
import my.mark.mybaibaoxiang.juc.biji07.utils.UnsafeInstance;
import sun.misc.Unsafe;
 
 
/**
* @author wxming.
* @description: 内存操作
*/
public class DirectMemoryAccessTest {
 
 
    public static void main(String[] args) {
 
 
        Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
 
 
        long oneHundred = 1;
        byte size = 1;
 
 
        /*
         * 调用allocateMemory分配内存
         */
        long memoryAddress = unsafe.allocateMemory(size);
 
 
        /*
         * 将1写入到内存中
         */
        unsafe.putAddress(memoryAddress, oneHundred);
 
 
        /*
         * 内存中读取数据
         */
        long readValue = unsafe.getAddress(memoryAddress);
 
 
        System.out.println("value : " + readValue);
    }
}
 
打印结果:
value : 1
        通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM
所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机
制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java
中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。
 
使用堆外内存的原因:
1、对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以
当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿
对于应用的影响。
2、提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的
数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建
议存储到堆外内存。
 
典型应用
       DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲
池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、
使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。
       下图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过
Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建
Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃
圾回收时,分配的堆外内存一起被释放。
 
 
4.2.2、CAS相关
如下源代码释义所示,这部分主要为CAS相关操作的方法。
/**
* 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);
 
偏移量:
一个对象在内存中会占一定区域,某个属性的偏移量就是这个属性,在这块内存区域中相对于起始位置的距离量。
 
典型应用
        如下图所示,AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏
移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的
objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段
valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实
现对value字段的原子操作。
 
 
       下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址
baseAddress=“0x110000”,通过baseAddress+valueOffset得到value的内存地址
valueAddress=“0x11000c”;然后通过CAS进行原子性的更新操作,成功则返回,否则
继续重试,直到更新成功为止。
 
 
4.2.3、线程调度
包括线程挂起、恢复、锁机制等方法。
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
 
方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方
法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;
unpark可以终止一个挂起的线程,使其恢复正常。
package my.mark.mybaibaoxiang.juc.biji07.unsafe;
 
 
import java.util.concurrent.locks.LockSupport;
 
 
/**
* @author wxming.
* @description: 线程,阻塞、唤醒
*/
public class ThreadParkerTest {
 
 
    public static void main(String[] args) {
 
 
        /*Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread - is running----");
                LockSupport.park();//阻塞当前线程
                System.out.println("thread is over-----");
            }
        });
 
 
        t.start();
 
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LockSupport.unpark(t);//唤醒指定的线程*/
 
 
        //以下代码,如果先unpark再park,是不会阻塞的。相当于先拿票然后用。但是如果先park,没票便会阻塞当前线程。
        //拿出票据使用
        LockSupport.park();
 
 
        System.out.println("main thread is over");
        //相当于先往池子里放了一张票据
        LockSupport.unpark(Thread.currentThread());//park和unpark两个方法在Linux都是调的底层库Pthread_mutex
 
 
        System.out.println("im running step 1");
    }
}
结果:阻塞。
典型应用
        Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用
LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而
LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。
package my.mark.mybaibaoxiang.juc.biji07.unsafe;
 
 
import my.mark.mybaibaoxiang.juc.biji07.utils.UnsafeInstance;
import sun.misc.Unsafe;
 
 
/**
* @author wxming.
* @description: 加对象锁.//跨方法的加锁。
*/
public class ObjectMonitorTest {
    static Object object = new Object();
 
 
/*    public void method1(){
        unsafe.monitorEnter(object);
    }
 
 
    public void method2(){
        unsafe.monitorExit(object);
    }*/
 
 
    public static void main(String[] args) {
        /*synchronized (object){
        }*/
        Unsafe unsafe = UnsafeInstance.reflectGetUnsafe();
 
 
        unsafe.monitorEnter(object);
        //业务逻辑写在此处之间
        unsafe.monitorExit(object);
    }
}
 
4.2.4、内存屏障
       在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类
同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的
所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
 
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏
障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,
屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止loadstore操作重排序
public native void fullFence();
package my.mark.mybaibaoxiang.juc.biji07.unsafe;
 
 
import my.mark.mybaibaoxiang.juc.biji07.utils.UnsafeInstance;
 
 
/**
* @author wxming.
* @description: 内存屏障。
*/
public class FenceTest {
 
 
    public static void main(String[] args) {
 
 
        UnsafeInstance.reflectGetUnsafe().loadFence();//读屏障
 
 
        UnsafeInstance.reflectGetUnsafe().storeFence();//写屏障
 
 
        UnsafeInstance.reflectGetUnsafe().fullFence();//读写屏障
 
 
    }
}
 
典型应用
StampedLock的validate()方法使用内存屏障,防止指令重排
       在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改
进版本。StampedLock提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完
全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于
StampedLock提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线
程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要遵
从如下图用例中使用的模式来确保数据的一致性。
 
 
       如上图用例所示计算坐标点Point对象,包含点移动方法move及计算此点到原点的距
离的方法distanceFromOrigin。在方法distanceFromOrigin中,首先,通过
tryOptimisticRead方法获取乐观读标记;然后从主内存中加载点的坐标值 (x,y);而后通过
StampedLock的validate方法校验锁状态,判断坐标点(x,y)从主内存加载到线程工作内存
过程中,主内存的值是否已被其他线程通过move方法修改,如果validate返回值为true,
证明(x, y)的值未被修改,可参与后续计算;否则,需加悲观读锁,再次从主内存加载(x,y)
的最新值,然后再进行距离计算。其中,校验锁状态这步操作至关重要,需要判断锁状态是
否发生改变,从而判断之前copy到线程工作内存中的值是否与主内存的值存在不一致。
       下图为StampedLock.validate方法的源码实现,通过锁标记与相关常量进行位运算、
比较来校验锁状态,在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存
屏障,目的是避免上图用例中步骤②和StampedLock.validate中锁状态校验运算发生重排
序导致锁状态校验不准确的问题。
 
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值