【线程】线程八锁与Synchronzied内部原理(十二)

2024-03-01 21:18

本文主要是介绍【线程】线程八锁与Synchronzied内部原理(十二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我的原则:先会用再说,内部慢慢来

文章目录

      • 一、线程八锁
      • 二、场景分析
        • 1. 两个普通同步方法,两个线程,标准打印, 打印? //one two
        • 2. 新增 Thread.sleep() 给 getOne() ,打印? //one two
        • 3. 新增普通方法 getThree() , 打印? //three one two
        • 4. 两个普通同步方法,两个 Number 对象,打印? //two one
        • 5. 修改 getOne() 为静态同步方法,打印? //two one (重点)
        • 6. 修改两个方法均为静态同步方法,一个 Number 对象? //one two
        • 7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象? //two one 对比5 (重点)
        • 8. 两个静态同步方法,两个 Number 对象? //one two 对比6
        • 9. === 场景结论 ===
      • 三、代码反编译 javap
      • 四、Java对象头与Monitor
      • 五、synchronized 底层分析
        • 5.1 代码块 demo
        • 5.2 monitorenter 源码
          • 1. 偏向锁获取
          • 2.偏向锁撤销和升级
      • 六、锁优化
      • 七、番外篇


一、线程八锁

题目:判断打印的 “one” or “two” ?

  1. 两个普通同步方法,两个线程,标准打印, 打印? //one two
  2. 新增 Thread.sleep() 给 getOne() ,打印? //one two
  3. 新增普通方法 getThree() , 打印? //three one two
  4. 两个普通同步方法,两个 Number 对象,打印? //two one
  5. 修改 getOne() 为静态同步方法,打印? //two one (重点)
  6. 修改两个方法均为静态同步方法,一个 Number 对象? //one two
  7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象? //two one (重点) 对比5
  8. 两个静态同步方法,两个 Number 对象? //one two 对比6

=== 点击查看top目录 ===

二、场景分析

1. 两个普通同步方法,两个线程,标准打印, 打印? //one two

代码:

public class _15_01_TestSynchronized {public static void main(String[] args) throws Exception{Number01 number = new Number01();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number.getTwo(),"B").start();}
}class Number01{public synchronized void getOne(){System.out.println(Thread.currentThread().getName() + "_one");}public  synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}
}

输出:

A_one
B_two

=== 点击查看top目录 ===

2. 新增 Thread.sleep() 给 getOne() ,打印? //one two

代码:

public class _15_02_TestSynchronized {public static void main(String[] args) throws Exception{Number02 number = new Number02();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number.getTwo(),"B").start();}
}
class Number02{public synchronized void getOne(){try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "_one");}public  synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}
}

输出:

(停顿3秒)
A_one
B_two

=== 点击查看top目录 ===

3. 新增普通方法 getThree() , 打印? //three one two

代码:

public class _15_03_TestSynchronized {public static void main(String[] args) throws Exception{Number03 number = new Number03();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number.getTwo(),"B").start();new Thread(() -> number.getThree(),"C").start();}
}class Number03{public synchronized void getOne(){try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "_one");}public synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}public void getThree(){System.out.println(Thread.currentThread().getName() + "_Three");}
}

输出:

C_Three
(停顿3秒)
A_one
B_two

=== 点击查看top目录 ===

4. 两个普通同步方法,两个 Number 对象,打印? //two one
public class _15_04_TestSynchronized {public static void main(String[] args) throws Exception{Number04 number = new Number04();Number04 number2 = new Number04();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number2.getTwo(),"B").start();}
}class Number04{public synchronized void getOne(){try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "_one");}public  synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}
}

输出:

B_two
(停顿3秒)
A_one

=== 点击查看top目录 ===

5. 修改 getOne() 为静态同步方法,打印? //two one (重点)

代码:

public class _15_05_TestSynchronized {public static void main(String[] args) throws Exception{Number05 number = new Number05();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number.getTwo(),"B").start();}
}class Number05{public static synchronized void getOne(){try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "_one");}public synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}
}

输出:

B_two
(停顿3秒)
A_one

=== 点击查看top目录 ===

