图片 4

CAS与原子变量,看AtomicInteger源码学习CAS算法

Posted by

一、线程

CAS

CAS(Compare-And-Swap)是CPU的原子指令,中文翻译成比较交换,汇编指令为CMPXCHG。CAS操作包含三个操作数内存值V、预期原值A和新值B,当且仅当预期原值A和内存值V相同时,将内存值V修改为新值B,否则,处理器不做任何操作。

CAS C++示意:

int compare_and_swap (int* reg, int oldval, int newval) {  
    int old_reg_val = *reg;  
    ATOMIC();
    if(old_reg_val == oldval)  
        *reg = newval;  
    END_ATOMIC();
    return old_reg_val;  
}

CAS是一种乐观加锁策略,在进行争用操作(读-修改-写)时操作线程总是认为操作会成功,如果不成功则立即返回,确保一个时刻只有一个线程写成功。相比基于MutexLock线程互斥,CAS原子操作不用挂起线程,减少了调度线程的资源开销,另外大部分CUP都支持CAS指令,硬件级的同步原语也使得CAS更加轻量级。

CAS用在了哪里?
1、JVM对内置锁synchronized的优化中引入的偏向锁和轻量都是基于CAS实现的,使用CAS操作替换对象头中的ThreadID和MarkWord来实加锁和解锁,比重量级锁效率有所提升。

2、CAS更是整个J.U.C包的基石,原子变量、同步器、并发容器等都大量的依赖CAS。

图片 1

JUC实现依赖

CAS存在的问题?
CAS操作时以预期原值和当前内存值是否相等作为是否修改的依据,这就会出现ABA问题。所谓ABA是指如果变量V的值原来A,变成了B,又变成了A,CAS进行检查时会发现它的值没有发生变化,但实际上变量V已经被修改过了。如果只关注最终结果,不关注中间状态是如何变化的,那么绝大部分情况下ABA问题是不会对操作结果什么影响的。

但是如果需要关注变量的变化过程,比如,一个链表A-B-C,线程1执行CAS(A,B)准备将头结点换成B,这时线程2先执行了CAS(B,C),此时链表为A-C,节点B被拿出了,CUP切换到线程1,线程1发现节点A没有变化将节点A换成节点B,因为B-next为null,节点C也被从链表上删除了,因为链表的头节点没有发生变化,所以CAS操作链表头是允许的,但是链表本身已经发生过变化。ABA问题会漏掉一些监控时间窗口,对于依赖过程值的运算会产生影响。
解决ABA问题的方法就是给变量增加版本号,每一次CAS更新操作都对版本号进行累加,变量值和版本号同时作为CAS比较的目标,只要能保证版本号一直累加就不会出现ABA问题,J.U.C中的有专门处理这个问题的类。

简书 菜小川

 转载请注明原创出处,谢谢!

图片 2

1.1 线程的概述

  • 一个运行程序就是一个进程,而线程是进程中独立运行的子任务
  • 线程是操作系统执行流中的最小单位,一个进程可以有多个线程,这些线程与该进程共享同一个内存空间
  • 线程是系统独立调度和分派CPU的基本单位,通常有就绪、运行、阻塞三种基本状态
  • 随着硬件水平的提高,多线程能使系统的运行效率得到大幅度的提高,同时异步操作也增加复杂度和各种并发问题

Unsafe

