深入并发编程——共享模型之管程(悲观锁)

深入并发编程——共享模型之管程(悲观锁)

  • synchronized变量
  • 变量的线程安全
    • 成员变量和静态变量的线程安全:
    • 局部变量的线程安全:
  • 重量级锁及Monitor
    • Java 对象头:
    • Monitor概念:
  • 轻量级锁
  • 锁动态
    • 锁膨胀
    • 自旋优化
    • 锁消除
  • 偏向锁
    • 撤销对象的可偏向状态情况
    • 批量重偏向
    • 批量撤销

synchronized变量

(1)synchronized 俗称对象锁,采用互斥的方式让同一时刻至多有一个线程能持有对象锁。Synchronized 的互斥保证了临界区代码的原子性,不会上下文切换引起指令的交错。同步是保证了某一个条件不满足时让线程等待,待条件满足后继续向下运行。
(2)thread1 线程获得锁后,即使时间片用完进入就绪态,其他线程也无法拿到 thread1 锁住的对象,也就无法进入临界区,此时处于 BLOCKED 状态。
(3)当 thread1 调用 synchronized 而 thread2 未调用 synchronized,即使锁未被释放 thread2 仍能访问临界区中的资源。
(4)synchronized 加在成员方法上表示对当前调用的对象加锁(锁住 this对象),加在类方法上表示对当前类对象加锁,类的实例化对象没有影响(不同的对象)。

变量的线程安全

成员变量和静态变量的线程安全:

(1)如果它们没有共享,则线程安全。
(2)如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:如果只有读操作,则线程安全;如果有读写操作,则这段代码是临界区,需要考虑线程安全。

局部变量的线程安全:

(1)局部变量是线程安全的,因为局部变量存在于方法独有栈帧的局部变量表中,其他线程无法访问。
(2)局部变量引用的对象则未必:如果该对象没有逃离方法的作用访问,它是线程安全的;如果该对象逃离方法的作用范围,需要考虑线程安全。

重量级锁及Monitor

Java 对象头:

普通 Java 对象头共 64 字节,Mark Word32 字节,Klassword(对象所属类型的指针)32 字节。数组对象头共 96 字节,比普通对象头多了 32 字节的 array length。
Mark Word 结构如下:
深入并发编程——共享模型之管程(悲观锁)_第1张图片

Monitor概念:

Monitor 被翻译为监视器或管程,即之前提到的锁。每个 Java 对象都可以关联一个 Monitor 对象,在调用 synchronized 尝试给对象上锁(重量级锁)之后,该对象头的 Mark Word 中就被设置指向Monitor 的指针。Monitor 是操作系统层面的,本身存在很多的 Monitor,初始
时 Monitor 的 Owner 指针都是 null。
深入并发编程——共享模型之管程(悲观锁)_第2张图片
给对象上锁时,若该对象尚未关联 Monitor,该对象的 MarkWord 会指向一个 Monitor 的地址并将 MarkWord 的标志位从 01 改为 10,State 由 Normal 状态变成 HeavyWeight Lock 状态,覆盖前面的 hashcode,分代年龄等信息用所指向的 Monitor 的地址覆盖(注意:Monitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。或者简单说就是重量锁可以存下 identity hash
code)。
线程层面,因为只有 Thread2 在申请锁,所以申请成功,Monitor 的 owner属性指向 Thread2。当 Thread1 使用 synchronized 获取一个对象的锁时先检查该对象是否已经关联某个 Monitor 锁,若已关联 Monitor 则先检查锁关联的Monitor 是否有主人(Owner 是否指向某一线程),若已有主人则将 Thread1 加入 Monitor 的 EntryList 阻塞队列(EntryList 底层是链表结构),进入 BLOCKED状态。
关联同一个对象才会有上述效果(关联同一个对象底层才会由同一个Monitor 处理),不加 synchronized 的对象不会关联监视器,不遵从以上规则。

轻量级锁

Synchronized 是操作系统层面的锁,频繁使用对性能影响比较大,因此出现了轻量级锁和偏向锁。
(1)由于很多情况下没有发生资源的竞争(比如对共享资源的使用是错开的), 而使用重量级锁 Monitor 对性能影响较大,此时适合使用轻量级锁。即如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
(2)轻量级锁对使用者是透明的,语法仍然是 synchronized。
(3)加锁:创建锁记录对象,每个线程的栈帧结构中都包含一个锁记录的结构,内部可以存储锁定对象的 MarkWord。在创建轻量级锁时,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间。
深入并发编程——共享模型之管程(悲观锁)_第3张图片
锁记录中主要存储两部分内容,一是要加锁对象的指针,指向该加锁的对象,二是存储加锁对象的 MarkWord 信息,因为加锁之后对象的 MarkWord 中例如 HashCode,分代年龄等信息都要被覆盖为加锁对象的指针。
深入并发编程——共享模型之管程(悲观锁)_第4张图片
刚创建出来的锁记录对象中存储着锁记录的地址以及标志位 00(表示轻量级锁),创建出来后先将锁记录的 Object reference 指向加锁的对象,并尝试用cas 替换锁记录的地址和对象的 MarkWord。
深入并发编程——共享模型之管程(悲观锁)_第5张图片
如果 cas 替换成功,则对象头中此时存储了锁地址和状态 00,表示由该线程为对象加了锁,而对象 MarkWord 中的信息则存储在了锁记录中,等解锁的时候恢复给对象 MarkWord 中。
当其他线程尝试给同一对象加锁时,检查 MarkWord 的标志位,发现是 00就证明已经被其他线程加过锁了,此时会进入锁膨胀的阶段。
当自己又执行了一遍 synchroized 锁重入,那么虽然标志位是 00,但是锁地址是指向自己的,此时会再添加一条 LockRecord 作为重入的计数,称为锁重入。
解锁:解锁时如果发现有取值为 null 的锁记录,就表示有锁重入,此时重置该锁记录,表示解开了一把重入的锁。当对象指针不为 null 时,此时再尝试使用 cas 将 MarkWord 的值恢复给对象头,如果成功,则解锁成功;如果失败说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程。

