005 volatile不能保证原子性之i++操作

本文探讨了volatile关键字在多线程环境下对于变量i++操作的非原子性问题,导致结果不准确。解决方案包括使用synchronized和AtomicInteger,以及JDK8的LongAdder,它们提供了原子性保证。volatile仅保证可见性,不保证操作的原子性,因此在高并发场景下需要额外的同步措施。
摘要由CSDN通过智能技术生成

1、多线程对volatile修饰的变量做 count++ 操作,没有保证原子性

下面启动了1000个线程对volatile修饰的变量进行i++操作,我们可能会想每一个线程都+1,当1000个线程都执行完+1操作之后结果应该是1000,但是执行代码试试,结果可能不是1000。这说明volatile不是原子性的。


public class Counter {
	public static volatile int count = 0;

	public static void inc() {
		try {
			// sleep 1 毫秒让效果明显
			Thread.currentThread().sleep(1);
		} catch (Exception e) {
			e.printStackTrace();
		}
		count++;
	}

	public static void main(String[] args) {

		// 同时启动1000个线程,去进行i++计算,看看实际结果
		for (int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					Counter.inc();
				}
			}).start();
		}

		// 保证前面的线程都执行完
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}

		// 这里每次运行的值都有可能不同,可能不是1000
		System.out.println("运行结果:Counter.count=" + Counter.count);
	}
}

2、多线程对volatile修饰的变量做 count++ 操作,没有保证原子性。解决方案一

去掉volatile关键字,在 count++ 的方法上加 synchronized 关键字;或者不去掉volatile关键字,在 count++ 的方法上加 synchronized 关键字

public class Counter {
	public static int count = 0;

	public synchronized static void inc() {
		try {
			// sleep 1 毫秒让效果明显
			Thread.currentThread().sleep(1);
		} catch (Exception e) {
			e.printStackTrace();
		}
		count++;
	}

	public static void main(String[] args) {

		// 同时启动1000个线程,去进行i++计算,看看实际结果
		for (int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					Counter.inc();
				}
			}).start();
		}

		// 保证前面的线程都执行完
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}

		// 这里每次运行的值都相同,一定是1000
		System.out.println("运行结果:Counter.count=" + Counter.count);
	}
}

synchronized 是重量级锁,开销很大。

3、多线程对volatile修饰的变量做 count++ 操作,没有保证原子性。解决方案二

使用AtomicInteger里面的getAndIncrement()方法


import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
	public static AtomicInteger count = new AtomicInteger(0);

	public static void inc() {
		try {
			// sleep 1 毫秒让效果明显
			Thread.currentThread().sleep(1);
		} catch (Exception e) {
			e.printStackTrace();
		}
		count.getAndIncrement();
	}

	public static void main(String[] args) {

		// 同时启动1000个线程,去进行i++计算,看看实际结果
		for (int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					Counter.inc();
				}
			}).start();
		}

		// 保证前面的线程都执行完
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}

		// 这里每次运行的值都相同,一定是1000
		System.out.println("运行结果:Counter.count=" + Counter.count);
	}
}

4、多线程对volatile修饰的变量做 count++ 操作,没有保证原子性。假如你使用的是JDK8 ,可以考虑解决方案三,LongAdder。

LongAdder,(高竞争环境下(比如1000个线程竞争的情况),LongAdderAtomicLong 的性能高,至于为什么高呢?去看看源码看看百度)


import java.util.concurrent.atomic.LongAdder;

public class Counter {
	public static LongAdder count = new LongAdder();

	public static void inc() {
		try {
			// sleep 1 毫秒让效果明显
			Thread.currentThread().sleep(1);
		} catch (Exception e) {
			e.printStackTrace();
		}
		count.increment();
	}

	public static void main(String[] args) {

		// 同时启动1000个线程,去进行i++计算,看看实际结果
		for (int i = 0; i < 1000; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					Counter.inc();
				}
			}).start();
		}

		// 保证前面的线程都执行完
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}

		// 这里每次运行的值都相同,一定是1000
		System.out.println("运行结果:Counter.count=" + Counter.count);
	}
}

5、为什么volatile修饰的变量做count++操作不具有原子性

例如你让一个volatile的integer自增(i++),其实要分成3步:
1)读取volatile变量值到local(线程本地内存);
2)(在线程的本地内存)增加变量的值;
3)把local的值写回(写回到主内存),让其它的线程可见。这3步的jvm指令为:

mov    0xc(%r10),%r8d ; Load
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier

最后一步是内存屏障。从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失(???)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值