说起Java中的多线程,就不得不说volatile关键词
volatile关键词执行修饰变量和实例变量,不能修饰方法参数,局部变量和实例常量。
volatile是Java提供的一种轻量级的同步机制,在并发编程中,它是担任了重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级。
想要了解votatile的来龙去脉,就必须了解CPU缓存模型和java内存模型,本文介绍前者。
众所周知,计算机所有的运算操作都是在cpu寄存器来完成的,运算操作无非就是数据的读取和写入操作,但CPU能访问的数据只能是计算机内存(RAM),虽然相比于普通硬盘和固态硬盘,RAM的速度远超这两者,但比起CPU的处理速度来说,这之间的差距可达数千倍。
举个简单的例子,随着光纤的普遍接入,网络速度大大提高,可不管下载速度如何提高,它的上限都受制于硬盘的写入速度,就算下载速度超过了10GB/s,可固态硬盘写入速度撑死也只能达到它的十分之一。
CPU也一样,由于运算速度和内存访问速度上面的不对等,导致CPU资源会受到极大的限制,于是就有在CPU主存和内存之间增加了缓存的设计。
看图中I9 9900K的参数,现在的缓存可以增加到3级。
在程序的运行中,会将运算所需要的数据复制一份到CPU缓存中,这样CPU就可以直接对缓存中的数据进行运算,当运算结束后,将缓存中的数据刷新到内存中,依靠这样的方式,极大提高了CPU的吞吐量。
在提高了吞吐量的同时,也出现了另一个问题,缓存不一致、
试想有一段运算操作:i++
- 读取i的值到cpu cache中
- 对i进行加1操作
- 将结果保存到cpu cache中
- 运算完成,将数据刷新到内存中
这里就存在和java一样的多线程问题,在两个线程同时对i进行操作的时候,每个线程都有自己的工作内存,变量i会在多个线程的本地内存中都保存一个副本,假设i值为1,在同一时间的两个线程读取i的值保存在cpu cache中,经过运算后再写入内存,但i再经过两次自增后,最后写入内存的值还有可能是2。
为了解决这个问题,通常有两种办法:
- 加锁的方式
- 缓存一致性协议
第一种方式是一种悲观的方式,只有一个cpu能抢到锁进行运算,而其他cpu就进入阻塞状态。这种方式的效率低下,所以就有了第二种方式。
缓存一致性协议的大致思想是:
如果cpu在操作数据的时候,发现数据是一个共享变量(其他cache也保存了副本),就进行如下操作:
- 读取操作:不做任何处理,只是将cache的数据读取到寄存器。
- 写入操作:发送一个信号通知其他cpu这个变量我已经改过了,设置为无效变量,其他cpu在进行操作的时候就不得不重新读取一次数据再进行操作。