JUC探险-5、原子类

18 篇文章 0 订阅

文章目录

一、概述与结构:

  ①原子类

    在并发编程中很容易出现线程安全问题,一个常见的例子就是多线程更新变量i。比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。
    实际上,在J.U.C下的atomic包提供了一系列的操作简单、性能高效,并能保证线程安全的类去更新基本类型变量、数组元素、引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。

  ②类型分类

    整体类结构可以参考JUC探险-1、初识概貌中的图。
    现在大致将原子类分为五类:
      ●基本类型:AtomicBooleanAtomicIntegerAtomicLong
      ●引用类型:AtomicReferenceAtomicStampedRerenceAtomicMarkableReference
      ●数组:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray
      ●对象的属性:AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
      ●JDK1.8新增类:DoubleAdderLongAdderDoubleAccumulatorLongAccumulator


二、:原子更新基本类型

  ①基本作用

    ●AtomicBoolean:以原子更新的方式更新Boolean。
    ●AtomicInteger:以原子更新的方式更新Integer。
    ●AtomicLong:以原子更新的方式更新Long。

  ②常用API

    这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法。
    ●addAndGet(int delta):以原子方式将输入的数值与实例中原本的数值相加,并返回相加后的结果。
    ●incrementAndGet():以原子的方式将实例中的原数值进行加1操作,并返回相加后的结果。
    ●getAndSet(int newValue):将实例中的数值更新为新数值,并返回旧数值。
    ●getAndIncrement():以原子的方式将实例中的原数值加1,返回的是自增前的旧数值。

  ③源码分析

    1、getAndIncrement()

public final int getAndIncrement() {
	return unsafe.getAndAddInt(this, valueOffset, 1);
}

      可以看出,该方法实际上是调用了unsafe实例的getAndAddInt()方法,unsafe实例的获取时通过UnSafe类的静态方法getUnsafe()获取:

private static final Unsafe unsafe = Unsafe.getUnsafe();

  Unsafe类在sun.misc包下,Unsafer类提供了一些底层操作,atomic包下的原子操作类的也主要是通过Unsafe类提供的compareAndSwapInt(),compareAndSwapLong()等一系列提供CAS操作的方法来进行实现。

    2、compareAndSet()

      AtomicBoolean类实现更新的核心方法是compareAndSet()方法,其源码如下:

