并发编程学习(十二):字段更新器、原子累加器

1、字段更新器

字段更新器,主要是用来更新自定义类的字段。Java 提供以下三种字段更新器:

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

        注意的是:字段更新器要操作(原子操作)哪个字段,哪个字段必须被 volatile 修饰,否则会出现异常。

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
package com.example.test.java.juc;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {
    private volatile int field;

    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater =
                AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdaterTest.class, "field");

        AtomicIntegerFieldUpdaterTest test5 = new AtomicIntegerFieldUpdaterTest();

        fieldUpdater.compareAndSet(test5, 0, 10);
        // 修改成功 field = 10
        System.out.println(test5.field);
        // 修改成功 field = 20
        fieldUpdater.compareAndSet(test5, 10, 20);
        System.out.println(test5.field);
        // 修改失败 field = 20
        fieldUpdater.compareAndSet(test5, 10, 30);
        System.out.println(test5.field);
    }
}

运行结果:

10
20
20

2、原子累加器

         原子累加器主要解决多线程情况下i++线程不安全的问题。

        jdk1.8后出现了DoubleAdder、DoubleAccumulator、 LongAdder、LongAccumulator,专门用于做累加操作的。

2.1、累加器性能比较

package com.example.test.java.juc;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class LongAdderTest {

    /**
     参数1:提供累加对象
     参数2: 操作方法

     supplier 提供者 无中生有 ()->结果
     consumer 消费者 一个参数没结果 (参数)->void,
     */
    private static  void demo(Supplier adderSupplier, Consumer action) {

        T adder = adderSupplier.get();
        // 纳秒
        long start = System.nanoTime();
        List ts = new ArrayList<>();
        // 40 个线程,对入参变量 每人累加 50 万 : 40*50万 = 2000万(原子操作的结果)
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 纳秒
        long end = System.nanoTime();
        // 从 Java SE 7 的版本开始,程序中的数字可以使用下划线来进行分割(_)以便于为程序提供更好的可读性。
        // 你可以对一个比较长的数字,使用下划线来进行分隔,以便于你不会数错 0
        System.out.println(adder + " cost:" + (end - start)/1000_000 + "ms");
    }

    /**
     * @param args
     *
     *      LongAdder性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]…
     *      最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
     *
     *
     */
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            demo(() -> new LongAdder(), adder -> adder.increment());
        }
        for (int i = 0; i < 5; i++) {
            demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
        }
    }

}

运行结果:

20000000 cost:105ms
20000000 cost:73ms
20000000 cost:79ms
20000000 cost:83ms
20000000 cost:94ms
20000000 cost:752ms
20000000 cost:735ms
20000000 cost:734ms
20000000 cost:748ms
20000000 cost:756ms

2.2、LongAdder源码

 LongAdder类有几个关键域:

// 累加单元数据,惰性初始化 。Cell进行初始化时,默认容量为2

transient volatile Cell[] cells;  

// 基础值,如果没有竞争,则用cas累加这个域,

transient vloatile long base;

// 在cells创建或扩容时,置为1,表示cas加锁。

transient volatile int cellsBusy;

如果对个线程同时对  LongAdder进行累加,cell会扩容,最大扩容到cpu的核数。每个线程对自己拿到的槽位上的数据进行累加。如果没有线程竞争,只是base进行累加,cell不会进行初始化。

 transient关键字:

 (1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法被访问。
(2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
(3)一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

2.2.1、cas加锁:代码示例

package com.example.test.thread.cas;

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * cas锁示例,自己代码中不建议这样写。
 */
@Slf4j(topic = "com.test.LockCas")
public class LockCas {

    // 0-没加锁,1-已加锁
    private AtomicInteger state = new AtomicInteger(0);

    // 加锁
    public void lock(){
        while (true) {

            if (state.compareAndSet(0,1)) {
                break;
            }
        }
    }

    // 解锁
    public void unlock() {
        log.debug("unkock...");
        state.set(0);
    }

    public static void main(String[] args) {
        LockCas lock = new LockCas();

        new Thread(() -> {
            log.debug("begin...");

            // 加锁
            lock.lock();
            try {
                log.debug("lock...");
                // 执行代码
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 代码执行完成后,解锁
                lock.unlock();
            }
        },"线程1").start();

        new Thread(() -> {
            log.debug("begin...");

            // 加锁
            lock.lock();
            try {
                log.debug("lock...");
                // 执行代码
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 代码执行完成后,解锁
                lock.unlock();
            }
        },"线程2").start();
    }
}

 运行结果:

10:35:32.273 [线程2] DEBUG com.test.LockCas - begin...
10:35:32.273 [线程1] DEBUG com.test.LockCas - begin...
10:35:32.281 [线程2] DEBUG com.test.LockCas - lock...
10:35:33.283 [线程2] DEBUG com.test.LockCas - unkock...
10:35:33.283 [线程1] DEBUG com.test.LockCas - lock...
10:35:34.284 [线程1] DEBUG com.test.LockCas - unkock...

2.2.2原理之伪共享

Cell :累加单元 。

源码如下:

// 该注解的作用:防止缓存行的伪共享。
@sun.misc.Contended 
static final class Cell {
	volatile long value;
	Cell(long x) { value = x; }
    // 最重要的方法:用cas方式进行累加,prev 表示旧值,next表示新值。
	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);
		}
	}
}

以上原理说明得从缓存说起:

1、缓存与内存的速度比较:

                                                下图为cpu的内存结构

并发编程学习(十二):字段更新器、原子累加器_第1张图片

从cpu到下列位置大约需要的时间周期
寄存器 1 cyle(4GHz的cpu约为0.25ns)
L1 : 一级缓存 3~4 cyle
L2 : 二级缓存 10~20 cyle
L3:三级缓存 40~45 cyle
内存 120~240 cyle

 因为cpu与内存的速度差异很大,需要靠预读数据到缓存来提升效率。

缓存以缓存行为单位,每个缓存行对应一块内存,一般是64bytes(8个long).

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心(cpu)的缓存行中,cpu要保证数据的一致性,如果某个cpu核心更改了数据,其他cpu核心对应的真个缓存行必须失效。

并发编程学习(十二):字段更新器、原子累加器_第2张图片

 因为Cell是数组形式,在内存中是连续存储的,一个Cell为24字节(16字节的对象头和8字节long类型的value),因此魂村行可以存下2个Cell对象。这样问题就来了:

core-0 要修改Cell[0]

core-1 要修改Cell[1]

无论谁修改成功,都会导致对方core的缓存行失效,比如core-0中Cell[0]=6000,Cell[1]=8000, 要累加Cell[0]=6001,Cell[1]=8000,这时会让core-1的缓存行失效。

@sun.misc.Contended用来解决这个问题,它的原理是在使用该注解的对象或字段的前后各增加128字节大小的padding,从而让cpu将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。如下图:

并发编程学习(十二):字段更新器、原子累加器_第3张图片

你可能感兴趣的:(#,线程,并发,Java基础,学习)