1.原子操作
原子操作可以是一个步骤,也可以是多个步骤操作,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视作一个整体,资源在该操作中保持一致,这是原子性的核心特征。
下面我们先执行一个例子
public class Counter {
volatile int i = 0;在这里插入代码片
public void add() {
i++;
}
}
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
final Counter ct = new Counter();
for (int i = 0; i < 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
ct.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(ct.i);
}
}
预期结果是:
done…
done…
done…
done…
done…
done…
60000
执行结果是:
第一次
done…
done…
done…
done…
done…
done…
31558
第二次:
done…
done…
done…
done…
done…
done…
27311
其结果不仅与预期结果不一致,每次运行的结果也不一致。
下面我们就来看一下原因:
现将counter反编译 执行javap -v -p Counter.class
会出现
多线程在jvm的执行流程可参考我上一篇写的https://blog.csdn.net/qq_24045275/article/details/104647714
写的知道其.class在jvm的操作流程图所示:
其中getfield获取字段值 iconst_1 符号加1 iadd操作加号 从临时操作树栈占顶取值,再将结果值放入栈 从操作树栈放入堆内存。如果多个线程其i值可能每个线程取到的值不会是最新的值,所以就导致了其不会得到预期的结果。
原子操作解决方案:
1.cas(Compare and Swap)
比较和交换。属于硬件同步源语,处理器提供了基本的原子性保证。
cas 操作需要2个数值,一个旧值A(期望操作前的的值)和一个新值B,在操作期间先对旧值进行比较,若没有发生变化,才能交换新值,发生了变化则不交换其无限循环操作。Java 中的sun.misc.Unsafe类,提供了compareAndSwapint(0和compareAndSwapLong等几个方法实现cas这就是所谓的原子性保证线程安全。
如上图之java通过unsafe不可能直接对内存地址对值修改 知道每一个对象的引用,对象里面标记通过偏移量 (而内存条从硬件角度保证同一时刻只能一个线程)
如上面的图进行判断 cas操作不正确无线循环重新加载(自旋)
j.u.c的原子包装操作封装类如下:
用原子解决开头实例代码如下:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
//i字段的偏移量
private static long valueOffset;
static {
//unsafe = Unsafe.getUnsafe();
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
/*获取i字段的偏移量*/
Field fieldi = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(fieldi);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
//i++;
for (;;){
int current = unsafe.getIntVolatile(this, valueOffset);
if (unsafe.compareAndSwapInt(this, valueOffset, current, current+1))
break;
}
}
}
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
final CounterUnsafe ct = new CounterUnsafe();
for (int i = 0; i < 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
ct.add();
}
System.out.println("done...");
}
}).start();
}
Thread.sleep(6000L);
System.out.println(ct.i);
}
}
其结果:
其中除了原子操作也可以用锁如:synchronized和lock
其代码如:
public class CounterLock {
volatile int i = 0;
Lock lock = new ReentrantLock();
public void add() {
lock.lock();
i++;
lock.unlock();
}
}
public class CounterSync {
volatile int i = 0;
public synchronized void add() {
i++;
}
}
3 、automic和
锁的区别:
如下图一个4核的cpu多个线程访问:
锁机制
cas的机制
从图中可以看出
sysinorized互斥锁都需要时间损耗, 更节约cpu性能,同一时刻只能一个线程执行,只有一个cpu执行,其他线程阻塞, cpu节省性能
atomic (cas)(对硬件更消耗)cpu跑满了节省消耗时间,用空间换取时间。
4、cas的三个问题
1、循环+cas,自旋的实现让所有的线程都处于高频运行,争抢cpu执行时间的状态。如果操作长时间不成功,会带来很大的cpu资源消耗。
2、仅针对单个变量的操作,不能用于多个变量来实现原子操作。
3、ABA问题。
其中aba问题如下图所示:
!
如图所示由个线程同时访问同一个内容i的值并且都是通过cas操作,其中假如线程1先访问成功并改变i的值为1,又将值改为0,而这时线程2访问cas(0,1)成功且改变为1结果值并没有影响变化,但是其实在这过程中线程1中一个版本号变了。就如一个企业高管使用了公共的钱,后面有补上去了,虽然钱没变化,但这其中钱不是原来的钱了。
下面我用一个图表示其中变化:
(1)
(2)
(3)
从上面图中知道最后的结果是堆栈中最后只剩下b值了。
图一线程1先将a和b存入堆栈中
图2线程2将a和b取出在讲 c ,d,a 入栈。
图三,就是线程1在对a去堆栈中的a比较,将栈顶指向游离b由于b的next为空,这样就导致了堆栈中就剩下一个b其他的a,d,c丢失了。