public final boolean compareAndSet(boolean expect, boolean update) {
	int e = expect ? 1 : 0;
	int u = update ? 1 : 0;
	return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

      可以看出,compareAndSet()方法的实际上也是先转换成0、1的整型变量,然后是通过针对int型变量的原子更新方法compareAndSwapInt()来实现的。

  AtomicInteger类主要利用CAS(compare and swap)+ volatile 和 native 方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

public class AtomicInteger extends Number implements java.io.Serializable {
	// setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}

  UnSafe类的objectFieldOffset()方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外value是一个volatile变量,在内存中可见,因此JVM可以保证任何时刻任何线程总能拿到该变量的最新值。

  atomic包中只提供了对boolean、int、long这三种基本类型的原子更新的方法。那么参考这些更新的思想、方式,原子更新char、double、float也可以采用类似的实现。


三、:原子更新引用类型

  ①基本作用

    ●AtomicReference:原子更新引用类型。
    ●AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。(带版本号是为了解决CAS的ABA问题)
    ●AtomicMarkableReference:原子更新带有标记位的引用类型。

  ②常用API

    这几个类的用法基本一致,这里以AtomicReference为例总结常用的方法。
    ●compareAndSet(V expect, V update):以原子更新的方式将对象进行更新。
    ●getAndSet(V newValue):将实例中的对象更新为新对象,并返回旧对象。

  ③源码分析

    1、compareAndSet()

public final boolean compareAndSet(V expect, V update) {
	return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

      可以看出,该方法实际上是调用了unsafe实例的compareAndSwapObject()方法,与原子基本类型情况类似。不同之处在于比较的是对象。


四、:原子更新数组

  ①基本作用

    ●AtomicIntegerArray:原子更新Integer数组中的元素。
    ●AtomicLongArray:原子更新Long数组中的元素。
    ●AtomicReferenceArray:原子更新引用类型数组中的元素。

  ②常用API

    这几个类的用法基本一致,这里以AtomicIntegerArray为例总结常用的方法。
    ●addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加。
    ●getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1。
    ●compareAndSet(int i, int expect, int update):以原子更新的方式将数组中索引为i的元素进行更新。

  ③源码分析

    1、addAndGet()

public final int addAndGet(int i, int delta) {
	return getAndAdd(i, delta) + delta;
}
public final int getAndAdd(int i, int delta) {
	return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}

      可以看出,该方法实际上是调用了unsafe实例的getAndAddInt()方法,与原子基本类型情况类似。不同之处在于会多一个指定数组索引位i,从数组中取值。


五、:原子更新字段类型

  ①基本作用

    ●AtomicIntegerFieldUpdater:原子更新整型字段类。
    ●AtomicLongFieldUpdater:原子更新长整型字段类。
    ●AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

  ②常用API

    这几个类的用法基本一致,这里以AtomicIntegerFieldUpdater为例总结常用的方法。
    ●compareAndSet(T obj, int expect, int update):以原子更新的方式将对象中的字段进行更新。
    ●getAndIncrement(T obj):以原子更新的方式将对象中的字段自增加1。

  ③源码分析

    1、compareAndSet()

public final boolean compareAndSet(T obj, int expect, int update) {
	accessCheck(obj);
	return U.compareAndSwapInt(obj, offset, expect, update);
}

      可以看出,该方法实际上是调用了unsafe实例的compareAndSwapInt()方法,与原子基本类型情况类似。不同之处在于在操作之前检查了对象类型。

  要想使用原子更新字段需要两步操作:
    1、原子更新字段类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的属性,例如:

private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(类名.class,"字段名");

    2、更新类的属性必须使用public volatile进行修饰。


六、:JDK1.8原子类型

  ①新增内容简介

    DoubleAdder、LongAdder、DoubleAccumulator、LongAccumulator是JDK1.8新增的部分,是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

  ②LongAdder分析

    AtomicLong的实现方式是内部有个value变量,当多线程并发自增、自减时,均通过CAS指令从机器指令级别操作保证并发的原子性。

    LongAdder中包含了一个Cell数组,Cell是Striped64的一个内部类,顾名思义,Cell代表了一个最小单元。定义如下:

// 为提高性能,使用注解@sun.misc.Contended,用来避免伪共享
@sun.misc.Contended static final class Cell {
	// 用来保存要累加的值
	volatile long value;
	Cell(long x) { value = x; }
	// cas更新value值
	final boolean cas(long cmp, long val) {
		return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
	}
	
	// Unsafe mechanics
	private static final sun.misc.Unsafe UNSAFE;
	private static final long valueOffset;
	// 这个静态方法用于获取偏移量
	static {
		try {
			UNSAFE = sun.misc.Unsafe.getUnsafe();
			Class<?> ak = Cell.class;
			valueOffset = UNSAFE.objectFieldOffset(ak.getDeclaredField("value"));
		} catch (Exception e) {
			throw new Error(e);
		}
	}
}

    Cell内部有一个非常重要的value变量,并且提供了一个CAS更新其值的方法。

    1、add()

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
    	// uncontended标识在cells中,当前线程要做cas累加操作的某个元素是否存在争用。(true代表不存在争用,false代表存在争用)
        boolean uncontended = true;
        // 判断一、判断二
        // 此处两个判断都代表cells数组没有被初始化成功(初始化成功的cells数组长度为2)
        if (as == null || (m = as.length - 1) < 0 ||
        	// 判断三
        	// 通过getProbe方法获取当前线程Thread的threadLocalRandomProbe变量的值,初始为0。
        	// getProbe() & m相当于getProbe() % cells.length
        	// 如果cells数组对应下标处的值为null,说明这个位置从来没有线程做过累加,继续向下执行,此时创建一个新的Cell对象a
            (a = as[getProbe() & m]) == null ||
            // 判断四
            // 尝试对cells数组对应下标处的值做累加操作,并返回操作结果;如果失败继续向下执行
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}
      ⅰ、运行进入if判断,cells此时为null,进入casBase()方法:
final boolean casBase(long cmp, long val) {
    return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

        此处执行CAS累加操作,并返回成功与否。

      ⅱ、如果casBase()方法执行成功,则不会进入if内部。但是当在高并发的情况下,casBase()方法可能执行失败,此时AtomicLong和LongAdder处理方式就有了差异。

        ●AtomicLong会自旋直到原子操作成功。
        ●LongAdder会进入if内部执行操作。(具体操作比较复杂,请往下看)

      ⅲ、在if内部,先设置uncontended标识为未争用,然后进行判断、操作。通过所有判断到达代码内部有三种情况:

        ●cells没有初始化。(通过前两个判断)
        ●当前线程hash到的cells数组中的位置还没有其它线程做过累加操作。(通过第三个判断)
        ●产生了冲突。(通过第四个判断)

      ⅳ、此时如果通过了各项判断,最终会来到longAccumulate()方法:
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    // 获取当前线程的threadLocalRandomProbe值作为h
    // 如果h == 0,代表新的线程开始参与cells争用,可能的场景有两种(凡是参与了cells争用操作的线程threadLocalRandomProbe都不为0)
    // 	 1、当前线程之前还没有参与过cells争用(或者cells数组还没初始化,进到当前方法来就是为了初始化cells数组后争用的),在外层cas操作失败
    // 	 2、在外层执行add()方法时,对cells某个位置的Cell的cas操作失败,将wasUncontended设置为false
    if ((h = getProbe()) == 0) {
    	// 如果h为0,说明当前线程是第一次进入该方法,则强制设置线程的threadLocalRandomProbe为ThreadLocalRandom类的成员静态私有变量probeGenerator的值,后文在介绍hash值的生成时会详细说明
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    // cas冲突标志,表示当前线程hash到的Cells数组的位置。(collide=true代表有冲突,collide=false代表无冲突)
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        // 分支一:cells已经初始化
        if ((as = cells) != null && (n = as.length) > 0) {
        	// 内部分支1:如果被hash到的位置为null,说明没有线程在这个位置设置过值,没有竞争,可以直接使用
        	if ((a = as[(n - 1) & h]) == null) {
            	// cellsBusy == 0,代表当前没有线程对cells数组做修改
                if (cellsBusy == 0) {       // Try to attach new Cell
                	// 将要累加的x值作为初始值创建一个新的Cell对象
                    Cell r = new Cell(x);   // Optimistically create
                    // cellsBusy == 0,尝试获取cellsBusy锁
                    if (cellsBusy == 0 && casCellsBusy()) {
                    	// 标记Cell是否创建成功并放入到cells数组被hash的位置上
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            // 再次检查cells数组不为null,且长度大于0,且hash到的位置的Cell为null
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                // 将新的cell设置到该位置
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                        	// 解除cellsBusy锁
                            cellsBusy = 0;
                        }
                        // 生成成功,跳出循环
                        if (created)
                            break;
                        // 如果created为false,说明上面指定的cells数组的位置cells[m % cells.length]已经有其它线程设置了cell了,继续执行循环
                        continue;           // Slot is now non-empty
                    }
                }
                // 如果执行到此处,说明cellsBusy不为0,有线程正在更改cells数组,代表产生了冲突,于是将collide设置为false
                collide = false;
            }
            // 内部分支2:被hash到的位置不为null,存在竞争
            else if (!wasUncontended)       // CAS already known to fail
            	// 设置未竞争标志位true,继续执行(后面会重新计算prob的值,然后重新执行循环)
                wasUncontended = true;      // Continue after rehash
            // 内部分支3:新的争用线程参与争用的情况(外层cas竞争失败,进入后未竞争标志位被设置为true)
            // 尝试将x值加到cells[m % cells.length]的value,如果成功直接退出
            else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
            // 内部分支4:内部分支3处理新的线程争用执行失败了
            // 这时如果cells数组的长度已经到了最大值(大于等于cup数量),或者是当前cells已经做了扩容,则将collide设置为false(后面会重新计算prob的值,然后重新执行循环)
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            // 内部分支5:cas冲突标志为false(无冲突)
            else if (!collide)
            	// 设置cas冲突标志为true,表示发生了冲突,需要再次生成hash(再次循环走到此处时会进入内部分支6)
                collide = true;
            // 内部分支6:至少两次竞争失败,考虑开始cells扩容
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                	// 检查cells是否已经被扩容
                    if (cells == as) {      // Expand table unless stale
                    	// cells扩容
                        Cell[] rs = new Cell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            // 为当前线程重新计算hash值
            h = advanceProbe(h);
        }
        // 分支二:cells尚未初始化或长度为0
        // 尝试获取cellsBusy锁
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                	// 初始化cells数组,初始容量为2,并将要累加的x值通过hash&1,放到0个或第1个位置上
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
            	// 解除cellsBusy锁
                cellsBusy = 0;
            }
            // 如果init = true,说明初始化成功,跳出循环
            if (init)
                break;
        }
        // 分支三:cells尚未初始化或长度为0,并且尝试获取cellsBusy锁失败
        // 尝试将值累加到base上
        else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}
        ㈠获取当前线程的threadLocalRandomProbe值,判断是否为0。

          如果threadLocalRandomProbe为0,代表新的线程开始参与cells争用,可能的场景有两种:
            ●当前线程之前还没有参与过cells争用(或者cells数组还没初始化,进到当前方法来就是为了初始化cells数组后争用的),在外层cas操作失败。
            ●在外层执行add()方法时,对cells某个位置的Cell的cas操作失败,将wasUncontended设置为false。

        ㈡进入一个死循环,其中包含外层三个分支路线,第一个分支路线又包含内部6个子分支路线。

          ●外层分支一:cells已经初始化时进入(处理外层判断三、判断四)
            ▶内层分支1:被hash到的位置为null,说明没有线程在这个位置设置过值,没有竞争,可以直接使用。此时用x值作为初始值创建一个新的Cell对象,对cells数组使用cellsBusy加锁,然后将这个Cell对象放到cells[m % cells.length]位置上。
            ▶内层分支2:被hash到的位置不为null,存在竞争。此时设置未竞争标志位true,继续执行。(后面会重新计算prob的值,然后重新执行循环)
            ▶内层分支3:新的争用线程参与争用的情况(外层cas竞争失败,进入后未竞争标志位被设置为true)。尝试将x值加到cells[m % cells.length]的value,如果成功直接退出。
            ▶内层分支4:内部分支3处理新的线程争用执行失败了。这时如果cells数组的长度已经到了最大值(大于等于cup数量),或者是当前cells已经做了扩容,则将collide设置为false。(后面会重新计算prob的值,然后重新执行循环)
            ▶内层分支5:cas冲突标志为false(无冲突)。设置cas冲突标志为true,表示发生了冲突,需要再次生成hash。(再次循环走到此处时会进入内部分支6)
            ▶内层分支6:至少两次竞争失败,考虑开始cells扩容。扩容前先检查是否已经扩容。
            为当前线程重新计算hash值。
          ●外层分支二:cells尚未初始化或长度为0时进入(处理外层判断一、判断二)
            ▶先尝试获取cellsBusy锁。
            ▶初始化cells数组,初始容量为2,并将要累加的x值通过hash&1,放到0个或第1个位置上。
            ▶解除cellsBusy锁,初始化成功后跳出循环。
          ●外层分支三:cells尚未初始化或长度为0,且获取cellsBusy锁失败时进入
            ▶尝试将要累加的x值累加到base上。

      ⅴ、回头看最外层if的判断,其实当不存在线程竞争的时候,cells将为null。只有当casBase()失败后,才会初始化cells。

    2、add()其他补充说明

      ⅰ、hash值的生成

        这个hash值是LongAdder定位当前线程应该将值累加到cells数组哪个下标的。
        首先看下Thread类中的一个成员变量threadLocalRandomProbe:

@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

        threadLocalRandomProbe这个变量的值就是LongAdder用来hash定位Cell在cells数组位置的,平时线程的这个变量一般用不到,它的值一直都是0。

        在Striped64中通过getProbe()方法获取当前线程threadLocalRandomProbe的值:

static final int getProbe() {
	// PROBE是threadLocalRandomProbe变量在Thread类里面的偏移量
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
      ⅱ、threadLocalRandomProbe的初始化

        在add()方法中,threadLocalRandomProbe的值保持为0,如果进入longAccumulate()方法,其值才会发生改变。
        进入longAccumulate()方法后,第一件事就是判断threadLocalRandomProbe的值是否为0,为0时会将其初始化,核心逻辑在ThreadLocalRandom.current()中:

private static final AtomicInteger probeGenerator = new AtomicInteger();
public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}
static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

        probeGenerator是static类型的AtomicInteger类,每执行一次localInit()方法,都会将probeGenerator累加一次。如果取得的值为0,会使用1提代。最后把值更新到threadLocalRandomProbe。

      ⅲ、threadLocalRandomProbe重新生成(重新计算hash)

        在longAccumulate()方法中,有一个重新生成hash的调用:

static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}

        对传入的值进行了一系列的左右移位 、异或操作,生成一个新值。

    3、sum()

public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

      返回累加的和,但在高并发情况下不是一个准确的数值,只是近似准确的计数值。(没有处理线程安全问题)只能适用于偏差允许的情况下。

    4、reset()

public void reset() {
    Cell[] as = cells; Cell a;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                a.value = 0L;
        }
    }
}

      重置计数器,只应该在明确没有并发的情况下调用,可以用来避免重新new一个LongAdder。

    5、sumThenReset()

public long sumThenReset() {
    Cell[] as = cells; Cell a;
    long sum = base;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null) {
                sum += a.value;
                a.value = 0L;
            }
        }
    }
    return sum;
}

      相当于sum()后再调用reset()。

  ③待添加


知识补充:Striped64类

  ①整体介绍

    Striped64是在java8中新增的用来支持高并发累加器的并发组件,它的设计思路是在竞争激烈的时候尽量分散竞争
    Striped64的内部有三个重要的成员变量:

/**
 * 存放Cell的hash表,大小为2的幂。
 */
transient volatile Cell[] cells;

/**
 * 基础值,在没有竞争时会更新这个值;在cells不可用时,也会尝试累加这个值。
 */
transient volatile long base;

/**
 * 自旋锁,通过CAS操作加锁,用于保护创建或者扩展cells。
 */
transient volatile int cellsBusy;

  ②具体内容

    此处结合上面的LongAdder来举例说明:

    1、cells

      cells是并发高性能的关键:
        ●AtomicInteger只有一个value,所有线程累加都要通过cas竞争value这一个变量,高并发下线程竞争非常严重。
        ●LongAdder则有两个值用于累加,一个是base,它的作用类似于AtomicInteger里面的value,在没有竞争的情况不会用到cells数组。当有了竞争后cells数组就上场了,第一次初始化长度为2,以后每次扩容都是变为原来的两倍,直到cells数组的长度大于等于当前服务器cpu的数量为止就不在扩容(超过cpu数量的话效率不会更好,还有可能增加cpu切换的情况);每个线程会通过线程对cells[threadLocalRandomProbe % cells.length]位置的Cell对象中的value做累加,这样相当于将线程绑定到了cells中的某个cell对象上。

    2、base

      ●在没有竞争的情况下,将累加值累加到base。
      ●在cells初始化的过程中,cells不可用,这时会尝试将值累加到base上。

    3、cellsBusy

      cellsBusy的作用是当要修改cells数组时加锁,防止多线程同时修改cells数组,0为无锁,1为加锁,加锁的状况有三种:
        ●cells数组初始化的时候。
        ●cells数组扩容的时候。
        ●如果cells数组中某个元素为null,给这个位置创建新的Cell对象的时候。

    4、注解@sun.misc.Contended

      此注解用于消除伪共享。

      伪共享

        cpu的缓存系统中是以缓存行(cache line)为单位存储的,缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节,cache line是cache和memory之间数据传输的最小单元。
        大多数现代cpu都包含了L1和L2cache。对于L1 cache,大多是write though的,L2 cache则是write back的,不会立即写回memory,这就会导致cache和memory的内容的不一致;另外,对于多处理器环境,由于cache是cpu私有的,不同cpu的cache的内容也存在不一致的问题,因此很多的计算架构,不论是ccNUMA还是SMP都实现了cache coherence的机制(即不同cpu的cache一致性机制)。

