【并发编程】synchronized原理

一。java对象头的组成

  1.普通对象:

 

 

   2.数组对象

 

 

   3.markword(32位)结构

 

 

   4.markword(64位)结构

 

 

 二。monitor概念

  每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

  在monitor中的owner指向该线程。

  刚开始 Monitor 中 Owner 为 null

  当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner

  在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED

  Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

  图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程。

 

 

   注意,当在sync过程中,如果出现了异常,如何解决monitor依然被锁的情况。

  答案是在字节码层面,会监控其方法区,当出现异常时,自动换回monitor中的线程号。

三。轻量锁优化

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

  轻量级锁对使用者是透明的,即语法仍然是 synchronized

  创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

 

 

   让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录。

 

 

   如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

 

 

   如果 cas 失败,有两种情况

    如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

    如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

 

 

 

   当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一

 

 

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

 

 

   3.自旋优化

  重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞。

  4.偏向锁

  轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。  

  Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

  - XX:BiasedLockingStartupDelay=0  禁用延迟

  -XX:-UseBiasedLocking 禁用偏向锁

  撤销偏向的三种情况:

    调用对象 hashCode

    其它线程使用对象

    调用 wait/notify

  批量重偏向

  如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID

  当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程

  批量撤销

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

四。wait-notify

  Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态   

  BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片

  BLOCKED 线程会在 Owner 线程释放锁时唤醒

  WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争。

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

   1) sleep 是 Thread 方法,而 wait 是 Object 的方法

  2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用

  3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁

  4) 它们 状态 TIMED_WAITING

五。join原理

  是调用者轮询检查线程 alive 状态

  如下:

synchronized (t1) {
 // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
 while (t1.isAlive()) {
 t1.wait(0);
 }
}

六。park unpark原理

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

 

   1. 当前线程调用 Unsafe.park() 方法

  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁

  3. 线程进入 _cond 条件变量阻塞

  4. 设置 _counter = 0

 

   1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

  2. 唤醒 _cond 条件变量中的 Thread_0

  3. Thread_0 恢复运行

  4. 设置 _counter 为 0

 

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

  2. 当前线程调用 Unsafe.park() 方法

  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行

  4. 设置 _counter 为 0

 

posted on 2022-05-14 16:57  一只萌萌哒的提莫  阅读(96)  评论(0编辑  收藏  举报