6. 修改两个方法均为静态同步方法,一个 Number 对象? //one two
public class _15_06_TestSynchronized {public static void main(String[] args) throws Exception{Number06 number = new Number06();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number.getTwo(),"B").start();}
}class Number06{public static synchronized void getOne(){try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "_one");}public static synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}
}

输出:

(停顿3秒)
A_one
B_two

=== 点击查看top目录 ===

7. 一个静态同步方法,一个非静态同步方法,两个 Number 对象? //two one 对比5 (重点)

代码:

public class _15_07_TestSynchronized {public static void main(String[] args) throws Exception{Number07 number = new Number07();Number07 number2 = new Number07();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number2.getTwo(),"B").start();}
}class Number07{public static synchronized void getOne(){try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "_one");}public synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}
}

输出:

B_two
(停顿3秒)
A_one

=== 点击查看top目录 ===

8. 两个静态同步方法,两个 Number 对象? //one two 对比6

代码:

public class _15_08_TestSynchronized {public static void main(String[] args) throws Exception{Number08 number = new Number08();Number08 number2 = new Number08();new Thread(() -> number.getOne(),"A").start();Thread.sleep(50);new Thread(() -> number2.getTwo(),"B").start();}
}class Number08{public static synchronized void getOne(){try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "_one");}public static synchronized void getTwo(){//thisSystem.out.println(Thread.currentThread().getName() + "_two");}
}

输出:

(停顿3秒)
A_one
B_two

=== 点击查看top目录 ===

9. === 场景结论 ===

一、锁类型:(对象锁和类锁不互斥)

  1. static 类锁 (所有的静态同步方法用的也是同一把锁 – 类对象本身)
  2. this 对象锁 (所有的非静态同步方法用的都是同一把锁 – 实例对象本身)

二、this对象锁
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能等待。(但是调用static的 synchronized方法不阻塞)

三、static 类锁
同一个类里面有多个 static synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个static synchronized方法了,其他的线程调用static synchronized 都只能等待。(但是调用普通的 synchronized方法不阻塞)

四、普通方法与同步锁无关。(各走个的)
=== 点击查看top目录 ===

三、代码反编译 javap

javap的用法格式:
javap
其中classes就是你要反编译的class文件。
在命令行中直接输入javap或javap -help可以看到javap的options有如下选项:

-help  --help  -?        输出此用法消息-version                 版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。-v  -verbose             输出附加信息(包括行号、本地变量表,反汇编等详细信息)-l                         输出行号和本地变量表-public                    仅显示公共类和成员-protected               显示受保护的/公共类和成员-package                 显示程序包/受保护的/公共类 和成员 (默认)-p  -private             显示所有类和成员-c                       对代码进行反汇编-s                       输出内部类型签名-sysinfo                 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)-constants               显示静态最终常量-classpath <path>        指定查找用户类文件的位置-bootclasspath <path>    覆盖引导类文件的位置

一般常用的是-v -l -c三个选项。
javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码。

=== 点击查看top目录 ===

四、Java对象头与Monitor

  • 在hotspot虚拟机中,对象在内存中存储的布局可以分为3个区域:对象头(Header)、实例数据(Instance data)和对象填充(Padding)。

    • 对象头(Header)包括两部分:

      • 1、存储对象自身的运行时数据–Mark Word (如hashCode,GC分代年龄,锁状态标志,对象持有的锁,偏向线程ID,偏向时间戳)。

      • 2、类型指针。虚拟机通过指针来确定这个对象是哪个类的实例。但并不是虚拟机实现都必须在对象类型上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身。

    • 实例数据 (Instance data):对象真正存储的有效信息。记录代码中无论是父类还是子类定义的各种类型的字段内容。记录的存储顺序受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在JAVA源码中的定义顺序的影响。HotSpot虚拟机默认的分配策略为:longs/doubles,ints,shorts/chars,bytes/booleans,oop(Ordinary Object Pointers)。在这前提下,父类定义的变量会出现在子类之前。如果CompactFields 参数为true(默认为true),那么子类之中较窄的变量可能会插入到父类变量的空隙之中。

    • 对象填充(Padding): 并非必须存在,仅仅起占位符的作用,由于HotSpot VM 的自动内存管理要求对象起始地址必须是8字节的整体倍,换句话说,就是对象的大小必须是 8字节的整体倍。对象头正好是8的倍数,那么如果实例部分凑不上8字节的整体倍的话,就要来填充啦。

