前言
Java util Concurrent 简称JUC,是javaEE里面很重要的一个知识点,下面学习一下juc里的关键词volatile。
Volatile的特点
1、保证可见性
2、不保证原子性
3、禁止指令重排
一、保证可见性
多线程并发编程时,程序的可见性是指,一个线程对资源进行变更后,立即写回主内存,并通知其他线程。
主内存和工作内存
java在运行时,对象信息都是存储在主内存当中,每个线程要对对象信息进行操作时,将会在工作内存中创建主内存里对象信息的一个副本,然后对对象进行操作,操作完成后,再将对象信息写回主内存,每个线程获取到数据操作后,其他线程并不知道,需要保证属性的可见性,才能减少并发编程中出现一些问题。
举例说明:
class MyTestData {
public int a = 0;
public void addData() {
a++;
}
}
public class TestConcurrent {
public static void main(String[] args) {
MyTestData myTestData=new MyTestData();
new Thread(()->{
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myTestData.addData();
}).start();
while (myTestData.a==0){
}
System.out.println("end");
}
}
假如a不修饰为volatile,则在新起的线程里修改了变量a,但是主线程里的a并不会变化,还是等于0。因为新起线程的里,改变了a的值,并把a 写回了主内存,但是并没有通知其他线程进行更新,这样主线程里工作内存里的a 并不会变化。
尝试修改为 public volatile int a = 0;
则正常结束,应该主线程收到通知后,就去主内存中,重新获取了a 的值。
二、不保证原子性
volatile在JUC中,只能算是一种轻型的同步,他比synchronize 更加轻量级,所以当我们只需要关系同步时的可见性时,那不需要使用synchronize关键词,只需要用更轻量级的volatile来修饰即可满足。但是volatile的轻量级,也是有缺陷的,因为它无法保证原子性。那么原子性又是个什么概念呢?
在JUC中,原子性是指一段代码在执行的过程中,不会被其他线程打断,所谓的要么不开始,要开始就执行到结束。调整一下上面的例子。
class MyTestData {
public volatile int a = 0;
public void addData() {
a++;
}
}
public class TestConcurrent {
public static void main(String[] args) {
MyTestData myTestData=new MyTestData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myTestData.addData();
}
}).start();
}
while (Thread.activeCount()>2){
}
System.out.println(myTestData.a);
}
}
在这里,我们起20个线程,每个线程循环1000遍a++,按理20*1000=20000,但是结果a的值却不是2万。
原因是因为a++这个操作,并不是一个原子操作,即便a 修饰了volatile 。
a++这个操作,可以拆分成以下4段。
1、从主内存中获取a ,并在工作内存中创建一个a的副本。
2、执行运算a+1
3、将a+1的结果赋值给工作内存中的a
4、讲工作内存中的a,赋值给主内存中的a,(修饰了volatile,并通知其他线程a值被更新了)。
那只要当前线程在取值后,到写入主内存之前,有其他线程已经将值写入了,那当前线程再写入主内存的时候,就会将其他线程的写入操作丢失了,这和数据库的事务操作很像。
原因大概了解了,那么要如何解决这种原子性的问题呢。
一种方法,就是使用synchronize关键词,或者使用ReentrantLock 来保证 a++操作的原子性。
另一种方法是使用JUC当中新的类型,AtomicInteger,JUC中危java的几种基本类型,提供了线程安全的对象方法类。
以下是解决的代码实现:
class MyTestData {
public volatile AtomicInteger a =new AtomicInteger();
public void addData() {
a.getAndIncrement();
}
}
public class TestConcurrent {
public static void main(String[] args) {
MyTestData myTestData=new MyTestData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myTestData.addData();
}
}).start();
}
while (Thread.activeCount()>2){
}
System.out.println(myTestData.a);
}
}
AtomicInteger 的getAndIncrement 就相当于线程安全的a++,重新执行一下,得到结果为20000。
三、禁止指令重排
指令重排是什么:一段代码要被执行,首先要被JVM编译成字节码,然后CPU再执行这些字节码,进行执行。
那么在这个过程中,JVM编译,CPU执行,都会都代码的顺序进行调整。
指令重排是为了在不改变执行结果的前提下,一种机器自我提高编译执行效率的方法。
举个例子:
int a=1; ——1
int b=2; ——2
int c=a+b;——3
这是我们自己编写的代码,正常的执行顺序应该是1,2,3。
但是实际上,1,2的执行顺序,并不影响后面的执行,所以指令重排的话,很有可能给你优化成2, 1, 3执行。
指令重排,在单线程环境中时没有任何问题的,但是到了多线程的时候,优化的顺序可能会对其他线程造成影响,导致结果不一致。
Volatile 是如何实现的呢,这里涉及到一个新的名词:内存屏障又称内存栅栏。
内存屏障可以保证被volatile修饰的变量,在执行前面都不会被指令重排,并且保证变量的可见性。
小结
以上是我看了volatile的一点自己的理解,如有不对,还请指出,大家共同学习,共同进步。