CAS是CUP底层指令,JAVA代码中要想使用它必须借助JNI访问到系统底层,需要使用Unsafe类。
sun.misc.Unsafe是一个特殊的类,包含在sun包中,提供了很多native接口:

  /**返回指定field内存地址偏移量*/
  public native long objectFieldOffset(Field field);
  /**返回指定静态field内存地址偏移量*/
  public native long staticFieldOffset(Field field);
  /**获取给定数组中第一个元素的偏移地址*/
  public native int arrayBaseOffset(Class arrayClass);
  /**CAS设置Int*/
  public native boolean compareAndSwapInt(Object obj, long offset,
                                          int expect, int update);
  /**CAS设置Long*/
  public native boolean compareAndSwapLong(Object obj, long offset,
                                          long expect, long update);
  /**CAS设置Ojbect*/
  public native boolean compareAndSwapObject(Object obj, long offset,
                                        Object expect, Object update);
  /***
   * 设置obj对象中offset偏移地址对应的整型field的值为指定值。
   * 这是一个有序或者有延迟的方法,并且不保证值的改变被其他线程立即看到。
   * 只有在field被修饰并且期望被意外修改的时候使用才有用。
   */
  public native void putOrderedInt(Object obj, long offset, int value);
  public native void putOrderedLong(Object obj, long offset, long value);
  public native void putOrderedObject(Object obj, long offset, Object value);
  /***
   * 设置obj对象中offset偏移地址对应的整型field的值为指定值。
   * 支持volatile  store语义
   */
  public native void putIntVolatile(Object obj, long offset, int value);
  public native void putLongVolatile(Object obj, long offset, long value);
  public native void putObjectVolatile(Object obj, long offset, Object value);
  /***
   * 设置obj对象中offset偏移地址对应的long型field的值为指定值。
   */
  public native void putInt(Object obj, long offset, int value);
  public native void putLong(Object obj, long offset, long value);
  public native void putObject(Object obj, long offset, Object value);
  /***
   * 获取obj对象中offset偏移地址对应的整型field的值,支持volatile load语义。
   */
  public native int getIntVolatile(Object obj, long offset);
  public native long getLongVolatile(Object obj, long offset);
  public native Object getObjectVolatile(Object obj, long offset);
  /***
   * 获取obj对象中offset偏移地址对应类型field的值
   */
  public native long getInt(Object obj, long offset);
  public native long getLong(Object obj, long offset);
  public native Object getObject(Object obj, long offset);
  /**挂起线程*/
  public native void park(boolean isAbsolute, long time);
  /**唤醒线程*/
  public native void unpark(Thread thread);
  /**内存操作*/
  public native long allocateMemory(long l);
  public native long reallocateMemory(long l, long l1);
  public native void freeMemory(long l);

这些方法在J.U.C中都会被大量使用,compareAndSwap就是CAS方法,分别对应int,long,Object三种数据类型的CAS操作,参数o是需要更新的对象、offset是对象字段在内存中的偏移量,expected是对象字段的预期原值,x是要更新的新值。

putXXXVolatile、getXXXVolatile方法是以Volatile语义设置和获取属性值,它们在并发容器中会大量用到。

park、unpark分别是挂起和唤醒线程,在AQS中会使用到。

Unsafe虽然强大但是对普通开发者是限制使用的,从源码中可以看到。

private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
    Class cc = sun.reflect.Reflection.getCallerClass(2);
    if (cc.getClassLoader() != null)
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

构造方法私有的,只提供一个工厂方法getUnsafe()。因为BootstrapClassLoader用来加载jre/目录下的核心库,它是由C++编写的虚拟机的一部分,JAVA代码无获取它的引用。所以sun.reflect.Reflection.getCallerClass(2)获取上层调用类cc为空说明调用者是BootstrapClassLoader加载的类,不为空说明调用者是其他类加载器加载的类,就会抛出SecurityException异常,也就是说只有通过BootstrapClassLoader加载的类才能对其进行实例化。

有些特殊情况比如NIO程序中的堆外内存(不会被GC回收)分配,可以通过反射获取Unsafe实例。

