我们先来看一手代码:
启动两个线程,每个线程中让静态变量count循环累加100次。
我们加上synchronized同步锁,再来看一下。
加了同步锁后,count自增的操作变成了原子性操作,所以最终输出结果一定是200,代码实现了线程安全。虽然synchronized确保了线程的安全,但是在有些情况下,这并不是最好的选择。
关键在于性能问题。
synchronized关键字会让没获得锁资源的线程进入BLOCKED(阻塞)状态,只有在争夺到锁资源的时候才转换成RUNNABLE(运行)状态。这其中涉及到操作系统中用户模式和内核模式之间的切换,代价比较高。
同时,尽管jdk对synchronized关键字进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能依然比较低,所以面对这种只对单个变量进行原子性的操作,最好使用jdk自带的“原子操作类”。
原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBoolean、AtomicInteger、AtomicXXX都是分别对应Boolean、Integer或其他类型的原子性操作。
现在我们采用AtomicInteger类试一下:
使用原子操作类之后,最终的输出结果同样是200,保证了线程安全。并且在某种情况下,该方案代码的性能会比synchronized更好。
首先,CAS的英文单词是Compare and Swap,即是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,需要替换的值B。
我们可以来看一个例子:
1.在内存地址V当中,存储着值为10的变量
2.此时线程1想要把变量的值增加1,对于线程1而言,它旧的预期值A=10,需要替换的最新值B=11。
3.在线程1要提交更新之前,另外一个线程2抢先一步,将内存地址V中的值更新成了11。
4.线程1开始提交更新的时候,按照CAS机制,首先进行A的值与内存地址V中的值进行比较,发现A不等于V中的实际值,于是提交失败。
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的值。在现在而言,线程1旧的预期值A=11,B=12.这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变该变量的值,所以线程1进行CAS机制,比较旧的预期值A与内存地址V中的值,发现相同,此时可以替换。
7.线程1进行替换,把地址V的值替换成B,也就是12.
在java中除了上面提到的Atomic操作类,以及Lock系列类的底层实现,甚至在jdk1.6以上,在synchronized转变为重量级锁之前,也会采用CAS机制。
CAS的优点自然是在并发问题不严重的时候性能比synchronized要快,缺点也有。
在并发量比较高的时候,如果许多线程都尝试去更新一个变量的值,却又一直比较失败,导致提交失败,产生自旋,循环往复,会对CPU造成很大的压力和开销。
CAS机制所确保的是一个变量的原子性操作,而不能保证整个代码块的原子性,比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized或者lock了。
这就是CAS最大的问题所在。下面说。
讲解了什么是CAS机制、CAS与synchronized的区别、它的优点和缺点之后。
下面我们来介绍两个问题:
我们使用idea查看一下AtomicInteger中常用的自增方法incrementAndGet
incrementAndGet调用的是unsafe的getAndAddInt方法并增加1
其中var1是当前对象,var2是内存地址V中的值,var6是旧的期望值A,var6+var4则是需要替换的值B。
可以看到,这一段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:
1.获取当前的值,该方法使用native实现,底层是用其他语言实现的。
2.当前值+var4,var4就是上一个方法传进来的1,计算出目标值B
3.进行CAS操作,如果成功交换则跳出循环,如果失败则重复以上步骤。
那我们怎么保证valueOffset也就是var2是正确的内存中最新的值的呢?很简单,用volatile关键字来保证线程间的可见性,也就保证了是最新的值。
可以看到我们的代码始终和unsafe这个类相关,那什么是unsafe呢?java语言不像C,C++语言一样可以直接访问底层操作系统,但是JVM为我们开了一个后门,这个后门就是unsafe,unsafe为我们提供了硬件级别的原子操作。
至于valueOffset变量则是通过unsafe的objectFieldOffset获得,所代表的就是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解成value变量的内存地址也就是V了。
我们前面说过,CAS机制中使用了三种基本操作数:内存地址V,旧的期望值A,新的需要替换的值B。
而unsafe的compareAndSwapXxx方法中的参数var2则就代表valueOffset(内存地址V),var6代表A,var6+var4代表B。
现在我们来说下什么是ABA问题。
2.此时有三个线程想要使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。
3.接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因某种原因阻塞住,没有做更新操作,此时线程3在线程1更新之后,获取了当前值B。
4.在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成A。
5.最后,线程2终于恢复了运行状态,由于阻塞之前已经获得到了”当前值A“,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量A的值更新为B。
我们假设一个取款机的例子。假如有一个遵循CAS机制的取款机。小肖有100元存款,需要提取50元。但由于取款机硬件出现了问题,导致取款操作同时提交了两遍,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。
理想情况下,应该一个线程更新成功,一个线程更新失败,小肖的存款只扣除一次,余额为50.
线程1首先执行成功,把余额100更新为50,同时线程2由于某种原因陷入了阻塞状态,这时候,小肖的妈妈汇款给了小肖50元。
线程2仍然是阻塞状态,线程3此时执行成功,把余额从50改成了100.
这时候,线程2恢复运行,由于之前阻塞的时候获得了”当前值“100,并且经过compare检测,此时存款也的确是100元,所以成功把变量值从100更新成50.
原本线程2应当提交失败,小肖的正确余额应该保持100元,结果由于ABA问题提交成功了。
这就是所谓的ABA问题,那么怎么解决呢?
我们仍然以刚才的例子来说明,假设地址V中存储着变量值A,当前版本号是01.线程1获取了当前值A和版本号01,想要更新为B,此时线程1陷入了阻塞状态。
这时候,内存地址V中的变量进行了多次改变,版本号提升到03,但是变量值仍然是A。
随后,线程1恢复运行,进行compare操作。首先经过比较,内存地址V中的值与当前值A相同,但是版本号不相同,所以这一次更新失败。
在Java中,AtomicStampedReference类就实现了用版本号做比较的CAS机制。