java并发 day02 临界区和竞态条件synchronized线程安全 对象头 Monitor管程 wait notifypark&unpark ReentrantLock

Posted halulu.me

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发 day02 临界区和竞态条件synchronized线程安全 对象头 Monitor管程 wait notifypark&unpark ReentrantLock相关的知识,希望对你有一定的参考价值。

临界区和竞态条件

临界区 Critical Section

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

为了避免临界区的竞态条件发生,有多种手段可以达到目的。
1、阻塞式的解决方案:synchronized,Lock

2、非阻塞式的解决方案:原子变量

synchronized

在这里插入图片描述

synchronized 实际上是使用对象锁保证了临界区代码的原子性,临界区的代码对外是不可分割的部分,不会被线程的上下文切换所打断。
有2个线程,1个加锁(synchronized),另一个不加锁是否可行?
不行,上下文切换到不加锁的线程的时候,依旧可以对共享数据进行修改,无法保证数据的可见性。

在这里插入图片描述

在这里插入图片描述

synchronized的底层原理

在这里插入图片描述

1、synchronized的底层涉及java的Mark Word和monitor两个方面。
2、从字节码的角度上看,当字节码指令执行到monitorenter的时候,也就是执行到synchronized加锁的时候,锁对象的Mark Word会指向monitor。
3、如果monitor的owner区没有线程则会占有owner区,反之会进入EntryList进入阻塞状态。
4、当线程占有owner区,并且字节码指令执行到monitorexit的时候,执行线程会退出owner区并唤醒EntryList的线程。
5、当临界区代码发生异常的时候,也会执行monitorexit释放锁的字节码指令,保证发生异常的时候锁一定会被释放。

线程安全

变量的线程安全问题

成员变量和静态变量是否线程安全?

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

局部变量是否线程安全?

局部变量是线程安全的
但局部变量引用的对象则未必
----如果该对象没有逃离方法的作用访问,它是线程安全的
----如果该对象逃离方法的作用范围,需要考虑线程安全

在这里插入图片描述

在这里插入图片描述

局部变量引用的对象是共享的,所以需要考虑线程安全问题。

在这里插入图片描述
在这里插入图片描述

list 是局部变量,每个线程调用时会创建其不同实例,没有共享

常见线程安全类

常见线程安全类

1、String
2、Integer
3、StringBuffer
4、Random
5、Vector
6、Hashtable
7、java.util.concurrent 包下的类(JUC)

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为它们的每个方法是原子的。

但注意它们多个方法的组合不是原子的,见后面分析

在这里插入图片描述

这并不是线程安全的,因为foo()方法需要被子类重写,它的因为是不固定的,可能会导致不安全的发生,这种方法被称之为外星方法。解决方法,使用final修饰

String类为什么要使用final修饰?

保证String类中的方法行为不会被子类重写,以此来保证线程安全的问题。体现的是一种闭合原则。

对象头

java对象由两部分组成,java对象头+实例数据+对齐填充字节
java对象头 = Mark Word + Klass Word (+Array Length)
实例数据:java代码中能够看到的属性和他们的值
对齐填充字节:JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数

