背景
上篇文章介绍了java的53个关键字,其中个人感觉volatile和synchronized两个java关键字可以重点详细介绍下.这两个关键字都是作用在多线程并发环境下,其中volatile能保证操作对象的可见性和有序性,synchronized能保证操作对象的原子性和可见性.
JMM
多线程并发环境有必要先了解JMM(java memory model),在了解JMM前我们需要知道PC物理机的内存模型,如图:
CPU处理指令的性能很高,而CPU直接从内存中读取数据的性能相对来说就很慢了,所以如果直接都从内存中读取数据,会严重拖慢PC的处理速度.所以才会有CPU缓存,一般都是3级缓存,级别越高CPU读取性能越高,CPU内存也有寄存器,它的读取性能是最高的.
JMM内存模型如图:
注意JMM不是真实物理的内存结构,它是java虚拟机栈工作内存的规范.每个线程都有自己的工作内存,线程对所有变量的操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存.
可见性
当一个共享变量被修改时,它的值会立即更新到主内存,当有其他线程读取时,都会去主存中读取最新值.说明该变量是对所有线程是具有可见性.
有序性
java虚拟机的编译器和处理器会对指令进行重排序优化,来提升代码的执行效率.重排序会依据happens-before原则,保证指令代码的有序性.重排序不会影响单线程情况下的执行结果,但多线程并发的情况下可能会影响到它的正确性.所以并发情况下需要防止虚拟机对一定代码的重排序.
原子性
多个代码执行,要么同时都执行,要么都不执行,像原子一样不能被分割,即这些操作不可被中断,我们就说这些操作是具备原子性.
volatile
可见性代码例子
public class VisibilityDemo {
static boolean flag = true;
public static void main(String[] args) throws Exception {
new Thread(()->{
System.out.println("开始循环啦~~~");
while(flag){
}
System.out.println("循环退出了~~~");
}, "t1").start();
Thread.sleep(2000);
new Thread(()->{
System.out.println("flag的值修改为false");
flag=false;
}, "t2").start();
}
}
t1线程中的flag一直获取不到t2线程修改flag变量后的值所以一直在循环中,运行结果如下图:
对变量使用volatile修饰就可以退出循环了.
public class VisibilityVolatileDemo {
static volatile boolean flag = true;
public static void main(String[] args) throws Exception {
new Thread(()->{
System.out.println("开始循环啦~~~");
while(flag){
}
System.out.println("循环退出了~~~");
}, "t1").start();
Thread.sleep(2000);
new Thread(()->{
System.out.println("flag的值修改为false");
flag=false;
}, "t2").start();
}
}
t1线程中的flag能获取到t2线程修改flag变量后的值所以就退出循环了,运行结果如下图:
有序性代码例子
public class OrderlinessDemo {
public static int a=0,b=0,i=0,j=0;
public static void main(String[] args) throws Exception {
int count = 0;
while(true){
a=0;
b=0;
i=0;
j=0;
Thread t1 = new Thread(() -> {
a = 1;
i = b;
}, "t1");
Thread t2 = new Thread(() -> {
b = 1;
j = a;
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
count++;
System.out.println("第"+ count +"次输出结果, i = "+i+", j ="+j);
if(i==0 && j==0){
break;
}
}
}
}
理论上来说,并发情况下只可能输出i=0,j=1;i=1,j=0;i=1,j=1的情况.只有当发生指令重排序,才能输出i=0,j=0的情况,运行结果如下图:
public class OrderlinessVolatileDemo {
public static volatile int a=0,b=0,i=0,j=0;
public static void main(String[] args) throws Exception {
int count = 0;
while(true){
a=0;
b=0;
i=0;
j=0;
Thread t1 = new Thread(() -> {
a = 1;
i = b;
}, "t1");
Thread t2 = new Thread(() -> {
b = 1;
j = a;
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
count++;
System.out.println("第"+ count +"次输出结果, i = "+i+", j ="+j);
if(i==0 && j==0){
break;
}
}
}
}
添加volatile修饰变量后,一直没有出现i=0,j=0的输出情况,运行结果如下图:
原子性代码例子
public class AtomicityDemo {
static int count = 0;
public static void main(String[] args) {
for(int i=0; i<100;i++){
new Thread(()->{
for(int j=0;j<10000;j++){
count++;
System.out.println("输出结果:"+count);
}
}).start();
}
}
}
100个线程进行累加1W次,输出的结果始终小于100W且每次运行的结果大概率会都不一样,运行结果如下图:
第一次:
第二次:
第三次:
public class AtomicityVolatileDemo {
volatile static int count = 0;
public static void main(String[] args) {
for(int i=0; i<100;i++){
new Thread(()->{
for(int j=0;j<10000;j++){
count++;
System.out.println("输出结果:"+count);
}
}).start();
}
}
}
使用volatile修饰变量后,结果也是小于100W,说明volatile是无法保证原子性的,但最后输出的数值会比上面未使用volatile修饰的运行结果更接近100W的结果,运行结果如下图:
第一次:
第二次:
第三次:
synchronized
原子性和可见性代码例子
public class AtomicitySynDemo {
static int count = 0;
public static void main(String[] args) {
for(int i=0; i<100;i++){
new Thread(()->{
for (int j = 0; j < 10000; j++) {
synchronized (AtomicitySynDemo.class) {
count++;
System.out.println("输出结果:" + count);
}
}
}).start();
}
}
}
使用synchronized的代码块,能保证代码块里面的操作具备的原子性和变量的可见性,所以每次运行结果都是100W.
总结
特点
volatile关键字解决的是内存可见性和有序性的问题.可见性是因为变量的写操作都会直接刷新到主存,读操作都会去主存中同步.有序性是变量通过内存屏障(是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束)来禁止指令重排序.
synchronized关键字通过锁机制来保证代码块的同步顺序执行,从而解决操作原子性的问题.同时synchronized代码块的每次执行开始和结束,都会分别将变量读取主存和写入主存中,从而解决变量可见性的问题.synchronized不能防止同步代码块里面的代码进行重排序,所以不能解决代码块的有序性.
[下一篇 介绍synchronized对象锁]