多线程自增运算的原子性与可见性分析

很多程序的复杂性都是由于并发引起的,如果在并发情况下,如果对象是无状态那可以很好的运行,无论是在单线程还是多线程环境下都没问题,但是很多时候对象的状态是需要上下文维护的,因此在并发情况下就很容易导致不一致性的情况。我们先看下面一个例子:

class File {
    private int num;

    public void incr() throws InterruptedException, BrokenBarrierException {
        this.num ++;
    }

    public int getNum() {
        return num;
    }

}

测试代码:

for (int j = 0;j < 100;j ++) {
            // 测试100次
            final ExecutorService executorService = Executors.newCachedThreadPool();
            Set<Integer> set = new HashSet<Integer>();
            final File file = new File();
            // 启动1000个线程
            for (int i = 0;i < 1000;i ++) {
                executorService.execute(new Runnable () {

                    @Override
                    public void run() {
                        try {
                            file.incr();
                        } catch (Exception ex) {

                        }
                    }

                });
            }
            executorService.shutdown();
            while (true) {
                // 等待线程任务执行完毕
                if (executorService.isTerminated()) {
                    System.out.println(file.getNum());
                    break;
                }
            }
        }

就是模拟多个线程去对num进行自增。线程安全性的定义是怎样这个没一个标准的定义,但是最广泛的说法就是与单线程执行保持一致性,包括数据库的最高事务隔离级别也是可串行化级别。如果这个操作是单线程执行,最后的结果必然是1000。运行100次也是同样的结果。但是在并发的情况下,这里i ++操作是非原子操作,这个操作实际上是分三步,读出i值,给i加1,赋值给i,然后将i写会主内存。这里需要介绍的内存模型,如图:
这里写图片描述
线程读取到数据进行操作的时候是在线程私有内存完成,因此可能出现这种情况:
这里写图片描述
两个线程同时并发读取对象i,然后在不同的栈内存内修改完值写会主内存会导致修改丢失。串行化的结果应该是i被增加了2,这就是非线程安全。
上面代码运行情况在高并发情况下很容易出现 num < 1000的情况。
归根到底是因为i ++并非原子操作。这个在jdk可以采用AtomicInteger类完成原子递增。这样:

class File {

    private AtomicInteger num = new AtomicInteger(0);
    public void incr() throws InterruptedException, BrokenBarrierException {
        num.addAndGet(1);
    }

    public int getNum() {
        return num.get();
    }

}

这样自增相当于是一个原始操作,可以达到类似于以下效果:

class File {
    private Integer num = new Integer(0);
    private Object lock = new Object();
    public void incr() throws InterruptedException, BrokenBarrierException {

        synchronized (this.lock) {
            // 其他线程阻塞到之前
            this.num = this.num + 1;
        }

    }

    public int getNum() {
        return num;
    }
}

保证自增为原子操作。值得注意的是我在测试的时候发现一个问题,如果是对Integer对象加锁:

synchronized (this.num) {
            this.num = this.num + 1;
        }

则不能保证其他线程阻塞在this.num = this.num + 1;之前。无法完成同步。.
当然除了采用Java内置锁还可以采用并发包提供的lock。例如:

private Integer num = new Integer(0);
private ReentrantLock lock = new ReentrantLock();
public void incr() throws InterruptedException, BrokenBarrierException {
        try {
            this.lock.lock();
            this.num ++;
        } catch (Exception ex) {}
        finally {
            this.lock.unlock();
        }

    }

一样能够保证自增的原子性。如果不采用锁机制:

private volatile Integer num = new Integer(0);

这样:

public void incr() throws InterruptedException, BrokenBarrierException {
        this.num ++;
    }

能够保证线程安全吗?线程安全包含两个要素,操作的原子性和可见性,volatile只能保证可见性而不能保证操作的原子性,因为不会采用锁机制。它的原理是如果多个线程栈内存缓存了某个值,如果其中一个线程修改了这个值,会立刻更新到共享内存,也会通知其他线程该变量指向的值是失效的,其他线程将不会使用该线程缓存的值而是强制去刷共享内存的值。
线程 : 读取i == 1 - >修改 i = 2 - > 写回内存(立即刷新共享内存)
volatile并不阻碍另一个线程去获取i的值。因此还是可能读到修改前的数据,无法保证线程安全性。
加锁机制:
线程1:读取i == 1 - >修改 i = 2 - > 写回内存
线程2:阻塞 - ——————————-> 读取i = 2 ..写回内存3
线程3 : 阻塞 - ————————————————————>读取i == 3
因此上述代码不是线程安全的。
总而言之volatile只能保证更新的数据能够立刻更新到共享内存,定义变量为volatile尽量不要让更新后的值依赖之前的值。当然由于 volatile不会加锁,因此会具有更好的并发性能。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值