在这里插入图片描述
对象 = 对象头 + 数据部分。
对象头 = Mark Word + Class 指针。

HotSpot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。

考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下。

  1. 无锁状态 01
锁状态25bit4bit1bit 是否是偏向锁2bit 锁标志位
无锁状态对象HashCode对象分代年龄001

markOop.hpp

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
  1. 其他状态(轻量级锁定00、重量级锁定10、GC标记11、可偏向01)
    在这里插入图片描述
锁状态30bit 锁内容2bit 锁标志位
轻量级锁指向锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
GC标记11
可偏向锁偏向线程ID(23bit)、偏向时间戳(2bit)、分代年龄(4bit)、是否偏向锁(1bit)01
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time
  • 无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

在这里插入图片描述

=== 点击查看top目录 ===

五、synchronized 底层分析

  1. Synchronzied 三种使用方法:
  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前 Instance 的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前 Class 的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
5.1 代码块 demo

TestBlock.java

public class TestBlock {public void test01(){synchronized(this){int a = 1;}}
}

javac TestBlock.java

反编译看一下

javap -c -l -v TestBlock

Classfile /Users/momo/Downloads/TestBlock.classLast modified Sep 14, 2019; size 358 bytesMD5 checksum 5c8519109a71ce4098170f7224fc8b90Compiled from "TestBlock.java"
public class TestBlockminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #3.#15         // java/lang/Object."<init>":()V#2 = Class              #16            // TestBlock#3 = Class              #17            // java/lang/Object#4 = Utf8               <init>#5 = Utf8               ()V#6 = Utf8               Code#7 = Utf8               LineNumberTable#8 = Utf8               test01#9 = Utf8               StackMapTable#10 = Class              #16            // TestBlock#11 = Class              #17            // java/lang/Object#12 = Class              #18            // java/lang/Throwable#13 = Utf8               SourceFile#14 = Utf8               TestBlock.java#15 = NameAndType        #4:#5          // "<init>":()V#16 = Utf8               TestBlock#17 = Utf8               java/lang/Object#18 = Utf8               java/lang/Throwable
{public TestBlock();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 2: 0public void test01();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=4, args_size=10: aload_01: dup2: astore_13: monitorenter  // 注意!!! 进入同步方法4: iconst_15: istore_26: aload_17: monitorexit   // 注意!!! 退出同步方法8: goto          16 // 能跑到这里说明程序正常运行,goto到16进行return11: astore_312: aload_113: monitorexit  // 注意!!! 退出同步方法,跑到这里说明抛异常了。14: aload_315: athrow16: returnException table:from    to  target type4     8    11   any11    14    11   anyLineNumberTable:line 4: 0line 5: 4line 6: 6line 7: 16StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame */offset_delta = 11locals = [ class TestBlock, class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 /* chop */offset_delta = 4
}
SourceFile: "TestBlock.java"

这里我们看一下关键代码:

         3: monitorenter  // 注意!!! 进入同步方法
...7: monitorexit   // 注意!!! 退出同步方法8: goto          16 // 能跑到这里说明程序正常运行,goto到16进行return
...13: monitorexit  // 注意!!! 退出同步方法,跑到这里说明抛异常了。

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。

=== 点击查看top目录 ===

5.2 monitorenter 源码
1. 偏向锁获取
  1. 下面开始偏向锁获取流程分析,代码在 bytecodeInterpreter.cpp#1816。注意本文代码都有所删减。
