博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
多线程自增运算的原子性与可见性分析
阅读量:3485 次
发布时间:2019-05-19

本文共 3359 字,大约阅读时间需要 11 分钟。

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

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
set = new HashSet
(); 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不会加锁,因此会具有更好的并发性能。

你可能感兴趣的文章
BFC(Block Formatting Context)
查看>>
什么是作用域,什么是闭包,什么是作用域链
查看>>
惰性求值,面向对象
查看>>
lodash源码分析之baseSlice()函数
查看>>
数据结构之列表
查看>>
发布/订阅模式 vs 观察者模式
查看>>
es5中的arguments对象
查看>>
git本地仓库和远程仓库关联,分支重命名
查看>>
js对象的深拷贝,你真的觉得很简单吗?
查看>>
你真的了解map方法吗?手动实现数组map方法。
查看>>
带你手动实现call方法,让你收获满满
查看>>
前端知识体系
查看>>
查找入职员工时间排名倒数第三的员工所有信息
查看>>
使用join查询方式找出没有分类的电影id以及名称
查看>>
Qt教程(2) : Qt元对象系统
查看>>
驱动开发误用指针错误:Unable to handle kernel NULL pointer dereference at virtual address
查看>>
Linux部署DocSystem知识/文件管理系统
查看>>
Centos7开机自启动脚本无法使用备用方案
查看>>
jvm虚拟机内存详解
查看>>
线程的创建方式
查看>>