volatile是java虚拟机提供的轻量级的同步机制。
它有三大特性:
-
保证可见性
-
不保证原子性
-
禁止指令重排
1、保证可见性
JVM运行程序的实体是线程,每个线程在创建时JVM都会为其创建一个工作内存(有些地方称为栈空间)。工作内存是每个线程的私有数据区,而java内存模型(JMM)中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。
但问题是:线程对变量的操作(读、取、赋值等)必须在工作内存中进行:首先将变量从主内存拷贝到自己的工作内存,然后再对变量进行操作,在操作完成之后再将变量写回到主内存中。
那么什么是可见性呢?可见性指的是当主内存区域中的值被某个线程修改后,会马上通知其它线程,其它线程会马上知道并得到修改后的值。
可见性代码验证:
public class VolatileDemo {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t come in");
//暂停一下A线程,让main线程先执行
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.change();
System.out.println(Thread.currentThread().getName() + "\t 修改后的值为"+data.number);
},"A").start();
while (data.number==0){
}
System.out.println(Thread.currentThread().getName() + "\t 通知别的线程成功,保证可见性");
}
}
class Data{ //资源类
int number = 0;
public void change(){
number = 10;
}
}
运行结果:
这说明:没有加volatile关键字的话,程序在多线程的情况下是不保证可见性的。加上volatile之后:
将int number = 0;修改为volatile int number = 0;
2、不保证原子性
原子性:不可分割,完整性。某个线程正在做某个具体业务时,中间不可以被加塞或被分割,要么同时成功,要么同时失败。
代码验证:
public class Test01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值看是多少
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t int类型最后结果"+myData.number);
}
}
class MyData{
volatile int number = 0;
public void addPlusPlus(){
number++;
}
}
上面number加了volatile,我们的期望结果是20000,但实际运行结果:
那么我们该如何解决原子性的问题呢?
-
加synchroized
运行结果:
-
使用JUC下的AtomicInteger
代码示例:
public class Test01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
myData.addMyAtomic();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值看是多少
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " AtomicInteger 类型的结果:"+myData.atomicInteger);
}
}
class MyData{
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
运行结果:
3、指令重排
什么是指令重排呢?
计算机在执行程序时,为了提高性能,编译器和处理器会对指令进行重排:
源代码-->编译器优化的重排-->指令并行的重排-->内存系统的重排-->最终执行的指令
处理器在进行重排时,必须考虑指令之间的数据依赖性。
单线程情况下,最终执行结果和代码顺序是一致的。
多线程环境中线程是交替执行的,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
public class Sort(){
int x = 1;
int y = 2;
x = x+3;
y = x*x;
}
单线程执行顺序:1 2 3 4
多线程可能出现很多情况:
-
2 1 3 4
-
1 3 2 4
上述情况便是指令重排,即内部执行顺序和我们代码的顺序不一样。
但是指令重排也是有限制的,不会出现下面的顺序:
-
4 3 2 1
因为处理器在进行指令重排的时候,必须考虑到指令之间的数据依赖性。
指令重排代码示例:
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method01(){
a = 1;
flag = true;
}
//多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
public void method02(){
if (flag){
a = a+5;
System.out.println("retValue:"+a);
}
}
}
我们顺序调用method01()和method02(),单线程下:
a = 1;
flag = true;
a = a+5;
但在多线程环境下会出现:
flag = true;
a = a+5;
System.out.println("retValue:"+a);
a = 1;
孤独,是给我们思考的时间,在一个人的日子里,我们要做的只有一件事,那就是把自己变得优秀。