锁动态

锁膨胀

(1)如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁,因为后进来的线程不能在干耗着,需要进入一个等待队列,而轻量级锁是没有等待队列的也就无法让后进的线程阻塞。
(2)当 Thread0 已经加上轻量级锁后,Thread1 还想对同一对象加轻量级锁,此时第一步先将锁记录的对象指针指向锁定的对象,之后发现要锁定Object 的 MarkWord 标志位为 00,即表明已被轻量级锁锁定,此时进入锁膨胀。
深入并发编程——共享模型之管程(悲观锁)_第6张图片
(3)先为 Object 申请一个 Monitor 锁,让 Object 对象头的 MarkWord 指向重量级锁的地址,并将其标志位置为 10。然后 Thread1 会进入 Monitor 的EntryList 中阻塞。
深入并发编程——共享模型之管程(悲观锁)_第7张图片
(4)当 Thread0 执行完临界区中代码准备解锁时,尝试使用 cas 将MarkWord 的值恢复给对象头,此时会失败,随即进入重量级锁的解锁流程。即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,并唤醒 EntryList中 Blocked 的线程。此时由于 Thread0 中锁记录还保存着 Object 的 HashCode、分代年龄等信息,还会将其交给 Monitor 存储。

自旋优化

(1)重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。因为阻塞会发生上下文切换影响性能。
(2)自旋优化要使用 CPU,适合多核 CPU。若单核 CPU 自旋没有意义。
(3)在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
(4)Java 7 之后不能控制是否开启自旋功能。

锁消除

JIT 即时编译器,Java 是解释+编译的运行模式,对 Java 字节码解释执行,但是对其中一些热点代码(反复执行的),会使用 JIT 进行优化,其中一个手段就是去分析局部变量是否能优化。若局部变量经过分析不会逃离方法的作用范围,就代表这个变量不可能被共享,此时对其加锁就没有任何意义,所以 JIT 即使编译器就会将 synchorized 优化掉,真正执行的时候就不会有加锁操作了。

偏向锁

(1)轻量级锁缺点:轻量级锁在没有竞争时(只有自己线程在使用临界资源),每次锁重入依然要执行 cas 操作。
(2)只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
(3)偏向锁和正常状态的状态位都是 01,不同的是 biased_lock 置为 1。即偏向锁的后三位为 101,正常状态的后三位为 001。
(4)偏向锁默认是延迟的,不会在 Java 程序启动时立即生效,而是会延迟两三秒后生效,即程序一启动就打印某一刚创建的对象头后三位是 001,而等几秒后新建对象并打印对象头,其后三位就为 101 了。如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。
(5)对于偏向的加锁状态,前 54 位为加锁线程的 ID,覆盖了正常MarkWord 的 unused 位和 hashcode 位。
(6)即使加了偏向锁的对象后来解锁了,对象的 MarkWord 也不会变化,因为目前为止并没有线程来竞争资源,偏向锁就默认该共享资源只被该线程使用。
(7)偏向锁适合的场景是冲突很少的场景,若项目的场景是高并发竞争很多就不适合使用偏向锁,可以添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁。

撤销对象的可偏向状态情况

(1)在使用某一个对象的偏向锁之前先调用该对象的 hashCode 方法,就会禁用该对象的偏向锁。(hashCode 默认是 0,只有第一次调用这个对象的 hashCode 时才会产生并写入对象头中,此时MarkWord 的空间不够再存储偏向锁所偏向的线程信息了,所以偏向锁就会效)。
(2)其它线程使用该对象使偏向锁升级为轻量级锁(注意此时的条件是有多个线程使用共享资源但是没有产生竞争,即虽然多个线程访问了共享资源但是使用时间是叫错开的,如果没有交错开发生了竞争则会直接进入重量级锁阶段,因为竞争的线程需要队列阻塞),即Thread0 当给一个对象加了偏向锁后,Thread1 在其运行结束使用共享资源,就会将偏向状态改为不可偏向,MarkWord 后三位由 101 设为 001,前面的偏向线程信息置为 0。
(3)使用了 wait/notify,因为这个机制只有重量级锁才有。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争发生,即刚刚所述的第二种撤销情况,且撤销偏向对性能的损耗不小。所以当撤销偏向锁的阈值超过 20 次以后,jvm 会觉得之前偏向错了,于是在给这些对象加锁时重新偏向至新加锁的线程。

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,
新建的对象也是不可偏向的。注意作用范围是整个类。

你可能感兴趣的:(深入并发编程,java,jvm,面试)