并发编程 原子操作

原子操作

  前面大概讲了下 cpu、进程、线程,知道了多线程在通常情况下可以加快程序的运行时间,来达到缩短程序相应时间的目的,但俗话说的好啊,鸡多不下蛋,人多瞎捣乱,线程一多就容易出乱子。看下面多例子:

public class ThreadErrorTest {

    public static Long count = 0L;

    public static void main(String[] args) {
        concurrent();
    }

    /** 并行计数 */
    public static void concurrent() {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                add();
            }
        });
        t1.start();
        t2.start();
    }

    /** 累加 */
    public static void add(){
        System.out.println(++count);
    }

  两个线程,对同一个count进行累加计算,分别累加 1000次,预期是最终结果为2000,但实际如何呢?

  第一次结果:

。。。。。。
1994
1995
1996
1997
Disconnected from the target VM, address: '127.0.0.1:52390', transport: 'socket'

  第二次结果:

。。。。。。
1996
1997
1998
1999
Disconnected from the target VM, address: '127.0.0.1:52458', transport: 'socket'

  发现每次执行但结果不确定,不符合预期,这是因为 ++count 的jvm 操作指令并不是一步,实际可以分为以下3步:

    • temp = memery[count]; 从内存中读取 count 的值
    • temp = temp + 1; 将内存中读取到到值进行累加操作
    • memery[count] = temp; 将累加后到结果保存进内存

  由于两个线程是并发进行到,那么谁也管不了谁,各干各的,就会相互捣乱了:A 和 B 线程同时从内存中读取了 count 值,读取到还是一样到值,都进行了累加操作,然后分别写回内存,写进去都值还是一样都,你看,总共进行了 2 次累加操作,count 值实际却只累加了一次。

  像 ++count 这种 三步操作,且这三步操作随时可以被其他线程插入中断,就是 非原子操作,非原子操作在多线程情况下,且有线程共享变量时(count),是不安全的,双方不能进行及时沟通,互相不知道工作进度,就导致了冲突。

 

原子操作

  ++count 在翻译成 jvm 指令时被分割成了三步,所以为非原子操作,那么那单独的三步就是原子操作,那些无法在被中断的一个或一系列操作,就是原子操作 

实现原子操作

  既然 ++count 分为了三条指令,是非原子操作,那么我们一定要用呢,我就是要++呢,那就强行原子!!

cpu 强行原子

  • 锁总线

  之前提到了线程需要从内存中读取变量count 的值,这里的内存就是 main 函数主线程里的内存(主),写进自己的内存(工作内存)(JMM模型),而cpu则是通过前端总线(FSB)访问内存的,总线相当于桥梁,供cpu与内存交换数据。

那么当cpu1 操作主内存中的数据是,把总线锁了,其他cpu核就要等锁释放,获取锁后在进行下面的操作,等cpu1 执行完了,在释放锁,这样就串行化了,准没问题,但是,直接把总线锁住,所有其他与之无关等操作都卡住了,不至于不至于。

  • 锁缓存行

  前面说数据在内存中,具体是存在缓存行中,缓存行是最小都缓存单位,如果数据小,那么一个缓存就能存贮好多变量,如果数据大,那么一条缓存行还不够,要俩缓存行才能存下一个变量。

  所以可以直接锁缓存行,即使缓存行中有count 以外大变量,也被误伤了,但总比锁总线强,但万一遇上变量跨缓存行了,那么就要锁总线了。

  这里提到了误伤的情况,那么就能避免误伤,即使count 数据小,但我可以人为扩充到一个缓存行到大小:

import com.google.code.yanf4j.util.LinkedTransferQueue


private static final class PaddedAtomicReference<T> extends AtomicReference<T> {
        private static final long serialVersionUID = 4684288940772921317L;
        Object p0;
        Object p1;
        Object p2;
        Object p3;
        Object p4;
        Object p5;
        Object p6;
        Object p7;
        Object p8;
        Object p9;
        Object pa;
        Object pb;
        Object pc;
        Object pd;
        Object pe;

        PaddedAtomicReference(T r) {
            super(r);
        }
    }

  英特尔酷睿 i7 ,酷睿, Atom 和 NetBurst , Core Solo 和Pentium 处理器的 L1 , L2 或 L3 缓存的高速缓存行是 64 个字节宽,一个 obj 4字节,还要 15个obj补足64字节。

java 强行原子

  • cas 操作(compare and swap):比较并替换

  cas 例子:

public class CASTest {

    private static AtomicInteger cas_count    = new AtomicInteger(0);

    private static Integer       normal_count = 0;

    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            threadList.add(new Thread(() -> {
                cas_add();
                normal_add();
            }));
        }
        for (Thread thread : threadList) {
            thread.start();
        }
        for (Thread thread : threadList) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.printf("normal result: %s%n", normal_count);
        System.out.printf("cas result: %s%n", cas_count.get());
    }

    /** cas 递增 */
    private static void cas_add() {
        cas_count.getAndIncrement();
    }

    /** 普通递增 */
    private static void normal_add() {
        normal_count++;
    }
}

  结果:

Connected to the target VM, address: '127.0.0.1:50550', transport: 'socket'
normal result: 199
cas result: 200
Disconnected from the target VM, address: '127.0.0.1:50550', transport: 'socket'

  多次执行,普通自增由于非原子操作,可能出现错误,但 atomic 类但cas 操作避免了该情况的发生。看下 AtomicInteger.getAndIncrement() 方法

// 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); }
}

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
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;
} 

  objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量,getIntVolatile方法用于在对象指定偏移地址处volatile读取一个int值。所以cas 原理为:先从内存地址中获取预期值,替换过程中,会先将预期起值和内存中的值进行比较,一致才会把新值替换进内存,不一致则替换失败,再重新获取预期值,比较替换,知道一致且替换成功。

   cas 三大问题:

    1. ABA问题:由于是先获取的预期值,替换的时候再获取预期值进行比较,所以中间值就存在被更改过,但又被改回来的可能,A->B->A,这样还是等于预期值,但其实此A非彼A。解决:加版本号。每次修改记录唯一但版本号:1A->2B->3A
    2. 自旋循环时间开销大
    3. 只能保证一个共享变量的原子操作

  顾名思义,锁住一段代码或一个方法,不管哪个线程,想执行锁住但代码,就得要有钥匙,而钥匙只有一把,同时只能在一个线程手上,就能达到线程间互斥执行,原子话这段代码(方法)。

  synchronized 举例:

package org.monkeyjesus.learn.thread;

/**
 * ThreadErrorTest
 *
 * @author monkeyjesus(cwb1204688)
 * @version Id: ThreadErrorTest.java, v 0.1 2022-01-08 22:36 monkeyjesus Exp $$
 */
public class SynchronizedTest {

    public static Long normal_count = 0L;
    public static Long sync_count   = 0L;

    public static void main(String[] args) {
        concurrent();
    }

    /** 并行计数 */
    public static void concurrent() {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                normal_add();
                sync_add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                normal_add();
                sync_add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("normal result = " + normal_count);
        System.out.println("sync result = " + sync_count);
    }

    /** 普通累加 */
    private static synchronized void sync_add() {
        ++sync_count;
    }

    /** 普通累加 */
    private static void normal_add() {
        ++normal_count;
    }
}

  结果:

Connected to the target VM, address: '127.0.0.1:51622', transport: 'socket'
normal result = 1854
sync result = 2000
Disconnected from the target VM, address: '127.0.0.1:51622', transport: 'socket'

  

posted @ 2022-01-10 00:09  MonkeyJesus  阅读(90)  评论(0)    收藏  举报