public static Unsafe  getUnsafe() {
    Field theUnsafe = null;
    Unsafe instance = null;
    try {
        theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        instance = (Unsafe) theUnsafe.get(null);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return instance;
}

线程带来的风险

Java支持多线程开发,在多线程开发过程中,开发人员需要格外注意三个方面的问题:安全性、活跃性和性能

安全性问题主要关注原子性、可见性和顺序性

活跃性问题主要关注死锁、饥饿和活锁

性能问题主要关注线程创建和销毁、上下文切换、调度等。

1.2 多线程的风险之一上下文切换

上下文切换:
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时可以重新加载这个任务的状态。所有任务从保存到再加载的过程就是一次上下文切换
多线程性能问题:由于线程有创建和上下文切换的开销,在多线程环境下,这种开销对时间和资源的利用都是一个极大的负担,很可能导致并发任务执行速度还不如串行快
减少上下文切换: 无锁并发编程、CAS算法、减少并发、使用最少线程、协程
.

原子变量

java.util.concurrent.atomic包中,提供了包含基本类型和引用类型的12中原子变量:

基本类型

  1. AtomicBoolean 原子布尔型
  2. AtomicInteger 原子整型
  3. AtomicLong 原子长整型

引用类型

  1. AtomicReference 原子引用类型
  2. AtomicMarkableReference原子标记位引用类型
  3. AtomicStampedReference原子版本号引用类型

字段类型

  1. AtomicIntegerFieldUpdater原子整型字段更新器
  2. AtomicLongFieldUpdater原子长整型字段更新器
  3. AtomicReferenceFieldUpdater原子应用字段更新器

数组类型

  1. AtomicIntegerArray 原子整型数组
  2. AtomicLongArray 原子长整型数组
  3. AtomicReferenceArray 原子引用数组

java中Volatile关键字只能保证变量单次操作的原子性,对与复合操作比如:i++,要想使其具有原子性只能加锁使线程同步,而这些原子变量,能够在不添加任务额外同步操作的情况下具有原子性。

AtomicInteger

int类型原子变量,内部布局:

public class AtomicInteger extends Number 
            implements java.io.Serializable {
    //Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //内存地址偏移量
    private static final long valueOffset;
    //int 值
    private volatile int value;
    public AtomicInteger() {}
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public final int get() {
        return value;
    }
    public final void set(int newValue) {
        value = newValue;
    }
    ... ...
}

getAndIncrement方法:

public final int getAndIncrement() {
    for (;;) {//循环CAS
        //获取当前值
        int current = get();
        //累加后的值 新值
        int next = current + 1;
        //CAS 更新,如果更新成功返回当前值
        if (compareAndSet(current, next))
            return current;
    }
}
/**unsafe CAS 更新INT变量*/
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

因为value变量是volatile修饰的,get()方法获取的当前值current肯定是最新值。将其+1作为新值next,进行CAS操作compareAndSet,for()循环体直到CAS设置成功才退出。在此期间不用担心其他线程的干扰,因为一旦有其他线程修改value,就会致使compareAndSet失败并立即返回重试。

getAndIncrement和incrementAndGet是两个常用的方法,get在前说是先返回再加1即返回来的是原值,get在后返回的是新值。

AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater用来CAS操作对象里面的int类型的字段,是一个抽象类,其具体实现由私有内部类AtomicIntegerFieldUpdaterImpl完成,AtomicIntegerFieldUpdater无法直接实例化,需使用静态工厂方法newUpdater获取其实例。

static class Person {
    volatile int age;
    public Person(int age) {
        this.age = age;
    }
}
public void testUpdate() {
    Person person = new Person(30);
    AtomicIntegerFieldUpdater<Person> updater = 
        AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
    boolean succeed = updater.compareAndSet(person, 30, 31);
    Assert.true(succeed);
    boolean failure  = updater.compareAndSet(person, 32, 33);
    Assert.false(failure);
}

更新的字段必须是volatile修饰的,因为要保证其可见性,同时也要保证字段是可访问的,如果是更新方法compareAndSet()和字段在同一个类中,字段可以是protected和private的,否则必须public的。AtomicLongFieldUpdater原子长整型字段更新器、AtomicReferenceFieldUpdater原子应用字段更新器与AtomicIntegerFieldUpdater使用方式一样。

AtomicStampedReference

AtomicStampedReference是避免ABA问题的引用类型,内部布局:

public class AtomicStampedReference <V> {
    //内部类
    private static class Pair<T> {
        final T reference;//引用
        final int stamp;//版本号
    //构造方法
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
    //工厂方法
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;//Pair实例 volatile可见性

    //构造方法
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
    ... ...
}

内部类Pair封装了引用对象reference和版本号stamp,初始化AtomicStampedReference时会使用引用initialRef和版本initialStamp实例化一个对象pair。

compareAndSet操作

/**
 * @param expectedReference 期望原值
 * @param newReference 新值
 * @param expectedStamp 原版本号
 * @param newStamp 新版本号
 */
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

如果期望值expectedReference等于当前值current.reference并且期望版本号expectedStamp等于当前版本号current.stamp才会往下进行,否则,直接返回false,设置失败。

如果当新值newReference等于当前值current.reference新版本号newStamp等于当前版本号current.stamp,也没必要CAS操。

用新值和新版本号实例化一个Pair对象,和原值current进行CAS操作。

锁机制存在的问题

synchronized(悲观锁、独占锁)内置加锁机制保证线程安全性,主要是指保证原子性和可见性,但是锁上发生竞争时,竞争失败的线程肯定会阻塞,会引发一些性能问题,例如一个线程持有锁后其他线程需要挂起、加锁和释放锁导致过多上下文切换和调度延时、如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置等。

volatile变量是一种轻量级的同步机制,也能保证共享变量可见性,不能保证原子性,当进行复合操作时,就不能使用volatile变量。

对于synchronized和volatile存在的阻塞和原子问题,本文主要探讨原子变量和非阻塞算法同步机制,解决一些性能问题。

二、并发编程中的锁

小结

1、CAS作为硬件同步原语,相比基于线程互斥的同步操作要更加轻量级,volatile的可见性加上CAS原子操作是J.U.C中无锁并发的基础。

2、原子变量可以使变量在无锁的情况下保持原子性。

码字不易,转载请保留原文连接http://www.jianshu.com/p/864b2786ec99

乐观锁

乐观锁其实就是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

2.1 悲观锁

Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。存在以下问题:
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;一个线程持有锁会导致其它所有需要此锁的线程挂起;
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

CAS

CAS(Compare and
Swap即比较并交换)是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作包含三个操作数 —— memory location V(内存位置V)、expected old
value( 预期值A)和new value(
新值B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在
CAS 指令之前返回该位置的值。

2.2 乐观锁

乐观锁( Optimistic
Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
上面提到的乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是Compare
and Swap(CAS)。

JVM对CAS的支持

Java5.0之前,如果不编写明确的代码,那么就无法执行CAS。在Java5.0中引入底层的支持。java.util.concurrent包中大多数类在实现时直接或间接使用了原子变量类,原子变量类是指java.util.concurrent.atomic中的AtomicXxx,他们使用这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作。

我们以原子变量类AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。

图片 3

几个关键点:

1、value是volatile变量,保证可见性。

2、unsafe是Unsafe类的实例,它提供了硬件级别的原子操作

3、valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值V。

代码执行

1、AtomicInteger里面的value原始值假设为10,即主内存中AtomicInteger的value为10,根据Java内存模型,线程1和线程2各自持有一份value的副本,值为10

2、线程1运行到第十行获取到当前的value为10,线程切换

3、线程2开始运行,获取到value为10,利用CAS对比内存中的值也为10,比较成功,修改内存,此时内存中的value改变成11,线程切换

4、线程1恢复运行,利用CAS比较发现自己的value为10,内存中的value为11,得到一个重要的结论–>此时value正在被另外一个线程修改,所以我不能去修改它

5、线程1的compareAndSet失败,循环判断,因为value是volatile修饰的,所以它具备可见性的特性,线程2对于value的改变能被线程1看到,只要线程1发现当前获取的value是11,内存中的value也是11,说明线程2对于value的修改已经完毕并且线程1可以尝试去修改它。

三、无锁执行者CAS

ABA问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的”ABA”问题。

微信公众号

图片 4

3.1 无锁的概念

在谈论无锁概念时,总会关联起乐观派与悲观派,对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事,但对于悲观派而已,他们总会认为发展事态如果不及时控制,以后就无法挽回了,即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁与无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键,下面我们进一步了解CAS技术的奇妙之处。

3.2 CAS

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。其算法核心思想如下

执行函数:CAS(V,E,N)

其包含3个参数

  • V表示要更新的变量

  • E表示预期值

  • N表示新值

如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下:

CAS原理图

四、Java对CAS的支持

我们以java.util.concurrent中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。

4.1 AtomicInteger 类的变量以及静态代码块

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    //获取unsafe对象
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    //value在内存中的地址偏移量  
    private static final long valueOffset;

    static {
        try {
            //获得value的内存地址偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //当前对象代表的值,注意是volatile(**下面会解释该关键字**)
    private volatile int value;

4.2 深究Unsafe类

从这个类的名字Unsafe上来说这个类就是一个不安全的类,它存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,也不开放给用户直接使用的(当然我们还是可以通过其他一些方法用到)。Java
9中将移除
Sun.misc.Unsafe,
原文链接:https://yq.aliyun.com/articles/87265

@CallerSensitive
    public static Unsafe getUnsafe() {

        //得到调用者的class对象,这里即是Unsafe
        Class arg = Reflection.getCallerClass();

       //判断调用Unsafe的类是否是BootstrapClassLoader加载的类 
        if (!VM.isSystemDomainLoader(arg.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

这个类本身是单例的,需要通过静态方法获取唯一实例。根据代码知道应该是通过类加载器限制。一般我们写的类都是由Application
ClassLoader(sun.misc.Launcher$AppClassLoader)进行加载的,层级比较低,这里的SystemDomainLoader就是BootstarpClassLoader(C++写的),也就是加载rt.jar里面的类的加载器,所以Java.xx用就不会有事,我们用就会有事。
想要使用Unsafe有两种方式。一种是用反射,比较简单;另外一种是通过虚拟机启动参数-Xbootclasspath,把你的classpath变为启动路径之一,这样就是BootstarpClassLoader加载你的类,跟java.xx一个待遇了,就不会报错了。可以看到,虽然是可以调用,但是会有一步判断,判断是不是内部会检查该CallerClass是不是由系统类加载器BootstrapClassLoader加载,因为它是不安全的类,官方api也没有对这个包下的类进行解释说明,如果是开发人员引用这个包下的类则会抛错。由系统类加载器加载的类调用getClassLoader()会返回null,所以要检查类是否为bootstrap加载器加载只需要检查该方法是不是返回null。
下面会重点讲解类加载器

4.3 类加载器

从下面的注释我们可知只要是由bootstrap加载器加载的类,返回值是null,这也就进一步说明了,java官方禁止自定义使用该类。

 /**
     * Returns the class loader for the class.  Some implementations may use
     * null to represent the bootstrap class loader. This method will return
     * null in such implementations if this class was loaded by the bootstrap
     * class loader.
     *
     */
    @CallerSensitive
    public ClassLoader getClassLoader() {
        ClassLoader cl = getClassLoader0();
        if (cl == null)
            return null;

        //JVM安全管理器,这里不做重点介绍
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
        }
        return cl;
    }

    ClassLoader getClassLoader0() { return classLoader; }
4.3.1 Class 文件有哪些来源呢?

首先,最常见的是开发者在应用程序中编写的类,这些类位于项目目录下;

然后,有 Java 内部自带的 核心类 如 java.lang、java.math、java.io 等
package 内部的类,位于 $JAVA_HOME/jre/lib/ 目录下,如
java.lang.String 类就是定义在 $JAVA_HOME/jre/lib/rt.jar 文件里;

另外,还有 Java 核心扩展类,位于 $JAVA_HOME/jre/lib/ext
目录下。开发者也可以把自己编写的类打包成 jar 文件放入该目录下;
最后还有一种,是动态加载远程的 .class 文件。

既然有这么多种类的来源,那么在 Java 里,是由某一个具体的 ClassLoader
来统一加载呢?还是由多个 ClassLoader 来协作加载呢?

4.3.2 哪些 ClassLoader 负责加载上面几类 Class?

首先,我们来看级别最高的 Java 核心类 ,即$JAVA_HOME/jre/lib 里的核心
jar 文件。这些类是 Java 运行的基础类,由一个名为 BootstrapClassLoader
加载器负责加载,它也被称作
根加载器/引导加载器。注意,BootstrapClassLoader 比较特殊,它不继承
ClassLoader,而是由 JVM 内部实现;

然后,需要加载 Java 核心扩展类 ,即 $JAVA_HOME/jre/lib/ext 目录下的
jar 文件。这些文件由 ExtensionClassLoader 负责加载,它也被称作
扩展类加载器。当然,用户如果把自己开发的 jar 文件放在这个目录,也会被
ExtClassLoader 加载;

接下来是开发者在项目中编写的类,这些文件将由 AppClassLoader
加载器进行加载,它也被称作 系统类加载器 System ClassLoader;

最后,如果想远程加载如(本地文件/网络下载)的方式,则必须要自己自定义一个
ClassLoader,复写其中的 findClass() 方法才能得以实现。

因此能看出,Java 里提供了至少四类 ClassLoader 来分别加载不同来源的
Class。

4.3.4 解压查看$JAVA_HOME/jre/lib/rt.jar文件

import

isun\misc的Unsafe类

通过上面两个图,证明了,Unsafe类是由BootstrapClassLoader
加载器加载的,所以在获取classLoader时正常情况下是返回null。

4.3.5 CallerSensitive注解是什么鬼?

细心的同学可能已经发现上面获取类加载器的方法上有该注解,那么它的作用是啥呢?我们先看stackoverflow网站给出的答案

CallerSensitive

简而言之,用@CallerSensitive注解修饰的方法从一开始就知道具体调用它的对象,这样就不用再经过一系列的检查才能确定具体调用它的对象了。它实际上是调用sun.reflect.Reflection.getCallerClass方法。

4.4 说下AtomicInteger类的getAndIncrement方法

 public final int getAndIncrement() {

        // 当前值加1返回旧值,底层CAS操作
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int x) {
        int expected;
        do {
            //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
            expected= this.getIntVolatile(o, offset);

        //第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,expected表示期望值,expected+x表示要设置的值。
        } while (!this.compareAndSwapInt(o, offset, expected, expected+ x));

        return expected;
    }

4.5 看线程的挂起与恢复理解Unsafe运行机制

将一个线程进行挂起是通过park方法实现的,调用
park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在
LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法

//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
public native void park(boolean isAbsolute, long time);  

//终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法,  
public native void unpark(Object thread); 

4.6 通过例子加深对Unsafe的理解

 private static Unsafe unsafe;

    public static void main(String[] args) {

        try {
            //通过反射获取rt.jar下的Unsafe类
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置该Field为可访问
            field.setAccessible(true);
            // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
            unsafe = (Unsafe) field.get(null);
            Integer target = 12;
            //compareAndSwapInt方法的属性分别是:目标对象实例,目标对象属性偏移量,当前预期值,要设的值.
            //compareAndSwapInt方法是通过反射修改对象的值,具体修改对象下面那个值,可以通过偏移量,对象字段的偏移量可以通过objectFieldOffset获取
            System.out.println(unsafe.compareAndSwapInt(target, 12, 12, 24));
        } catch (Exception e) {
            System.out.println("Get Unsafe instance occur error" + e);
        }
    }

输入不同的参数得到以下结果:

正确的期望值

错误的期望值

4.7 CAS的ABA问题及其解决方案

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换(比较和交换是原子性),如果在取出和比较并交换之间发生数据变化而不能察觉,就出现所谓的ABA问题了。

image.png

ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。

优化方向:CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。

常见实践:“版本号”的比对,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。

五、对volatile关键字的理解

5.1 volatile写操作的内存语义

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存

写操作

5.2 volatile读操作的内存语义

读操作

5.3 变量在内存中的工作过程

image.png

5.4 volatile非原子原因

  • 多线程环境下,”数据计算”和”数据赋值”操作可能多次出现,即操作非原子
  • 若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致
  • 对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步

六、参考文章

偶然的机会看了下面其中一篇文章便开始对cas产生了兴趣,激起我继续看源码写文章的激情。感谢下面的作者们,深度好文!

https://www.zybuluo.com/kiraSally/note/850631
http://www.10tiao.com/html/249/201706/2651960240/1.html
https://juejin.im/entry/595c599e6fb9a06bc6042514

相关文章

Leave a Reply

电子邮件地址不会被公开。 必填项已用*标注