Java中多线程与并发_volatile关键字的深入理解

网友投稿 221 2023-02-13

Java中多线程与并发_volatile关键字的深入理解

一、volatile关键字

volatile是JVM提供的一种轻量级的同步机制,特性:

1.保证内存可见性

2.不保证原子性

3.防止指令重排序

二、JMM(java Memory Model)

Java内存模型中规定了所有的变量都存储在主内存中(如虚拟机物理内存中的一部分),每条线程还有自己的工作内存(如CPU中的高速缓存),线程的工作内存中保存了该线程使用到的变量到主内存的副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存和工作内存的交互关系如下图所示:

三、验证

1.验证volatile的可见性

1.1 假如 int num = 0; num变量之前根本没有添加volatile关键字修饰,没有可见性

1.2 添加了volatile,可以解决可见性问题

MyData类

class MyData {

volatile int num = 0;

public void addT060() {

this.num = 60;

}

}

内存可见性验证,其中两个线程分别为AAA线程和main线程

//volatile可以保证可见性,及时通知其它线程,主内存的值已经被修改

@Test

public 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();

}

myData.addT060();

System.out.println(Thread.currentThread().getName() + "\t update num value: " + myData.num);

},"AAA").start();

//第2个线程是我们的main线程

while (myData.num == 0) {

//main线程就一直在这里等待循环,直到num值不再等于0.

}

System.out.println(Thread.currentThread().getName() + "\t mission is over,main get num value: " + myData.num );

}

对num变量加volatile修饰后结果

AAA come in

AAA update num value: 60

main 我能见到AAA线程对num修改的结果啦,main get npFXwBIxum value: 60

Process finished with exit code 0

2.验证volatile不保证原子性

2.1 原子性指的是什么意思?

不可分割,完整性,也即某个线程正在做某个具体任务时,中间不可以被加塞或者被分割。需要整体完整。要么同时成功,要么同时失败。

2.2 volatile不保证原子性的案例演示

2.3 为什么不保证原子性?

2.4 如何保证原子性

加sync

使用我们juc下的AtomicInteger (底层实现CAS)

给MyData类加addPlusPlus()方法

class MyData {//MyData.java ===> MyData.class ===> JVM字节码

int num = 0;

public void addT060() {

this.num = 60;

}

//请注意,此时num前面是加了关键字修饰的,volatile不保证原子性

public void addPlusPlus() {

num++;

}

}

2.2 volatile不保证原子性的案例演示

num++在多线程操作的情况下不保证原子性的

创建20个线程并行执行num++操作2000次,多次测试,结果不为40000

public static void main(String[] args) {

MyData myData = new MyData();

for (int i = 1; i <= 20; i++ ) {

new Thread(() -> {

for (int j = 1; j <= 2000; j++) {

myData.addPlusPlus();

}

},String.valueOf(i)).start();

}

//需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?

while(Thread.activeCount() > 2) {

Thread.yield();

}

System.out.println(Thread.currentThread().getName() + "\t finally num value:" + myData.num);

}

结果:数值小于40000,出现写值丢失的情况

main  finally num value:38480

Process finished with exit code 0

2.3 为什么不保证原子性?

因为当线程A对num++操作从自己的工作内存刷新到主内存时,还未通知到其他线程主内存变量有更新的瞬间,其他线程对num变量的操作结果也对主内存进行了刷新,从而导致了写值丢失的情况

num++通过汇编指令分析,通过javap反编译得到如下汇编指令

class com.slx.juc.MyData {

volatile int num;

com.slx.juc.MyData();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: aload_0

5: iconst_0

6: putfield #2 // Field num:I

9: return

public void addT060();

Code:

0: aload_0

1: bipush 60

3: putfield #2 // Field num:I

6: return

public void addPlusPlus();

Code:

0: aload_0

1: dup

2: getfield #2 // Field num:I

5: iconst_1

6: iadd

7: putfield #2 // Field num:I

10: return

}

可见num++被拆分成了3个步骤,简称:读-改-写

执行getfield拿到原始num;

执行iadd进行加1操作;

执行putfield写把累加后的值写回

2.4 如何保证原子性

加sync

使用我们juc下的AtomicInteger (底层实现CAS)

MyData类中添加原子类操作方法

AtomicInteger atomicInteger = new AtomicInteger();

public void addMyAtomic() {

atomicInteger.getAndIncrement();

}

调用该方法打印结果

public static void main(String[] args) {

MyData myData = new MyData();

for (int i = 1; i <= 20; i++ ) {

new Thread(() -> {

for (int j = 1; j <= 2000; j++) {

myData.addMyAtomic();

}

},String.valueOf(i)).start();

}

//需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?

while(Thread.activeCount() > 2) {

Thread.yield();

}

System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type ,finally num value:" + myData.atomicInteger);

}

测试结果为40000,不会出现之前int类型的丢失值的情况

main  AtomicInteger type ,finally num value:40000

Process finished with exit code 0

总结

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:信息聚合网站(信息聚合网站有哪些)
下一篇:Java实现上传和下载功能(支持多个文件同时上传)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~