CASE(_monitorenter): {// lockee 就是锁对象oop lockee = STACK_OBJECT(-1);// derefing's lockee ought to provoke implicit null checkCHECK_NULL(lockee);// code 1:找到一个空闲的Lock RecordBasicObjectLock* limit = istate->monitor_base();BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();BasicObjectLock* entry = NULL;while (most_recent != limit ) {if (most_recent->obj() == NULL) entry = most_recent;else if (most_recent->obj() == lockee) break;most_recent++;}//entry不为null,代表还有空闲的Lock Recordif (entry != NULL) {// code 2:将Lock Record的obj指针指向锁对象entry->set_obj(lockee);int success = false;uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;// markoop即对象头的mark wordmarkOop mark = lockee->mark();intptr_t hash = (intptr_t) markOopDesc::no_hash;// code 3:如果锁对象的mark word的状态是偏向模式if (mark->has_bias_pattern()) {uintptr_t thread_ident;uintptr_t anticipated_bias_locking_value;thread_ident = (uintptr_t)istate->thread();// code 4:这里有几步操作,下文分析anticipated_bias_locking_value =(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &~((uintptr_t) markOopDesc::age_mask_in_place);// code 5:如果偏向的线程是自己且epoch等于class的epochif  (anticipated_bias_locking_value == 0) {// already biased towards this thread, nothing to doif (PrintBiasedLockingStatistics) {(* BiasedLocking::biased_lock_entry_count_addr())++;}success = true;}// code 6:如果偏向模式关闭,则尝试撤销偏向锁else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {markOop header = lockee->klass()->prototype_header();if (hash != markOopDesc::no_hash) {header = header->copy_set_hash(hash);}// 利用CAS操作将mark word替换为class中的mark wordif (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {if (PrintBiasedLockingStatistics)(*BiasedLocking::revoked_lock_entry_count_addr())++;}}// code 7:如果epoch不等于class中的epoch,则尝试重偏向else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {// 构造一个偏向当前线程的mark wordmarkOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);if (hash != markOopDesc::no_hash) {new_header = new_header->copy_set_hash(hash);}// CAS替换对象头的mark word  if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {if (PrintBiasedLockingStatistics)(* BiasedLocking::rebiased_lock_entry_count_addr())++;}else {// 重偏向失败,代表存在多线程竞争,则调用monitorenter方法进行锁升级CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);}success = true;}else {// 走到这里说明当前要么偏向别的线程,要么是匿名偏向(即没有偏向任何线程)// code 8:下面构建一个匿名偏向的mark word,尝试用CAS指令替换掉锁对象的mark wordmarkOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |(uintptr_t)markOopDesc::age_mask_in_place |epoch_mask_in_place));if (hash != markOopDesc::no_hash) {header = header->copy_set_hash(hash);}markOop new_header = (markOop) ((uintptr_t) header | thread_ident);// debugging hintDEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {// CAS修改成功if (PrintBiasedLockingStatistics)(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;}else {// 如果修改失败说明存在多线程竞争,所以进入monitorenter方法CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);}success = true;}}// 如果偏向线程不是当前线程或没有开启偏向模式等原因都会导致success==falseif (!success) {// 轻量级锁的逻辑//code 9: 构造一个无锁状态的Displaced Mark Word,并将Lock Record的lock指向它markOop displaced = lockee->mark()->set_unlocked();entry->lock()->set_displaced_header(displaced);//如果指定了-XX:+UseHeavyMonitors,则call_vm=true,代表禁用偏向锁和轻量级锁bool call_vm = UseHeavyMonitors;// 利用CAS将对象头的mark word替换为指向Lock Record的指针if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {// 判断是不是锁重入if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {		//code 10: 如果是锁重入,则直接将Displaced Mark Word设置为nullentry->lock()->set_displaced_header(NULL);} else {CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);}}}UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);} else {// lock record不够,重新执行istate->set_msg(more_monitors);UPDATE_PC_AND_RETURN(0); // Re-execute}
}

以上是偏向锁加锁的流程(包括部分轻量级锁的加锁流程),如果当前锁已偏向其他线程||epoch值过期||偏向模式关闭||获取偏向锁的过程中存在并发冲突,都会进入到InterpreterRuntime::monitorenter方法, 在该方法中会对偏向锁撤销和升级。

2.偏向锁撤销和升级

查看 interpreterRuntime.cpp ,内部的 InterpreterRuntime::monitorenter 方法:

翻译: BiasedLock -> 偏向锁

