并发编程 原子操作
原子操作
前面大概讲了下 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 三大问题:
- ABA问题:由于是先获取的预期值,替换的时候再获取预期值进行比较,所以中间值就存在被更改过,但又被改回来的可能,A->B->A,这样还是等于预期值,但其实此A非彼A。解决:加版本号。每次修改记录唯一但版本号:1A->2B->3A
- 自旋循环时间开销大
- 只能保证一个共享变量的原子操作
- 锁
顾名思义,锁住一段代码或一个方法,不管哪个线程,想执行锁住但代码,就得要有钥匙,而钥匙只有一把,同时只能在一个线程手上,就能达到线程间互斥执行,原子话这段代码(方法)。
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'

浙公网安备 33010602011771号