首先得了解一下volatile是干什么的:
volatile 关键字:当多个线程进行操作共享数据时,可以保证共享数据的内存可见性。
使用方式例如:volatile int a = 3;
什么是内存可见性?先不急着,看看下面java的内存模型的说明:
想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中
保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋
值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传
递均需要通过主内存来完成。
*这时候可以解释一下可见性的含义:是指当多个线程访问同一个变量时,一个线程的工作内存修改了这个变量的值,会让主内存该变量的值立刻改变,这可保证其他线程读取共享变量的值每次都是最新的值。
还是不太理解?个人看法是,如果不加volatile修饰的普通共享变量,一个线程对其的值进行修改时,会有以下步骤:
1.在该线程内存里的变量值修改
2.再把线程内存里的值写回主内存里,更新主存内的变量值
而实际上多线程数据操作里,步骤二可能会不能及时地将线程内存修改后的值写回主存里,导致这时其他线程从主存中读取到的值是未更新的,从而发生脏读现象。
但是加上了volatile修饰后,在步骤一发生的同时,主存内的变量值也会立刻被修改,这就保证了其他线程读取共享变量的值每次都是最新的值。
volatile除了确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,
例如:double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的。所以,如果一个线程正在修改该 long 变量的值,这时可能该long变量对于其他变量来说只有一半是“可见的”。
因而当你知道该long、double成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。
因为,当你对一个 volatile 变量进行写操作之前,Java 内存模型会插入一个写屏障(writebarrier),读一个volatile 变量之前,会插入一个读屏障(read barrier),这能解决上面long、double类型只能读“一半”的尴尬现象。
*但需要注意的是:
volatile虽然能保证数据操作的内存可见性,但其不能保证原子性
为什么呢?看下面的例子:
1.下面我们假设有两个线程对添加了volatile修饰的共享变量i进行 i++操作,就假设i的初值为3.
2.当线程一要进行i++操作时,先读取i的值3,但在还没来得及对i进行自增操作并写入主内存的时候。
线程二也读取了i的值为3,等到线程一对i进行自增操作并将volatile的可见性将更新后的值立刻写回主内存中时,主存里的i变为4。
3.可是这时已经晚了,因为线程二对i的值得读取是发生在线程一的修改前的,这是线程二里的i的值也是3,那么线程二也对i进行自增并将值写回主存中去,这时主存i的值还是4,这显然不符合我们的需求。
因而仅靠使用volatile有时候是无法保证数据的原子性的,在适当的时候我们还是需要使用synchronized或Lock等锁机制以保证共享数据操作的原子性。
总结
volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题的,但是如果多写,同样无法解决线程安全问题。例如是 count++操作,应该使用如下类实现:
AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推荐使用
LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
这篇文章给了我很好的思路,而且写的也更详细更有意思,对volatile尚有疑惑的可以去看看: