郑州市建设网站谷歌seo关键词优化
文章目录
- volatile关键字
- 验证可见性的代码demo:
- 原子性
- 重排序
- 禁止重排列的原因
- volatile怎么实现的禁止重排序保证有序性
- 小结
volatile关键字
volatile是Java虚拟机提供的轻量级同步机制主要特征有三个:
- 保证可见性
- 不保证原子性
- 禁止指令重排
首先要了解一下,为什么需要同步机制!!
由于计算机的高速发展,性能的提高,再也不是单核单线程了,多线程也是由于硬件的提升才得以实现,那么多线程会出现什么问题呢?
一个多线程问题主要在于共享变量之间的资源分配,如果多个核心线程处理,运行时其实是各个线程到主内存中拿到变量的值,克隆一个副本到自己的线程内存上执行,读写操作,当线程在自己的工作内存上处理好了数据,需要将其重新写入到主内存。而多线程并行的时候要注意的是可见性问题,如一个用户属性age=20,各个线程在主内存拿到值拷贝副本会自己的工作内存,之后在自己内存改完后,要同步到主线程内存中,同时其他线程要可见,这样才能避免数据不一致的情况。接下来就讲讲volatile关键字相关内容。
验证可见性的代码demo:
package com.it.test;import java.util.concurrent.TimeUnit;class MyData {int number=0;public void addT060(){this.number=60;}
}/*** 1.验证volatile的可见性* 1.1假如int number=0;,number变量之前没有加volatile关键字修饰*/
public class VolatileDemo {public static void main(String[] args) {MyData myData = new MyData();//资源类new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");//暂停一会线程try {TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}//加入方式使得number在AAA线程中值变为60myData.addT060();System.out.println(Thread.currentThread().getName()+"\t update number value" + myData.number);},"AAA").start();//第二个线程就是我们的main线程while (myData.number==0){//main线程就在这里一直等待循环,直到number值不等于0}System.out.println(Thread.currentThread().getName()+"\t mission is over");}
}
第一句打印后,等待3s又打印了第二句,但是主线程一直在等待number=0,所以最后一句没有打印。
在MyData类加入volatile关键字
volatile int number=0;
VolatileDemo类最后一句修改,以示区别确认number是否被主线程可见。
System.out.println(Thread.currentThread().getName()+"\t mission is over,get number value:"+ myData.number);
可见确实变成60了
原子性
首先将方法重构:
使用快捷键
Ctrl + Alt + M
或者
选中待提炼代码 --> 右击 --> Refactor --> Extract --> Method.
安了refactor之后方法变成以下方法:
//volatile可以保证可见性,及时通知其他线程,主物理内存的值已经修改public static void seeOkbyVolatile() {MyData myData = new MyData();//资源类new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");//暂停一会线程try {TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}//加入方式使得number在AAA线程中值变为60myData.addT060();System.out.println(Thread.currentThread().getName()+"\t update number value" + myData.number);},"AAA").start();//第二个线程就是我们的main线程while (myData.number==0){//main线程就在这里一直等待循环,直到number值不等于0}System.out.println(Thread.currentThread().getName()+"\t mission is over,get number value:"+ myData.number);}
注意多线程条件判断必须使用while,而不是if,使用if会出现问题。接下来,在MyData中加入number++方法
//number++方法,此时的number是已经加了volatile的public void addPlusPlus(){number++;}
main方法验证代码:
public static void main(String[] args) {//seeOkbyVolatile();//String.valueOf(i).start()即为使用名字0-19的名字启动线程MyData myData = new MyData();for (int i = 0; i <20 ; i++) {new Thread(()->{for (int j = 0; j <1000 ; j++) {myData.addPlusPlus();}},String.valueOf(i)).start();}//需要上面20个线程都全部计算完成后,再用main线程取得最终的结果是多少//大于的2意为还有线程在跑while (Thread.activeCount()>2){//线程让步,让上面的线程继续跑Thread.yield();}System.out.println(Thread.currentThread().getName()+"\t finally number value" + myData.number);}
完整代码:
package com.it.test;import java.util.concurrent.TimeUnit;class MyData {volatile int number=0;public void addT060(){this.number=60;}//number++方法,此时的number是已经加了volatile的public void addPlusPlus(){number++;}
}/*** 1.验证volatile的可见性* 1.1假如int number=0;,number变量之前没有加volatile关键字修饰* 1.2使用volatile,解决可见性问题** 2验证volatile不保证原子性* 2.1原子性定义* 不可分割,也即某个线程正在做某个具体业务的时,中间不可被加塞或者分割。需要整体完整* 要么同时成功,要么同时失败* 2.2是否可以保证原子性**/
public class VolatileDemo {public static void main(String[] args) {//seeOkbyVolatile();//String.valueOf(i).start()即为使用名字0-19的名字启动线程MyData myData = new MyData();for (int i = 0; i <20 ; i++) {new Thread(()->{for (int j = 0; j <1000 ; j++) {myData.addPlusPlus();}},String.valueOf(i)).start();}//需要上面20个线程都全部计算完成后,再用main线程取得最终的结果是多少//大于的2意为还有线程在跑while (Thread.activeCount()>2){//线程让步,让上面的线程继续跑Thread.yield();}System.out.println(Thread.currentThread().getName()+"\t finally number value" + myData.number);}//volatile可以保证可见性,及时通知其他线程,主物理内存的值已经修改public static void seeOkbyVolatile() {MyData myData = new MyData();//资源类new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");//暂停一会线程try {TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}//加入方式使得number在AAA线程中值变为60myData.addT060();System.out.println(Thread.currentThread().getName()+"\t update number value" + myData.number);},"AAA").start();//第二个线程就是我们的main线程while (myData.number==0){//main线程就在这里一直等待循环,直到number值不等于0}System.out.println(Thread.currentThread().getName()+"\t mission is over,get number value:"+ myData.number);}
}
跑一下测试,跑多几次
结果都不一样,大多数情况都不会是20000。
分析原因:
由于number++执行这个指令的时候实际上是被JVM拆分为
1.执行getfield拿到原始number;
2.执行iadd进行加1操作
3.执行putfield写把累加后的值写回
大家线程都拿到主内存的number,然后再自己内存写入,如初始情况都是0,然后3个线程都拿了0回到自己的工作线程处,各自进行加加操作,突然有线程挂起了,然后在非常短的时间内唤醒之后执行写putfield和前面写出一样的值进入了主线程。所以出现数值少于20000的原因,就是出现了丢失写值的情况。
而如果加了synchronized关键字,则别的线程即使你挂起,也需要在你加完,这个流程执行完才能执行。可以测多几次都是一样的结果,20000.没问题
当然直接加synchronized太重,juc给我们提供了保证原子类的方法,在此我们可以使用AtomicInteger方法。
给MyData方法创建
//使用原子类方法AtomicInteger atomicInteger=new AtomicInteger();public void addMyAtomic(){//自增方法atomicInteger.getAndIncrement();}
给main方法的j线程循环加入
myData.addMyAtomic();
main方法最后加入
System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value" + myData.atomicInteger);
看看完整代码,然后演示比较普通方法和原子方法:
package com.it.test;import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;class MyData {volatile int number=0;public void addT060(){this.number=60;}//number++方法,此时的number是已经加了volatile的public void addPlusPlus(){number++;}//使用原子类方法AtomicInteger atomicInteger=new AtomicInteger();public void addMyAtomic(){//自增方法atomicInteger.getAndIncrement();}
}/*** 1.验证volatile的可见性* 1.1假如int number=0;,number变量之前没有加volatile关键字修饰* 1.2使用volatile,解决可见性问题** 2验证volatile不保证原子性* 2.1原子性定义* 不可分割,也即某个线程正在做某个具体业务的时,中间不可被加塞或者分割。需要整体完整* 要么同时成功,要么同时失败* 2.2是否可以保证原子性* 2.3为什么会不够20000* 写入的时候,出现了写重复的情况* 2.4解决方法如下:* * 加synchronized* * 使用juc包里的原子性方法AtomicInteger*/
public class VolatileDemo {public static void main(String[] args) {//seeOkbyVolatile();//String.valueOf(i).start()即为使用名字0-19的名字启动线程MyData myData = new MyData();for (int i = 0; i <20 ; i++) {new Thread(()->{for (int j = 0; j <1000 ; j++) {myData.addPlusPlus();myData.addMyAtomic();}},String.valueOf(i)).start();}//需要上面20个线程都全部计算完成后,再用main线程取得最终的结果是多少//大于的2意为还有线程在跑while (Thread.activeCount()>2){//线程让步,让上面的线程继续跑Thread.yield();}System.out.println(Thread.currentThread().getName()+"\t int type finally number value" + myData.number);System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value" + myData.atomicInteger);}//volatile可以保证可见性,及时通知其他线程,主物理内存的值已经修改public static void seeOkbyVolatile() {MyData myData = new MyData();//资源类new Thread(()->{System.out.println(Thread.currentThread().getName()+"\t come in");//暂停一会线程try {TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}//加入方式使得number在AAA线程中值变为60myData.addT060();System.out.println(Thread.currentThread().getName()+"\t update number value" + myData.number);},"AAA").start();//第二个线程就是我们的main线程while (myData.number==0){//main线程就在这里一直等待循环,直到number值不等于0}System.out.println(Thread.currentThread().getName()+"\t mission is over,get number value:"+ myData.number);}
}
如果修改后出现debug但是原子类方法,打印的不是20000,你可以尝试一下,先直接run,之后再debug调试,方法就输出就正常的了,加synchronized的时候也一样。
重排序
禁止重排列的原因
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量是否能保证一致性是无法确定的,结果无法预测。
volatile怎么实现的禁止重排序保证有序性
volatile实现禁止指令重排优化
,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory Barrier)又称为内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行性
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在指令见插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
小结
工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或者volatile关键字解决,他们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化