案例分析
为什么多个线程同时对变量i++会出现非预期结果呢?
/**
如下代码 6个线程同时对变量i做i++的操作,每个线程自增1w次,
结果时随机的且值基本小于6w
*/
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
// final CounterUnsafe ct = new CounterUnsafe();
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); //输出的结果是??
}
static class Counter {
volatile int i = 0;
public void add() {
i++; //原子性问题,为什么??
}
}
}
问题分析(i++操作时非原子性的)
2个线程同时去对i++, 由于i++这个操作时分部进行的 导致t1 ,t2拿到 i=0 然后去做对应的自增及写入操作。
thread1 getFiledI =0 ->压入操作数栈 i=0 -> 执行i++=1 ->putFiled 1 到堆内存
thread2 getFiledI =0 ->压入操作数栈 i=0 -> 执行i++=1 -> putFIled 1到堆内存
怎么解决这个问题? 保证这种情况下同时只能被一个线程访问,等t1 i++完成后 t2再访问。
> 加锁 ->synchronized , ReentrantLock
对于i++来说加锁后岂不是就是同步的了,那用多线程还有什么意义呢
CAS(Compare and swap)
原子操作:
CAS (Compare and swap)
public class CounterUnsafe {
volatile int i = 0; //cas 硬件 内存地址 --Long 232323523454235
private static Unsafe unsafe =null;//Unsafe类时final修饰的
private static long valueOffSet;// 初始话的时用来存储 变量i对应的内存地址
static {
try {
// unsafe = Unsafe.getUnsafe(); **该方法禁止程序直接调用会抛出异常 , 对于属性静止调用的我们可以通过发射机制来获取值**
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
Field field1 = CounterUnsafe.class.getDeclaredField("i");
valueOffSet = unsafe.objectFieldOffset(field1);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
for(;;) {
int current = unsafe.getIntVolatile(this, valueOffSet); //**性能 : 分析 解决原子性问题 方式有: 阻塞 或 自旋(cas) 分别损失的是谁? 自旋属于计算损失cpu 阻塞hunt住线程 损失内存(某种意义上也会损失cpu)**
//**系统中 内存 和 cpu 哪个更容易出现异常或 100% ?cpu更容易出事 内存出问题大概率是程序的问题 ---更多情况下我们会选择损耗内存**
if (unsafe.compareAndSwapInt(this, valueOffSet, current, current + 1)){ //要么成功,要么失败
break;
};
}
}
}
JUC包内的原子操作封装类
内部实现即为CAS -用Unsafe类
AtomicBoolean: 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong: 原子更新长整型
AtomicIntergerArray:原子更新整形数组里的元素
AtomicLongArray: 原子更新长整型数组里的元素
AtomicReferenceArray: 原子更新引用类型数组里的元素
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器
AtomicLongFieldUpdater: 原子更新长整型字段的更新器
AtomicReferenceFieldUpdater: 原子更新引用类型里的字段
AtomicReference:原子更新引用类型
AtomicStampedReference: 原子更新带有版本号的引用类型
AtomicMarkableReference: 原子更新带有标记为的引用类型
public class CounterAtomic {
//volatile int i = 0;
AtomicInteger i = new AtomicInteger(0); //封装了CAS机制.
public void add() {
i.incrementAndGet();
}
}
CAS的三个问题 -为什么cas不受待见
1.循环+CAS,自旋的实现让所有线程多处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗
2. 仅正对单个变量的操作,不能用于多个变量来实现原子操作
3. ABA问题,如下:
ABA 问题是由于仅仅比较值的时候可能在并发情况下与预期不一致的结果,解决方案:添加版本号
代码可以参考: 中aba–此处需要补充