image-1672057150526

认识JMM

JMM 是指Java Mermory Model ,java 内存模型;它的目的是用来屏蔽计算机硬件和操作系统之间的差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

JMM 规定所有的变量都存储在内存中(主内存/主存),每个线程有自己的工作内存;线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存中的数据。不同的线程之间也无法直接方队对方工作内存的变量,线程间变量值的传递均需要通过主内存来完成。

image-1672058045291

并发编程需要解决的三类问题:原子性问题,可见性问题,有序性问题

原子性

什么是原子性

并发编程中的原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。

验证原子性问题

下面我们通过一段java程序一起验证原子性问题

我们使用100个线程,每个线程进行1万次累加操作;如何多线程没有原子性问题,那么所有的线程执行完毕以后,应该n的值应该是100 0000。

 private static long n = 0L;

    public static void main(String[] args) throws Exception {

        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    n++;
                }
                latch.countDown();
            });
        }
        for (Thread t : threads) {
            t.start();
        }
        latch.await();
        System.out.println(n);

    }

实际结果并非是预期的100 0000,出现了原子性问题。

如何保证原子性

我们可以通过如下手段来保证原子性;

  • synchronized 保证原子性

  • CAS 操作保证原子性,即通过compare and sweep 来实现

    例如:AtomicInteger 或者 Unsafe.getUnsafe().compareAndSwapXXX 操作

  • 使用ReentrantLock 操作保证原子性

可见性

什么是可见性

之前描述了JMM,线程操作变量都是在自己的工作内存中操作的,并且线程也无法看到其他线程的工作内存中的变量;所以多线程提高效率,本地缓存数据,造成数据修改不可见,要想保证可见,要么触发同步指令,要么加上volatile,被修饰的内存,只要有修改,马上同步涉及到的每个线程。

验证可见性问题

下面我们通过一段java程序一起验证可见性问题

public class Test {

  static boolean flag =true;
  
  public static void main(String[] args) throws InterruptException{
     
      Thread t1 = new Thread(()->{
          while(flag){
          }
          System.out.println("循环结束");
      });
     
     t1.start();
     Thread.sleep(100L);
     flag = false;
      System.out.println("主线程修改结束");
  }

}

运行后,线程t1 没有像预期的那样停止,主线程对标志位的修改,对t1线程不可见。

如何保证可见性

知道多线程在并发环境中有可见性问题以后,我们该如何在工作中避免发生可见性问题呢?

我们可以利用如下手段,来保证可见性:

  • 使用volatile 来保证可见性

    为什么volatile可以保证可见性?

    volatile的语义决定的;

    当对volatile修饰的变量进行读操作时会将CPU缓存的变量副本设置为无效,必须去主内存重新读取。
    当对volatile修饰的变量进行写操作时会将当前线程对应的CPU缓存及时的写入到主内存中去。

  • 使用synchronized 来保证可见性

    为什么synchronized可以保证可见性?

    synchronized修饰的同步代码块或者同步方法,在获取锁资源后,会将内部涉及到的变量CPU缓存中移除,就必须重新去主内存中获取;而且在锁资源释放之后,会立即把CPU缓存中的数据立即同步到主内存中去。

  • 使用ReentrantLock 来保证可见性

有序性

什么是有序性

我们编写的.java程序经过编译器编译后会生成.class文件,.class文件在实际成CPU能够识别的一条条的指令集;而CPU在执行这些指令的时候有可能时乱序执行的。

验证有序性问题

下面我们通过一段代码来验证有序性问题。经典面试题

public class Test {

  static int a,b,x,y;
 
 public static void main(String[] args ){
      for(int i=0;i<Integer.MAX_VALUE; i++){
         a =0;
         b =0;
         x =0;
         y =0;

         Thread t1 = new Thread(()->{
             a=1;
             x=a;
         });
         
         Thread t2 = new Thread(()->{
            b=1;
            y=b;
         });
         
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
         if(x==0&&y==0){
             System.out.print("出现非预期结果");
             break;
         }

      }
 
 }

}

如何保证有序性

为了防止出现有序性问题,我们可以通过volatile 来保证有序性。

关于有序性,CPU的乱序执行其实是为了提高CPU的执行效率,从宏观层面看上去就像是as if serial 。

同时JMM为了保证有序性,也有相关的原则,我们称之为happen-before 原则;分别如下:

  • 程序次序规则

    在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后买的操作。

  • 管程锁定规则

    一个unlock操作先行发生于后面对同一个锁的lock操作,后面是指时间上的先后。

  • volatile 变量规则

    对一个volatile变量的写操作线程发生于后面对于这个变量的读操作,后面是指时间上的先后。

  • 线程启动规则

    线程的start方法先行发生下对于此线程的每一个动作。

  • 线程终止规则

    线程中的所有操作先行发生于对此线程的终止检测。

  • 线程中断规则

    对线程interrupt方法的调用,先行发生于被中断线程代码检测到中断事件的发生。

  • 对象回收规则

    对象的初始化先行发生于对象的finalize方法的开始

  • 传递性规则

    A先行发生于B,B先行发生于C;那么A先行发生于C。

Mark

synchronized 可以保证原子性,可见性。

volatile 可以保证可见性,有序性。