Java锁学习笔记

  • synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
  • Java中每一个对象都可以作为锁,普通同步方法,锁是当前对象的实例;静态同步方法,锁是当前类的class对象;静态代码块,锁是括号里面的对象。
  • 同步代码块:同步代码块是使用monitorenter和monitorexit指令实现的;monitorenter指令插入到同步代码块的开始位置,monitorexit指定插入到同步代码块的结束为止,JVM保证每一个monitorenter和monitorexit成对出现。任何U对象都有一个monitor与之相对应,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。
  • 同步方法:synchronized方法会被翻译成普通的方法调用和返回指令,如:invokevirtual、areturn指令,在JVM字节码层面并没有任何特别的指令 来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志物置位1,表示该方法是同步方法使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass作为锁对象。
  • Java对象头:synchronized用的锁存在Java对象头里面。Hotspot虚拟机的对象头主要包括两部分数据,Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身运行时数据,它是实现轻量级锁和偏向锁的关键。
  • Monitor:Monitor可以理解为一个同步工具,也可以描述为一种同步机制,也是一个对象,每个Java对象都有可能成为一个Monitor,因为每一个Java对象本身就带有一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor是线程私有的数据结构,每个线程都有一个可用的monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中还有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
  • 自旋锁:自旋锁让线程不会被立即挂起,而是让线程执行一段时间的循环去等待,看持有锁的线程是否会很快释放锁。自旋等待不能代替阻塞,在JDK1.6中默认开启,同时自旋的默认次数为10次,可以通过-XX:PreBlockSpin来调整次数。
  • 适应自旋锁:所谓自适应就意味着自旋的次数不是固定的,它由前一次在同一个锁上的自旋时间以及锁的拥有着的状态决定。具体为如果这一次自旋成功了,那么下次自旋的次数就会加多。反之,如果对于某个锁很少有自旋成功的,那么以后这个锁的自旋次数就会减少或者省略自旋过程直接挂起。
  • 锁消除:为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支撑。
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
  • 轻量级锁:引入轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。对于轻量级锁,其性能提升的依据是"对于绝大部分锁,在整个生命周期内都是不会存在竞争的",如果打破这个依据,除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。
  • 偏向锁:引入偏向锁的主要目的是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。偏向锁不会主动释放,而是等待其它的线程来竞争锁才会释放,偏向锁的撤销需要等待全局安全点(这个时间点是没有正在执行的代码)。
  • 重量级锁:重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程间的切换需要从用户态到内核态的装换,切换成本给常高。
  • volatile的定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获得这个变量。
  • 解决缓存一致性的方案:
    1. 通过在总线加LOCK#锁的方式。
      采用一种独占的方式来实现,只能有一个CPU能够运行,其它CPU都阻塞,效率低。
    2. 通过缓存一致性协议。
      缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其它CPU告知该变量的副本是无效的,因此其它CPU在读取该变量时,发现其无效会重新从主存中加载该数据。
  • 原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立刻看到修改后的值。
  • 有序性:程序执行是顺序按照代码的先后顺序执行。
  • volatile可以保证线程的可见性并提供了一定的有序性,但无法保证原子性。在JVM底层volatile采用"内存屏障"来实现,汇编中会使用lock前缀指令形成内存屏障。volatile使用b必须满足两个条件:(1)对变量的写操作不依赖当前值。(2)该变量没有包含在具有其他变量的不变式中。
  • volatile实现原理:
    • 在每一个volatile写操作前面插入一个StoreStore屏障。
    • 在每一个volatile写操作后面插入一个StoreLoad屏障。
    • 在每一个volatile读操作后面插入一个LoadLoad屏障。
    • 在每一个volatile读操作后面插入一个LoadStore屏障。
      StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
      StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
      LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
      LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
  • happens-before:在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。
  • as-if-serial语义:所有的操作均可以为了优化而被重排序,但是必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下无效。
  • 重排序不会影响单线程环境的运行结果,但会破坏多线程的执行语义。
  • DCL(Double Check Lock)多重检测锁定。
  • JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
  • AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。
    AQS解决了子啊实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
    在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩行,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。
    AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
    AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。
    AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
    AQS主要提供了如下一些方法:
    • getState():返回同步状态的当前值;
    • setState(int newState):设置当前同步状态;
    • compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
    • tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;
    • tryRelease(int arg):独占式释放同步状态;
    • tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
    • tryReleaseShared(int arg):共享式释放同步状态;
    • isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
    • acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
    • acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
    • tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
    • acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
    • acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
    • tryAcquireSharedNanos(int arg, long nanosTimeout)`:共享式获取同步状态,增加超时限制;
    • release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
    • releaseShared(int arg):共享式释放同步状态;
  • CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

你可能感兴趣的:(Java锁学习笔记)