ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 三种使用方式 * 修饰普通的实例方法,对于普通的同步方法,锁式当前实例对象 * 修饰静态方法,对于静态同步方法,锁式当前类的Class对象 * 修饰代码块,对于同步方法块,锁是Synchronized配置的对象 ## java对象头 ![](https://img.kancloud.cn/1a/a2/1aa25b32794bb76453c004bf46407f99_892x562.png) ”Mark Word"存储一下消息 ~~~ hash:保存对象的哈希码 age:GC分代年龄 biased_lock:偏向锁标志 lock:锁状态标志 JavaThread* 当前线程 epoch:保存偏向时间戳 //其中关于当前锁的状态标志markOopDesc类中也进行了详细的说明,具体代码如下: enum { locked_value = 0,//轻量级锁 对应[00] unlocked_value = 1,//无锁状态 对应[01] monitor_value = 2,//重量级锁 对应[10] marked_value = 3,//GC标记 对应[11] biased_lock_pattern = 5//是否是偏向锁 对应[101] 其中biased_lock一个bit位,lock两个bit位 }; ~~~ ## synchronized锁优化 Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是: 1. 无锁状态 2. 偏向锁状态 3. 轻量级锁状态 4. 重量级锁状态 这几个状态会随着竞争情况逐渐升级,锁**可以升级但不能降级**,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率 所谓的锁,本质上就是一个共享变量,这个变量标识了某个共享对象是否被其他线程占用。 线程在访问共享对象时,要先判断该共享变量。 ### 偏向锁 理解锁被同一线程 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。为了让线程获得锁的代价更低而引入了偏向锁 当一个线程访问同步块并获取锁时,会在对象头中的“Mark word"和栈帧中的锁记录里存储锁偏向的线程ID。以后该线程在进入和退出同步块时,不需要进行CAS操作来加锁和解锁。只需简单地测试一下对象头的”Mark Word“里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下“Mark Word”中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。 ### 轻量级锁 理解锁被多个线程,但线程之间不存在竞争 * 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。 * 然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 ### 重量级锁 理解锁被多个线程,且线程之间存在竞争 重量级锁的竞争是在objectMonitor.cpp中ObjectMonitor::enter()方法中实现的。 简述整个过程,可以是根据虚拟机规范的要求,在执行monitorenter指令时: 1. 首先要尝试获取对象的锁。 2. 如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1。相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。 3. 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。 虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意的。 1. synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题 2. 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。 **ObjectMonitor结构** 在讲解具体的锁获取之前,我们需要了解**每个锁对象(这里指已经升级为重量级锁的对象)都有一个ObjectMonitor(对象监视器)**。也就是说**每个线程获取锁对象都会通过ObjectMonitor** ~~~ class ObjectMonitor { public: enum { OM_OK, // 没有错误 OM_SYSTEM_ERROR, // 系统错误 OM_ILLEGAL_MONITOR_STATE, // 监视器状态异常 OM_INTERRUPTED, // 当前线程已经中断 OM_TIMED_OUT // 线程等待超时 }; volatile markOop _header; // 线程帧栈中存储的 锁对象的mark word拷贝 protected: // protected for JvmtiRawMonitor void * volatile _owner; // 指向获得objectMonitor的线程或者 BasicLock对象 volatile jlong _previous_owner_tid; // 上一个获得objectMonitor的线程id volatile intptr_t _recursions; // 同一线程重入锁的次数,如果是0,表示第一次进入 ObjectWaiter * volatile _EntryList; // 在进入或者重进入阻塞状态下的线程链表 protected: ObjectWaiter * volatile _WaitSet; // 处于等待状态下的线程链表 ObjectWaiter volatile jint _waiters; //处于等待状态下的线程个数 } ~~~ 重量级级锁的竞争步骤,主要分为以下几个步骤: 1. 通过CAS操作尝试吧monitor的\_owner( 指向获得objectMonitor的线程或者 BasicLock对象)设置为当前线程,如果CAS操作成功,表示线程获取锁成功,直接执行同步代码块。 2. 如果是同一线程重入锁,则记录当前重入的次数。 3. 如果2,3步骤都不满足,则开始竞争锁,走EnterI()方法。 EnterI()方法实现如下: 1. 把当前线程被封装成ObjectWaiter的node对象,同时将该线程状态设置为TS\_CXQ(竞争状态) 2. 在for循环中,通过CAS把node节点push到\_cxq链表中,如果CAS操作失败,继续尝试,是因为当期\_cxq链表已经发生改变了继续for循环,如果成功直接返回。 3. 将node节点push到\_cxq链表之后,通过自旋尝试获取锁(TryLock方法获取锁),如果循环一定次数后,还获取不到锁,则通过park函数挂起。(并不会消耗CPU资源) 重量级锁的释放可以分为以下步骤: 1. 判断当前锁对象中的\_owner没有指向当前线程,如果\_owner指向的BasicLock在当前线程栈上,那么将\_owner指向当前线程。 2. 如果当前锁对象中的\_owner指向当前线程,则判断当前线程重入锁的次数,如果不为0,那么就重新走ObjectMonitor::exit(),直到重入锁次数为0为止。 3. 释放当前锁,并根据QMode的模式判断,是否将\_cxq中挂起的线程唤醒。还是其他操作。 ## 参考资料 [Java并发编程:synchronized](https://blog.csdn.net/fei20121106/article/details/83268379)