Write-through(直写模式):在数据更新时,同时写入缓存Cache和后端存储。此模式的优点是操作简单。缺点是因为数据修改需要同时写入存储,数据写入速度较慢。
Write-back(回写模式):在数据更新时只写入缓存Cache。只在数据被替换出缓存时,被修改的缓存数据才会被写到后端存储。此模式的优点是数据写入速度快,因为不需要写存储。缺点是一旦更新后的数据未被写入存储时出现系统掉电的情况,数据将无法找回。

        cache coherence的一种实现是通过cache-snooping协议,每个cpu通过对bus的snoop实现对其它cpu读写cache的监控:
          ●当cpu1要写cache时,其它cpu就会检查自己cache中对应的cache line。如果是dirty的,就write back到memory,并且会将cpu1的相关cache line刷新;如果不是dirty的,就废弃该cache line。
          ●当cpu1要读cache时,其它cpu就会将自己cache中对应的cache line中标记为dirty的部分write back到memory,并且会将cpu1的相关cache line刷新。
        所以,提高cpu的缓存命中率,减少cache和memory之间的数据传输,将会提高系统的性能。

        因此,在程序和二进制对象的内存分配中保持cache line一致就十分重要,如果不保证cache line对齐,出现多个cpu中并行运行的进程或者线程同时读写同一个cache line的情况的概率就会很大。这时cpu的cache和memory之间会反复出现write back和refresh情况,这种情形就叫做cache thrashing。

        为了有效的避免cache thrashing,通常有以下两种途径:
          ●对于heap的分配,很多系统在malloc(动态内存分配)调用中实现了强制的一致性。
          ●对于stack的分配,很多编译器提供了stack aligned的选项。
        当然,如果在编译器指定了stack aligned,程序的尺寸将会变大,会占用更多的内存。因此,这中间的取舍需要仔细考虑。

      为了解决这个问题在jdk1.6采用了long padding的方式,jdk1.7的某个版本后优化了long padding,然后在jdk1.8中加入了@sun.misc.Contended。

    除了LongAdder,Striped64在ConcurrentHashMap中也有使用,此处学习之后可以触类旁通。


知识补充:CAS

  ①整体介绍

    CAS全称Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

    CAS算法涉及到三个操作数:
      ●需要读写的内存值 V。
      ●进行比较的值 A。
      ●要写入的新值 B。
    当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

  ②代码学习

    上面提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:

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

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}

    根据定义我们可以看出各属性的作用:
      ●unsafe: 获取并操作内存的数据。
      ●valueOffset: 存储value在AtomicInteger中的偏移量。
      ●value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。

    接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:

// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

    根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

    后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

  ③问题与不足

    CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:
      ●ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
      JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
      ●循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
      ●只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
      Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

系列文章传送门:

JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值