java的对象头(32位的虚拟机)

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(64bits)                                               |
|-----------------------------------------------------------------------------------------------------------------|
|                       Mark Word(32bits)                           |  Klass Word(32bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|     hashcode:25                      | age:4 | biased_lock:0 | 01 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
|     thread:23              | epoch:2 | age:4 | biased_lock:1 | 01 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_lock_record:30                                    | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|     ptr_to_heavyweight_monitor:30                            | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|


java的对象头(64位的虚拟机)

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(128bits)                                              |
|-----------------------------------------------------------------------------------------------------------------|
|                                   Mark Word(64bits)               |  Klass Word(64bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|    unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:0| 01 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
|    thread:54|      epoch:2       |unused:1|age:4|biase_lock:1| 01 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|                        ptr_to_lock_record:62                 | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                       ptr_to_heavyweight_monitor:62          | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|


数组对象
在这里插入图片描述

32位和64位的数组对象的长度都是4个字节。

数据解读

Klass Word指向了对象所存储的class,是指向类的指针。
Mark Word 存储了hashCode和锁信息。
age:垃圾回收的分代年龄
biased_block:偏向锁的状态

在这里插入图片描述

Monitor管程

Monitor :监视器或管程。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
不加 synchronized 的对象不会关联监视器,不遵从以上规则
monitor是操作系统提供的

Monitor 结构

在这里插入图片描述

1、当使用synchronized同步锁的时候,Mark Word会指向monitor
2、刚开始 Monitor 中 Owner 为 null ,当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
3、在 Thread-2上锁的过程中,如果 Thread-1,Thread-3 也来执行 synchronized(obj),就会进入EntryList BLOCKED
4、Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
5、WaitSet 是之前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify)

synchronized的优化

monitor是由操作系统提供的,成本比较高,对程序运行的性能是有影响的。

1、轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized

在这里插入图片描述

CAS交换成功,对象头存储了锁记录地址和状态00(轻量级锁),表示有该线程给对象加锁。

在这里插入图片描述

CAS交换失败:
1、如果是其它线程占有了锁对象,这时表明有竞争,进入锁膨胀状态升级为重量级锁
2、如果是相同的锁对象,则会执行synchronized锁重入,同时增加一条Lock Record作为锁重入的计数(每次锁重入都会执行CAS)

在这里插入图片描述

2、重量级锁

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

在这里插入图片描述

Thread1进行轻量级加锁时,Thread0已经占有了锁对象。这时会进入锁膨胀状态:
1、即为锁对象申请monitor,让锁对象执行重量级锁的地址
2、进入Monitor的EntryList阻塞区

在这里插入图片描述

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

3、自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
Java 7 之后不能控制是否开启自旋功能

4、偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用CAS 将线程 ID 设置到对象的 Mark Word 头,之后发生这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
使用线程ID替换Mark Word(偏向当前线程)

在这里插入图片描述

偏向锁状态

在这里插入图片描述

1、如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
2、偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
3、如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

4、开启偏向锁的时候,对象头并不会保存hashCode。所以,如果要获取hashCode,则必须得撤销偏向锁。
5、撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW),所以撤销偏向锁是很耗性能的。
6、在调用hashCode时记得使用 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode

撤销偏向锁

1、调用hashCode自动撤销偏向锁
2、当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
3、调用wait/notify会撤销偏向锁(重量级锁)
4、撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
5、偏向锁的撤销需要等待全局安全点(safe point),暂停持有偏向锁的线程,检查持有偏向锁的线程状态。首先遍历当前JVM的所有线程,如果能找到偏向线程,则说明偏向的线程还存活,此时检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁。可以看出撤销偏向锁的时候会导致stop the word。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID(撤销偏向锁)
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程t2

批量撤销

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

5、锁消除

JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。(JIT即时编译器的优化)

6、锁粗化

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度。(JIT即时编译器的优化)

wait notify原理

在这里插入图片描述

1、Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态 并释放锁
2、BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
3、BLOCKED 线程会在 Owner 线程释放锁时唤醒
4、WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList重新竞争

api
在这里插入图片描述

它们都是线程之间进行协作的手段,都属于 Object对象的方法。必须获得此对象的锁,才能调用这几个方法。(必须使用synchronized获取锁对象,再通过锁对象调用相关方法,重量级锁)

sleep(long n) 和 wait(long n) 的区别

1、 sleep 是 Thread 方法,而 wait 是 Object 的方法
2、 sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
3、sleep 在睡眠的同时,不会释放对象锁的,但 wait在等待的时候会释放对象锁
4、 它们状态 TIMED_WAITING

wait-notify的正确使用
在这里插入图片描述

park&unpark

在这里插入图片描述

与 Object 的 wait & notify 相比

1、wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
2、park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
3、park & unpark 可以先 unpark,而 wait & notify 不能先 notify

每个线程都有一个parker对象,由三部分组成_counter,_cond和_mutex

调用park
java并发 day02 临界区和竞态条件synchronized线程安全 对象头 Monitor管程 wait notifypark&unpark ReentrantLock

竞态条件和临界区

15 同步于互斥 并发竞态和编译乱序执行乱序

JUC并发编程 -- 避免临界区的竞态条件之synchronized 解决方案(同步代码块)

内核的并发和竞态(信号量completion自旋锁)

JUC并发编程 -- 共享代来的问题(操作统一资源) & 临界区 Critical Section & 竞态条件 Race Condition