//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERTthread->last_frame().interpreter_frame_verify_monitor(elem);
#endifif (PrintBiasedLockingStatistics) {Atomic::inc(BiasedLocking::slow_path_entry_count_addr());}Handle h_obj(thread, elem->obj());assert(Universe::heap()->is_in_reserved_or_null(h_obj()),"must be NULL or an object");if (UseBiasedLocking) { //重点看这里!是否使用偏向锁// Retry fast entry if bias is revoked to avoid unnecessary inflationObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);} else {ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);}assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),"must be NULL or an object");
#ifdef ASSERTthread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
  • 看下 Input 入参
    JavaThread : 指向java中的当前线程
    BasicObjectLock :
class BasicObjectLock {BasicLock _lock; // object holds the lock;oop  _obj;   
}
class BasicLock {//主要用来保存_obj指向Object对象的对象头数据volatile markOop _displaced_header;
}
  • 看下关键代码
if (UseBiasedLocking) { //重点看这里!是否使用偏向锁// Retry fast entry if bias is revoked to avoid unnecessary inflationObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);} else {ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);}
  1. 看下判断条件 UseBiasedLocking
    UseBiasedLocking标识虚拟机是否开启偏向锁功能,如果开启则执行fast_enter逻辑,否则执行slow_enter;

  2. fast_enter
    ObjectSynchronizer::fast_enter位于 synchronizer.cpp 文件内部。

 void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {if (UseBiasedLocking) {if (!SafepointSynchronize::is_at_safepoint()) {BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {return;}} else {assert(!attempt_rebias, "can not rebias toward VM thread");BiasedLocking::revoke_at_safepoint(obj);}assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");}

如果不在安全点上,我们就进入偏向。我们从revoke_and_rebias(翻译:撤销或者重偏向)这个名字可以看出,偏向锁就是在没有多线程竞争的情况下,减少不必要的锁执行路径,只是更新了一下线程Id(撤销一个,偏向另外一个)。

备注:JVM中的每个类也有一个类似mark word的prototype_header,用来标记该class的epoch和偏向开关等信息。上面的代码中lockee->klass()->prototype_header()即获取class的prototype_header。

  • 看下 revoke_and_rebias 方法
    位于:biasedLocking.cpp
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");// ===== 1、通过markOop mark = obj->mark()获取对象的markOop数据mark,即对象头的Mark WordmarkOop mark = obj->mark();// ===== 2、判断mark是否为可偏向状态,即mark的偏向锁标志位为 1,锁标志位为 01;if (mark->is_biased_anonymously() && !attempt_rebias) {//如果是匿名偏向且attempt_rebias==false会走到这里,如锁对象的hashcode方法被调用会出现这种情况,需要撤销偏向锁。markOop biased_value       = mark;//prootype本身构建的是 markOop( no_hash_in_place | no_lock_in_place );markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());//执行CAS,如果当前对象的mark没有变更,就换成 unbiased_prototypemarkOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);if (res_mark == biased_value) {//如果之前的和现在的一样,说明撤销成功,BIAS_REVOKED本身是一个枚举  return BIAS_REVOKED;}} else if (mark->has_bias_pattern()) { // ===== 4、锁对象开启了偏向模式会走到这里//已经被线程偏向了,获取Klass对象,即类本身的头,obj则是它的实例 Klass* k = Klass::cast(obj->klass());markOop prototype_header = k->prototype_header();if (!prototype_header->has_bias_pattern()) {//code 1: 如果对应class关闭了偏向模式//直接设置成已经撤销偏向即可 markOop biased_value       = mark;markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");return BIAS_REVOKED;} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {     //code2: 如果epoch过期//实例的epoch和类本身的epoch值不一样,说明它已经过期,也就是说这个对象当前处于未偏向但是可偏向的状态(rebiasable)   if (attempt_rebias) {//执行rebias     wait希望直接撤销assert(THREAD->is_Java_thread(), "");markOop biased_value       = mark;markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);if (res_mark == biased_value) { //当前线程抢到了这个对象的偏向          return BIAS_REVOKED_AND_REBIASED;}} else {markOop biased_value       = mark;markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());//CAS撤销偏向锁markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);if (res_mark == biased_value) {//撤销了偏向return BIAS_REVOKED;}}}}//===== 5:没有执行偏向,通过启发式的方式决定到底是执行撤销还是执行rebiasHeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias);if (heuristics == HR_NOT_BIASED) {  //5.1:偏向状态改成了不需要偏向return NOT_BIASED; //不可偏向直接返回} else if (heuristics == HR_SINGLE_REVOKE) {//5.2:启发式决定执行单次的撤销Klass *k = Klass::cast(obj->klass());markOop prototype_header = k->prototype_header();if (mark->biased_locker() == THREAD &&prototype_header->bias_epoch() == mark->bias_epoch()) {// 走到这里说明需要撤销的是偏向当前线程的锁,当调用Object#hashcode方法时会走到这一步// 因为只要遍历当前线程的栈就好了,所以不需要等到safepoint再撤销。ResourceMark rm;if (TraceBiasedLocking) {tty->print_cr("Revoking bias by walking my own stack:");}BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD);((JavaThread*) THREAD)->set_cached_monitor_info(NULL);assert(cond == BIAS_REVOKED, "why not?");return cond;} else {// 下面代码最终会在VM线程中的safepoint调用revoke_bias方法VM_RevokeBias revoke(&obj, (JavaThread*) THREAD);VMThread::execute(&revoke);return revoke.status_code();}}assert((heuristics == HR_BULK_REVOKE) ||(heuristics == HR_BULK_REBIAS), "?");//6:等到虚拟机运行到safepoint,实际就是执行  VM_BulkRevokeBias 的doit的 bulk_revoke_or_rebias_at_safepoint方法//批量撤销、批量重偏向的逻辑VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD,(heuristics == HR_BULK_REBIAS),attempt_rebias);VMThread::execute(&bulk_revoke);return bulk_revoke.status_code();
}
  • 看下 is_biased_anonymously 方法

is_biased_anonymously方法判断是否为可偏向状态,mark的偏向锁标志位为1,锁标识位为01。
具体分析如下:
位于:markOop.hpp

  bool is_biased_anonymously() const {return (has_bias_pattern() && (biased_locker() == NULL));}
  • 看下 has_bias_pattern 方法
    位于:markOop.hpp
bool has_bias_pattern() const {// //这里后面的 biased_lock_pattern值是5,二级制101。(mark的偏向锁标志位为1,锁标识位为01。)return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern);
}
  • biased_lock_pattern 的值
    位于:markOop.hpp
enum { locked_value             = 0,unlocked_value           = 1,monitor_value            = 2,marked_value             = 3,biased_lock_pattern      = 5};
  1. slow_enter
    ObjectSynchronizer::slow_enter 位于 synchronizer.cpp 文件内部。
// -----------------------------------------------------------------------------
// Interpreter/Compiler Slow Case
// This routine is used to handle interpreter/compiler slow case
// We don't need to use fast path here, because it must have been
// failed in the interpreter/compiler code.
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {markOop mark = obj->mark();assert(!mark->has_bias_pattern(), "should not see bias pattern here");if (mark->is_neutral()) {// Anticipate successful CAS -- the ST of the displaced mark must// be visible <= the ST performed by the CAS.lock->set_displaced_header(mark);if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {TEVENT (slow_enter: release stacklock) ;return ;}// Fall through to inflate() ...} elseif (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {assert(lock != mark->locker(), "must not re-lock the same lock");assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");lock->set_displaced_header(NULL);return;}

总结一下:
偏向锁获取的过程如下,当锁对象第一次被线程获取的时候,虚拟机把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中的偏向线程ID,并将是否偏向锁的状态位置置为1。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,直接检查ThreadId是否和自身线程Id一致,
如果一致,则认为当前线程已经获取了锁,虚拟机就可以不再进行任何同步操作(例如Locking、Unlocking及对Mark Word的Update等)。
  其实一般来说偏向锁很少又说去主动释放的,因为只有在其他线程需要获取锁的时候,也就是这个锁不仅仅被一个线程使用,可能有两个线程交替使用,根据对象是否被锁定来决定释放锁(恢复到未锁定状态)还是升级到轻量锁状态。

=== 点击查看top目录 ===

六、锁优化

在这里插入图片描述

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段,这里并不打算深入到每个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每个锁的核心优化思想,毕竟涉及到具体过程比较繁琐,如需了解详细过程可以查阅《深入理解Java虚拟机原理》。

  1. 偏向锁
    偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

tip: 作者加注:公司有一部电梯,只供CEO或者特殊情况应急用,那么就满足上面的在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。

  1. 轻量级锁
    倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”(我用完的时候,别人才会刚好过来拿),注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

tip: 作者加注:在富士康工厂的三班倒状态下,每一台机器,都会轮流给不同的工人使用,所以在整个同步周期内不存在竞争。

在这里插入图片描述
在这里插入图片描述
3. 自旋锁 (耗CPU)
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

因为许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

tip: 作者加注:一层楼有一个厕所,厕所有5个坑,坑被占据了之后,上厕所的人就会先跑去写代码,然后写一阵子又跑回来找坑,跑来跑去循环跑。

  1. 锁消除
    消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

demo1:

public void elimination01(){Object object = new Object();synchronized (object){System.out.println("hello world ...");}
}
// 执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。
public void elimination02(){Object object = new Object();System.out.println("hello world ...");
}

demo2:

public void elimination03(){StringBuffer stringBuffer = new StringBuffer();stringBuffer.append("a");stringBuffer.append("b");stringBuffer.append("c");
}
  • 看下 append 方法
@Override
public synchronized StringBuffer StringBuffer#append(java.lang.String)(String str) {toStringCache = null;super.append(str);return this;
}

由于 stringBuffer 是局部变量,不存在竞争关系,所以内部的 synchronized 会被消除掉。

  1. 锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:
// =========== 锁粗化 ===========public void coarsening01() {for (int i = 0; i < 100000; i++) {synchronized (this) {System.out.println("hello world ...");}}}// 锁粗化:通过扩大锁的范围,避免反复加锁和释放锁。public void coarsening02() {synchronized (this) {for (int i = 0; i < 100000; i++) {System.out.println("hello world ...");}}}
  1. 重量级锁(尽量避免)
    Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

总结:
在这里插入图片描述
=== 点击查看top目录 ===

七、番外篇

下一章节:【线程】CountDownLatch 内部原理(十三)
上一章节:【线程】ReentrantReadWriteLock 内部共享锁与排他锁源码剖析 (十一)

这篇关于【线程】线程八锁与Synchronzied内部原理(十二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/763834

相关文章

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu

hdu4059容斥原理

求1-n中与n互质的数的4次方之和 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWrit

STM32内部闪存FLASH(内部ROM)、IAP

1 FLASH简介  1 利用程序存储器的剩余空间来保存掉电不丢失的用户数据 2 通过在程序中编程(IAP)实现程序的自我更新 (OTA) 3在线编程(ICP把整个程序都更新掉) 1 系统的Bootloader写死了,只能用串口下载到指定的位置,启动方式也不方便需要配置BOOT引脚触发启动  4 IAP(自己写的Bootloader,实现程序升级) 1 比如蓝牙转串口,

寻迹模块TCRT5000的应用原理和功能实现(基于STM32)

目录 概述 1 认识TCRT5000 1.1 模块介绍 1.2 电气特性 2 系统应用 2.1 系统架构 2.2 STM32Cube创建工程 3 功能实现 3.1 代码实现 3.2 源代码文件 4 功能测试 4.1 检测黑线状态 4.2 未检测黑线状态 概述 本文主要介绍TCRT5000模块的使用原理,包括该模块的硬件实现方式,电路实现原理,还使用STM32类

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

FreeRTOS内部机制学习03(事件组内部机制)

文章目录 事件组使用的场景事件组的核心以及Set事件API做的事情事件组的特殊之处事件组为什么不关闭中断xEventGroupSetBitsFromISR内部是怎么做的? 事件组使用的场景 学校组织秋游,组长在等待: 张三:我到了 李四:我到了 王五:我到了 组长说:好,大家都到齐了,出发! 秋游回来第二天就要提交一篇心得报告,组长在焦急等待:张三、李四、王五谁先写好就交谁的

java线程深度解析(六)——线程池技术

http://blog.csdn.net/Daybreak1209/article/details/51382604 一种最为简单的线程创建和回收的方法: [html]  view plain copy new Thread(new Runnable(){                @Override               public voi

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之