《Java后端知识体系》系列之并发关键字(volatile、synchronized)

三天没有写博客了,三天都在整理并发编程的知识,发觉真的太多了,放到一起肯定不好整理,就这样分开整理,方便自己平时的阅读!

《Java后端知识体系》系列之并发关键字(volatile、synchronized)

并发关键字:

  • volitile

    • 特性
      • 可见性:在变量前面加了volatile的关键字就是指示JVM这个变量不稳定,所有线程更新它时都会立即同步到主存中,所有线程使用它的时候都会到主存中读取,这样保证了所有的线程使用该变量时都是最新的
        • 原理:volatile是从JVM层面保证变量的可见性。JVM中所有的变量存储在主内存中,同时每个线程又有自己独立的缓存用来存储从主内存中缓存的数据,这样每次获取更新数据时更新缓存中的数据,然后再同步到主存中,这样就减轻了主存的压力,而volatile怎么解决的可见性问题呢?首先为了维持缓存以及主从中数据一致性的问题,volatile使用了JVM层面中的lock锁,当一个volatile的变量更新时,会对该线程进行lock加锁,同时也会让其它线程中缓存的该变量都失效,当执行成功之后将该变量写入主内存中,然后解除lock状态,这样每次一个更新操作都是立即同步到主存中的,而所有的读取变量也都是通过该操作获取到主存中最新的值。
      • 禁止重排序:对于使用volatile关键字的变量来说,volatile能够保证它前面的操作都已经执行完毕并且返回的结果对后面的操作可见,后面的操作还没有执行,能够按照设定的顺序执行一系列操作
        • 原理:volatile也是从JVM层面也保证有序性的。在程序中有很多的优化重排序:编译器优化的重排序指令级的优化重排序内存系统的优化重排序,而volatile为了解决防止这些重排序的发生,使用了内存屏障来解决排序问题,也就是在每个volatile操作的前后都加入一个内存屏障,来保证前后的执行都是按照顺序执行的,实现如下:

          • 在每个volatile写操作的前面插入一个StoreStore的屏障,同时在volatile写操作的后面插入一个StoreLoad的屏障,
          • 在每个volatile读操作的前面加入 LoadLoad屏障同时在每个volatile读操作后面加入LoadStore屏障

          通过设定这样的内存屏障来保证操作都是按照顺序来执行的。

    • 用法:使用在变量
  • synchronized

    • 特性
      • 原子性:
      • 一致性:
      • 可见性:
      • **可重入性:**同一个线程可以获取同一把锁多次(wait、notify、notifyAll的作用)
    • 用法
      • 实例方法:在普通方法中使用synchronized关键字,作用的是实例对象。 synchronized都是作用在对象中的,只有作用在对象中,才能使用waitnotifynotifyAll来重新执行该线程。在实例方法中是通过ACC_SYNCHRONIZED这样一个标志,来告诉JVM这是一个同步方法,因此在进入该方法之前先要获取相应的,然后锁的计数器+1,执行结束后计数器-1,如果失败就阻塞,直到该锁被释放,因此synchronized关键字作用于方法是都是通过ACC_SYNCHRONIZED来实现一个加锁的操作的。

      • 静态方法:在静态方法中使用synchronized关键字,作用的是整个类对象。此时也是作用于方法中的,所以也是通过ACC_SYNCHRONIZED来实现的加锁操作。

      • 代码块:在代码块中使用synchronized关键字,作用的是调用这个代码块的对象。在代码块中由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。
        但是反编译的代码中有两个monitorexit,这是因为第二个monitorexit是来处理异常的,正常情况下第一个monitorexit之后会执行goto指令,而该指令转向的就是末尾的return,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。

      • **总结:**在JVM中,对象是分成三部分存在的:对象头实例数据对齐填充

        • 对象头:要结构是由Mark Word 和 Class Metadata Address 组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例
        • 实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度
        • 对齐填充:部分内存按4字节对齐;对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐
        • synchronized作用的是对象,这也是为什么等待和通知是在Object类中而不是在Thread中声明的一个原因。

今天依然是会敲代码的汤姆猫!