好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Java中JMM与volatile关键字的学习

JMM

JMM是指Java内存模型,不是Java内存布局,不是所谓的栈、堆、方法区。

每个Java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。

JMM可能带来可见性、原子性和有序性问题。

1.可见性: 指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题 是不存在。因为你在任何一个操作步骤中修改某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。但是这个问题在并行程序中就不见得了。如果一个线程修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。

2.原子性: 指一个操作是不可中断的,即使是多个线程一起执行的时候,一个线程操作一旦开始,就不会被其他线程干扰比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A 给他赋值 1,线程 B 给它赋值为 -1,。那么不管这两个线程以何种方式,何种步调工作,i的值要么是1,要么是-1,线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。

3.有序性: 对于一个线程的执行代码而言,我们总是习惯地认为代码的执行时从先往后,依次执行的。这样的理解也不能说完全错误,因为就一个线程而言,确实会这样。但是在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。

volatile关键字

volatile关键字是Java提供的一种轻量级同步机制。它能够保证可见性和有序性,但是不能保证原子性。

可见性与原子性测试

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

class MyData{

     int number= 0 ;

     //volatile int number=0;

     AtomicInteger atomicInteger= new AtomicInteger();

     public void setTo60(){

         this .number= 60 ;

     }

     //此时number前面已经加了volatile,但是不保证原子性

     public void addPlusPlus(){

         number++;

     }

     public void addAtomic(){

         atomicInteger.getAndIncrement();

     }

}

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

private static void volatileVisibilityDemo() {

     System.out.println( "可见性测试" );

     MyData myData= new MyData(); //资源类

     //启动一个线程操作共享数据

     new Thread(()->{

         System.out.println(Thread.currentThread().getName()+ "\t come in" );

         try {TimeUnit.SECONDS.sleep( 3 );myData.setTo60();

         System.out.println(Thread.currentThread().getName()+ "\t update number value: " +myData.number);} catch (InterruptedException e){e.printStackTrace();}

     }, "AAA" ).start();

     while (myData.number== 0 ){

      //main线程持有共享数据的拷贝,一直为0

     }

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

}

可见性:

MyData类是资源类,一开始number变量没有用volatile修饰,所以程序运行的结果是:

可见性测试
AAA come in
AAA update number value: 60

虽然"AAA"线程把number修改成了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。

如果对number添加了volatile修饰,运行结果是:

AAA come in
AAA update number value: 60
main mission is over. main get number value: 60

可见某个线程对number的修改,会立刻反映到主内存上。

原子性:

volatile 并不能保证操作的原子性 。这是因为,比如一条number++的操作,底层会形成3条指令。

?

1

2

3

4

getfield        //读

iconst_1    //++常量1

iadd        //加操作

putfield    //写操作

假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程A执行完毕,number=1,也立刻通知到了其它线程,但是此时线程B、C已经拿到了number=0,所以结果就是写覆盖,线程B、C将number变成1。

解决的办法就是:

对 addPlusPlus() 方法加锁。 使用 java.util.concurrent.AtomicInteger 类。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

