Java内存模型和volatile、synchronized

389 阅读6分钟

前言

  1. 先说说计算机缓存:计算机在执行程序的时候,都是通过CPU来执行指令,当然执行一串指令少不了需要某些数据,这些数据就在主内存中(物理内存)。随着科技不断发展,CPU执行速度越来越快,但内存存取发展并没有跟上CPU飞速发展的脚步,导致性能瓶颈出现在了内存存取上,所以这个时候出现了缓存技术来加快数据的存取。
  2. 在程序真正运行时,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读写数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
  3. 但是当出现多核CPU时,每个核上都存在高速缓存,而且都可能运行着线程,线程又是并发的,它们都会修改自己所在核的高速缓存中的变量,就会导致数据不一致的情况。

原子性、可见性、有序性

  1. 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。 -- 处理器优化会导致原子性问题
  2. 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 -- 缓存一致性
  3. 有序性即程序执行的顺序按照代码的先后顺序执行。 -- 指令重排导致有序性问题
  4. 如何实现上面说到的三个特性呢?=>这就是Java的内存模型要解决的问题了

Java内存模型 -- JMM

  1. Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是使到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
  2. 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
  3. 而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步
  4. 所以,JMM就是一种规范,其用于解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。
  5. Java内存模型,除了定义了一套规范,还提供了一系列原语(volatile、synchronized、final、concurrent包),封装了底层实现后,供开发者直接使用。

volatile

  1. 使用volatile能将被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次使用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。
  2. volatile只能保证有序性和可见性。

synchronized

  1. synchronized可以保证原子性、有序性和可见性。
  2. 作用域:
  3. 作用于普通方法
  4. 作用于静态方法
  5. 作用于代码块
  6. 只有在同步的块或者方法中才能调用wait/notify等方法
  7. 作用范围:
  8. 当修饰方法时候,其只作用于本对象实例
  9. 当修饰静态方法时,作用于该类的对象(静态方法本就属于这个类,所以任何关于该类的都可)
  10. 当修饰代码块时,还是该对象实例
  11. 这里再延伸探讨一下synchronized是如何保证原子性的呢?这就要先说说Java虚拟机如何执行线程同步了

Java虚拟机如何执行线程同步

  1. 在Java的内存结构中,有两块区域会被所有的线程共享 -- 方法区(存储静态变量)和堆(存储所有对象实例),因为实现了共享所以同一个对象也应该能被多个线程改变。要实现这些改变那就需要Java虚拟机来管控。
  2. 所以虚拟机给每个对象和类都加了一个锁,当某个线程要去改变这个对象的时候,就会去向虚拟机申请锁(虚拟机可能会延迟给锁),获取到锁的线程对对象进行修改,之后再将锁还给虚拟机,虚拟机又为下个线程分配锁。
  3. 其中给类加锁,其实也是通过给对象加锁实现的,因为每个类加载的时候,都会有一个java.lang.Class的实例,给这个实例加锁来实现。
  4. 监视器(Monitors):JVM中锁的是通过一个叫做监视器的东西来实现的,监视器用来监视一段代码,保证同一时间只有一个线程在执行它。
  5. 每一个监视器都与一个对段象相关联,当开始运行到这段代码时,线程必须要获取到它所引用对象的锁。一旦获得锁,线程便可以进入“被保护”的代码开始执行。当线程离开代码块的时候,无论如何离开,都会释放所关联对象的锁。

synchronized实现原理

  1. 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
    3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  2. 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
    1. 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
  • 这是synchronized同步代码块的原理,还有同步方法是通过ACC_SYNCHRONIZED标志实现的,具体待补充...

最后回到synchronized是如何保证原子性问题上

比如线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。

参考文章

how-the-java-virtual-machine-performs-thread-synchronization 再有人问你Java内存模型是什么,就把这篇文章发给他。 Synchronized的实现原理