你知道的越多,你不知道的也越多!
聪明的大家,都知道在高并发的场景下,**i++**这种操作是线程不安全的。
原因大家都懂吧?啊?大家说啥?好吧,你们说的都对,的确只有我不懂。呜呜呜~ 来,你们告诉我吧。我现在就记在我的小本本上。
- 1.i++ 为什么不安全
- 2. AtomicInteger如何保证
- 2.1 volatile
- 2.2 Unsafe
- 3.更多的思考
i++ 看似一句代码,实际底层是3条指令:取值+加1操作+最新的值回写。
假如2个线程,同时对一个i=0变量进行i++操作。就问下有没有如下的可能性发生?两个线程都进行i++操作但是,最终结果还是1?
其实是有的! 如果不信,可以使用两个Thread对同一个i 进行加1操作,会发现最终的结果远小于预期结果。代码就不提供了,简单且非本文核心。
根本原因: i++非原子操作,既然非原子操作即当前线程随时有着挂起的可能性,相当于操作暂停。等到再次恢复过来继续执行,可能结果就不是预期那样了。
2. AtomicInteger如何保证总结: volatile 的value内部变量 + Unsafe的CAS机制
2.1 volatile作用就是保证可见性 ,禁止重排令。这里肯定用到的是可见性啦。
稍微回顾下知识点吧。耳熟能详的主内存和工作内存。一个变量,如果子线程需要操作,会从主内存里拷贝一份到自己的内存里来。如果变量加了volatile修饰,也就意味着,主内存会有个嗅探,当发现其它线程修改了该变量,则会通知其它子线程,告诉它们各自存储的这个变量失效了,需要从主内存重新获取最新的值。
这样能保证各个线程拥有最新的值,保证了可见性。
来看下,AtomicInteger的内部成员变量,不多也就3个。
你会发现有个volatile修饰的变量。
其实 AtomicInteger 不管是加操作,还是取值操作,最终都是和这个value相关的。
这个类主要是jdk底层提供的类,依据内存地址偏移量来获取对应的值,底层很多方法都是native方法,由c++编写。听着都yyds~
来看下AtomicInteger常用的getAndAdd方法,这个和getAndIncrement区别就是递增量不是固定值:1 ,而是入参传进来的。
看完两个疑问点:
- unsafe 什么时候构造出来的?怎么构造出来的?
- valueOffset 是个啥?什么时候初始化的?
valueOffset应该是value这个内部变量基于AtomicInteger的内存地址偏移量。已知AtomicInteger地址,依据偏移量可以快速获取value的偏移量,进而快速获取值。
好了,继续看getAndAddInt方法吧。
看到do{}while() 这种模式的,应该很熟悉了,CAS的常规思路。
Unsafe的compareAndSwapInt()方法是可以保证原子性的。
其实拆解出来就是if(条件成立){则进行只修改} 这种判断-操作 在Unsafe里是线程安全的! 日常开发中,一般采用“一锁二判三更新”的思路保证原子性。
其实,我们在ConcurrentHashMap的put方法源码里也看到了do{}while()的CAS:
-
CAS 和一些排他锁区别?
排他锁当线程持有锁之后,其它线程是处于一种Blocking状态。当锁被释放的时候,那些等待锁的线程又会被切换成Running的状态。所以这种情况有个线程上下文切换的过程。
而CAS整个过程,线程状态一直都是Running,不存在上下文切换。也正因为这样,CAS认为有些时候,等待锁的开销小于上下文切换所带来的开销。 -
CAS适用的场景?
适用于并发量不是很大的场景,且每个线程块处理耗时不是很长时间。
想象一下,假如几百万的并发量,所有的线程都在重试这个锁,其它事都不干,那其它请求谁来处理?