private static void atomicDemo() {

     System.out.println( "原子性测试" );

     MyData myData= new MyData();

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

         new Thread(()->{

             for ( int j = 0 ; j < 1000 ; j++) {

                 myData.addPlusPlus();

                 myData.addAtomic();

             }

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

     }

     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 不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而 AtomicInteger 可以保证原子性。

?

1

2

3

原子性测试

main     int type finally number value: 17542

main     AtomicInteger type finally number value: 20000

有序性:

volatile可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。就好比一份试卷,题号是老师规定的(代码是程序员规定的),但是考生(CPU)可以先做选择题,也可以先做填空题。

但是有时候这种情况就会出现问题:

?

1

2

3

4

int x = 11 ; //语句1

int y = 12 ; //语句2

x = x + 5 ;  //语句3

y = x * x;  //语句4

以上例子,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。

哪些地方用到过volatile?

单例模式的安全问题

常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public class SingletonDemo {

     private static SingletonDemo singletonDemo= null ;

     private SingletonDemo(){

         System.out.println(Thread.currentThread().getName()+ "\t 我是构造方法" );

     }

     //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断

     public static SingletonDemo getInstance(){

         if (singletonDemo== null ){

             synchronized (SingletonDemo. class ){

                  if (singletonDemo== null ){

                      singletonDemo= new SingletonDemo();

                  }

             }

         }

         return singletonDemo;

     }

     public static void main(String[] args) {

         for ( int i = 0 ; i < 10 ; i++) {

             new Thread(()->{

                 SingletonDemo.getInstance();

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

         }

     }

}

这个漏洞比较tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();可以大致分为三步:

?

1

2

3

memory = allocate();     //1.分配内存

instance(memory);    //2.初始化对象

instance = memory;   //3.设置引用地址

由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Medel,即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候instance因为已经在线程A内执行过了第三点,instance已经是非空了,所以线程B直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误很可能会隐藏很久。

解决的方法就是对 singletondemo 对象添加上 volatile 关键字,禁止指令重排。

你知道CAS吗?

CAS是指 Compare And Swap , 比较并交换 ,是一种很重要的同步思想。如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。

?

1

2

3

4

5

6

7

8

public class CASDemo {

     public static void main(String[] args) {

         AtomicInteger atomicInteger= new AtomicInteger( 5 );

         System.out.println(atomicInteger测试数据pareAndSet( 5 , 2021 )+ "\t current data : " + atomicInteger.get());

         //修改失败

         System.out.println(atomicInteger测试数据pareAndSet( 5 , 1024 )+ "\t current data : " + atomicInteger.get());

     }

}

第一次修改,期望值为5,主内存也为5,主内存的值修改成功,为2021;第二次修改,期望值为5,主内存实际值为2021,修改失败。

CAS底层原理

?

1

2

3

public final int getAndIncrement(){

     return unsafe.getAndAddInt( this ,valueOffset, 1 );

}

查看 AtomicInteger.getAndIncrement() 方法,发现其没有加 synchronized 也实现了同步。这是为什么?

AtomicInteger 内部维护了 volatile int value 和 private static final Unsafe unsafe 两个比较重要的参数。

AtomicInteger.getAndIncrement() 调用了 Unsafe.getAndAddInt() 方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。

?

1

2

3

4

5

6

7

public final int getAnddAddInt(Object var1, long var2, int var4){

     int var5;

     do {

         var5 = this .getIntVolatile(var1, var2);

     } while (! this 测试数据pareAndSwapInt(var1, var2, var5, var5 + var4));

     return var5;

}

这个方法的var1和var2,就是根据对象和偏移量得到在主内存的快照值var5。然后 compareAndSwapInt 方法通过var1和var2得到当前主内存的实际值。如果这个实际值跟快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

比如有A、B两个线程,一开始都从主内存中拷贝了原值为3,A线程执行到 var5=this.getIntVolatile ,即var5=3。此时A线程挂起,B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的。A线程被唤醒,执行 this测试数据pareAndSwapInt() 方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。

CAS缺点

CAS实际上是一种自旋锁

一直循环等待,开销比较大。 只能保证一个变量的原子操作,多个变量依然要加锁。 引出ABA问题。

ABA问题

所谓的ABA问题,就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。比如线程T1将一个值从A改为B,然后又从B改为A。当线程T2访问时,看到的就是A,但是却不知道这个A其实发生了更改。尽管线程T2 CAS操作成功,但是不代表就没有问题。

有的需求,比如CAS,只注重头尾(只看期望值和实际值),只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改,这就引出了AtomicReference:原子引用。

AtomicReference

AtomicInteger 对整数进行原子操作,但是如果对象是一个POJO呢?我们这时就可以使用 AtomicReference 来包装这个POJO,使其操作原子化。

?

1

2

3

4

5

6

User user1 = new User( "Jack" , 25 );

User user2 = new User( "Lucy" , 21 );

AtomicReference<User> atomicReference = new AtomicReference<>();

atomicReference.set(user1);

System.out.println(atomicReference测试数据pareAndSet(user1,user2)); // true

System.out.println(atomicReference测试数据pareAndSet(user1,user2)); //false

AtomicStampedReference 和 ABA 问题的解决

使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个版本号Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才能执行更新操作。

?

1

AtomicStampedReference测试数据pareAndSet(expectedReference,newReference,oldStamp,newStamp);

使用实例:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

package thread;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicReference;

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {

     static AtomicReference<Integer> atomicReference = new AtomicReference<>( 100 );

     static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>( 100 , 1 );

     public static void main(String[] args) {

         System.out.println( "======ABA问题的产生======" );

         new Thread(() -> {

             atomicReference测试数据pareAndSet( 100 , 101 );

             atomicReference测试数据pareAndSet( 101 , 100 );

         }, "t1" ).start();

         new Thread(() -> {

             try {

                 TimeUnit.SECONDS.sleep( 1 );

             } catch (InterruptedException e) {

                 e.printStackTrace();

             }

             System.out.println(atomicReference测试数据pareAndSet( 100 , 2019 ) + "\t" + atomicReference.get().toString());

         }, "t2" ).start();

         try { TimeUnit.SECONDS.sleep( 2 ); } catch (InterruptedException e) { e.printStackTrace(); }

         System.out.println( "======ABA问题的解决======" );

         new Thread(() -> {

             int stamp = atomicStampedReference.getStamp();

             System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);

             try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }

             atomicStampedReference测试数据pareAndSet( 100 , 101 ,

                     atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+ 1 );

             System.out.println(Thread.currentThread().getName() + "\t第二次版本号: " + atomicStampedReference.getStamp());

             atomicStampedReference测试数据pareAndSet( 101 , 100 ,

                     atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+ 1 );

             System.out.println(Thread.currentThread().getName() + "\t第三次版本号: " + atomicStampedReference.getStamp());

         }, "t3" ).start();

         new Thread(() -> {

             int stamp = atomicStampedReference.getStamp();

             System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);

             try { TimeUnit.SECONDS.sleep( 3 ); } catch (InterruptedException e) { e.printStackTrace(); }

             boolean result=atomicStampedReference测试数据pareAndSet( 100 , 2019 ,

                     stamp,stamp+ 1 );

             System.out.println(Thread.currentThread().getName()+ "\t修改成功与否:" +result+ "  当前最新版本号" +atomicStampedReference.getStamp());

             System.out.println(Thread.currentThread().getName()+ "\t当前实际值:" +atomicStampedReference.getReference());

         }, "t4" ).start();

     }

}

总结

总结来源于GitHub,内部带有源码和用例图。

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注的更多内容!

原文链接:https://blog.csdn.net/Pluto_1223/article/details/120417558

查看更多关于Java中JMM与volatile关键字的学习的详细内容...

  阅读:11次