化妆品网站程序,教育培训 营销型网站系统,logo图标,设计书籍频道开放说明1. 对象在内存中的布局分为三块区域#xff1a;
#xff08;1#xff09;对象头#xff08;Mark Word、元数据指针和数组长度#xff09;
对象头#xff1a;在32位虚拟机中#xff0c;1个机器码等于4字节#xff0c;也就是32bit#xff0c;在64位虚拟机中#xff0…1. 对象在内存中的布局分为三块区域
1对象头Mark Word、元数据指针和数组长度
对象头在32位虚拟机中1个机器码等于4字节也就是32bit在64位虚拟机中1个机器码是8个字节也就是64bitJava对象头一般占有2个机器码即64bit但是 如果对象是数组类型则需要3个机器码即96bit因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小但是无法从数组的元数据来确认数组的大小所以用一块来记录数组长度。
Mark Word32bit存储代表该对象运行时的一些信息,哈希码、GC分代年龄、锁标志位、偏向线程ID、偏向时间戳等信息。 这些信息都是与对象自身定义无关的数据所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。
解读
1前面30bit位可能表示的意思不一样但是最后2个bit表示的都是锁模式。
2当偏向锁标志是0锁标志位是01也就是最后3位是001的时候表示无锁模式。Mark Word记录的数据就是对象的hashcode 和 GC的年龄。当有第一个线程请求加锁的时候会升级为偏向锁
3 当偏向锁标志是1锁标志是01也就是最后三位是101的时候处于偏向锁模式Mark Word这个时候记录的数据就是获取偏向锁的线程ID、Epoch、对象GC年龄当有第二个线程请求加锁的时候会升级为轻量级锁
4当锁标志位是00的时候表示处于轻量级锁模式。会把锁记录放在加锁的线程的虚拟机栈空间中所以这种情况下锁记录在哪个线程虚拟机栈中就表示所在线程就获取到了锁。Mark Word记录的数据就是就指向那个锁记录地址这个锁记录地址在哪个线程中就表示哪个线程获取到了轻量级锁。
5当锁标志位是10的时候表示处于重量级锁模式这个时候就说明竞争激烈了处于重量级锁模式了此时使用重量级加锁不是Mark Word的职责范围了是monitor的职责Mark Word 记录的数据就是monitor的地址有加锁的需求直接根据这个地址找到monitor找monitor加锁。 元数据指针Klass Point它主要指向类的数据也就是指向方法区中的位置通过这个指针我们就可以知道该实例属于哪个类长度通常为32bit。
数组长度Array Length 如果是数组对象头中还有一块用于存放数组长度的数据因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小但是从数组的元数据中无法确定数组的大小。只有数组对象才有在32位或者64位JVM中长度都是32bit。
2实例数据
实例数据存放类的属性数据信息包括父类的属性信息。
3对齐填充非必须
对齐填充由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的仅仅是为了字节对齐。
2. 通过以上理解可以清楚对象头、Mark Word 和 monitor之间的关系
当Mark Word中最后两位的锁标志位是10的时候Mark Word的前面是monitor监视器的地址我现在就给你画出来对象头、Mark Word 和 monitor之间的关系图 3. synchronized是如何通过monitor加锁的
3.1 monitor概念
monitor叫做对象监视器、也叫作监视器锁JVM规定了每一个java对象都有一个monitor对象与之对应这monitor是JVM帮我们创建的在底层使用C实现的。
3.2 monitor属性
其实monitor在底层也是某个类的对象那个类就是ObjectMonitor它拥有的属性字段如下 ObjectMonitor() { _header NULL; _count 0; // 非常重要表示锁计数器_count 0表示还没人加锁_count 0 表示已经加锁加锁的次数可重入锁的原理在此再次执行monitorenter进入后就加1释放时候执行monitorexit指令前就减1。 _waiters 0, _recursions 0; _object NULL; _owner NULL; // 非常重要指向加锁成功的线程_owner null 时候表示没人加锁,比如线程A获取锁成功了则 _owner 线程A。 _WaitSet NULL; // wait线程的集合在synchorized代码块中调用wait()方法的线程会释放锁被加入到此集合中沉睡然后线程就会被挂起等待别人调用notify叫醒它。 _WaitSetLock 0 ; _Responsible NULL ; _succ NULL ; //多线程竞争锁进入时的单向链表 _cxq NULL ; FreeNext NULL ; //_owner从该双向循环链表中唤醒线程节点_EntryList是第一个节点 _EntryList NULL ; // 非常重要等待队列加锁失败的线程会block住被加入到这个等待队列中等待再次争抢锁 _SpinFreq 0 ; // 获取锁之前的自旋的次数 JDK1.6之后对synchronized进行优化原先JDK1.6以前只要线程获取锁失败线程立马被挂起线程醒来的时候再去竞争锁这样会导致频繁的上下文切换性能太差了。 JDK1.6后优化了这个问题就是线程获取锁失败之后不会被立马挂起而是每个一段时间都会重试去争抢一次这个 _spinFreq就是最大的重试次数也就是自旋的次数如果超过了这个次数抢不到那线程只能沉睡了。 _SpinClock 0 ; // 获取之前每次锁自旋的时间上面说获取锁失败每隔一段时间都会重试一次这个属性就是自旋间隔的时间周期比如50ms那么就是每隔50ms就尝试一次获取锁。 OwnerIsThread 0 ; _previous_owner_tid 0; } 3.3 monitor如何通过这些属性加锁
1首先呢没有线程对monitor进行加锁的时候是这样的_count 0 表示加锁次数是0也就是没线程加锁 _owner 指向null也就是没线程加锁。
2此时线程A、线程B来竞争加锁了都请求将_count 修改为1此修改具有原子性同一时间只有一个线程可以修改成功此时线程A竞争到锁将 _count 修改为1表示加锁次数为1将_owner 线程A也就是指向自己表示线程A获取到了锁。同理可得释放锁的时候将_count 设置为 0 , 将 _owner 设置为 null 。
3在线程A持有锁的时候monitor里面记录的 _spinFreq 、spinclock 信息告诉线程B你可以每隔50ms来尝试加锁一次总共可以尝试10次。
4如果线程B在10次尝试加锁期间获取锁成功了那线程B将 _count 设置为 1 _owner 指向自己表示自己获取锁成功了。
5如果10次尝试获取锁此时都用完了那线程B只能放到等待队列_EntryList里面先睡觉去了也就是线程B被挂起了。
3.4 线程获取锁失败后的自旋操作好处
这个其实跟jvm获取monitor锁的优化有关。
1首先线程挂起之后唤醒的代价很大底层涉及到上下文切换用户态和内核态的切换打个比方可能最少耗时3000ms这样这只是打个比方。
2线程A获取了锁这个时候线程B获取失败。按照上面自旋的数据 _spinclock 50ms每次自旋50ms _spinFreq 10最多10次自旋。
3假如线程A使用的时间很短比如只使用150ms的时间那么线程B自旋3次后就能获取到锁了也就花费了150ms左右的时间相比于挂起之后唤醒最少花费3000ms的时间大大减少了等待时间这也就提高了性能了。
4如果不设置自旋的次数限制而是让它一直自旋。假如线程A这哥们耗时特别的久比如它可能在里面搞一下磁盘IO或者网络的操作花了5000ms。
那线程B可不能在那一直自旋着等着它吧毕竟自旋可是一直使用CPU不释放CPU资源的CPU这时也在等着不能干别的事这可是浪费资源啊所以啊自旋次数也是要有限制的不能一直等着否则CPU的利用率大大被降低了。
所以在10次自旋之后也就是500ms之后还获取失败那就把自己挂起释放CPU资源咯。
3.5 monitor的wait和notify
说起monitor里面的waitset上面讲的就是一个集合。
当线程获取锁之后才能调用wait()方法然后此时释放锁将_count恢复为0将_owner指向 null然后将自己加入到waitset集合中等待别人调用notify或者notifyAll将其中waitset的线程唤醒
3.6 notify和notifyAll区别
简单说就是notify就是从waitset中随机挑一个线程来唤醒只唤醒一个。notifyAll这方法就是将waitset中所有等着的线程全部唤醒了。
3.7 wait() 和 Thread.sleep()的区别
wait()会释放锁而Thread.sleep()不释放锁
4. synchronized锁升级优化
4.1 偏向锁 引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径因为轻量级锁的获取及释放依赖多次CAS原子指令而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令由于一旦出现多线程竞争的情况就必须撤销偏向锁所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗。轻量级锁是为了在线程交替执行同步块时提高性能而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
4.1.1 偏向锁获取过程
当线程A第一次进入synchronized的同步代码块之内发现Mark Word的最后三位是001表示当前无锁状态于是选择代价最小的方式加了个偏向锁只在第一次获取偏向锁的时候执行CAS操作将自己的线程Id通过CAS操作设置到Mark Word中同时将偏向锁标志位改为1。后面如果自己再获取锁的时候每次检查一下发现自己之前加了偏向锁就直接执行代码就不需要再次加锁了。加了偏向锁的线程是个自私的线程这家伙用完了锁之后自己加锁时候修改过的Mark Word信息都不会再改回来了也就是它不会主动释放锁。
这个哥们不释放锁如果它用完了别人这个时候需要进入synchronized代码块怎么办此时涉及到偏向锁之重偏向。
4.1.2 偏向锁之重偏向
线程B去申请加锁发现是线程A加了偏向锁这时候回去判断一下线程A是否存活如果线程A挂了就可以重新偏向了重偏向也就是将自己的线程ID设置到Mark Word中。
如果线程A没挂但是synchronized代码块执行完了这个时候也可以重新偏向了将偏向标识指向自己。
如果线程B在申请获取锁的时候线程A这哥们还没执行完synchronized同步代码块怎么办这就需要将锁升级一下了都使用偏向锁不行吗不升级有什么坏处
4.1.2 偏向锁的释放
偏向锁的释放在上述提到。偏向锁不会主动释放锁只有遇到其他线程尝试竞争偏向锁时持有偏向锁的线程才会释放锁。偏向锁的释放需要等待全局安全点在这个时间点上没有字节码正在执行它会首先暂停拥有偏向锁的线程判断锁对象是否处于被锁定状态撤销偏向锁后恢复到未锁定标志位为“01”或轻量级锁标志位为“00”的状态。
4.2 轻量级锁
4.2.1 偏向锁为什么要升级为轻量级锁
轻量级锁模式下加锁之前会创建一个锁记录然后将Mark Word中的数据备份到锁记录中Mark Word存储hashcode、GC年龄等很重要数据不能丢失了以便后续恢复Mark Word使用。
这个锁记录放在加锁线程的虚拟机栈中加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中就说明哪个线程获取了轻量级锁。
先看如下代码块
// 代码块1
synchronized(this){// 业务代码1
}
// 代码块2
synchronized(this){// 业务代码2
}
// 代码块3
synchronized(this){// 业务代码3
}
// 代码块4
synchronized(this){// 业务代码4
}
假如这个时候有线程A、B、C、D四个线程线程A先加了偏向锁。之前讲过偏向锁只是在第一次获取锁的时候加锁后面都是直接操作的不需要加锁。
这个时候其它几个线程B、C、D想要加锁如果线程A连续执行上面4个代码块那么其他线程看到线程A都在执行synchronized同步代码块没完没了了想重偏向都不行!! 这个时候就需要等线程A执行完4个synchronized代码块之后才能获取锁啊哈哈别的线程都只能看线程A一个人自己在那表演了这样代码就变成串行执行了。所以偏向锁需要升级。
1首先线程A持有偏向锁然后正在执行synchronized块中的代码。
2这个时候线程B来竞争锁发现有人加了偏向锁并且正在执行synchronized块中的代码为了避免上述说的线程A一直持有锁不释放的情况需要对锁进行升级升级为轻量级锁。
3先将线程A暂停为线程A创建一个锁记录Lock Record将Mark Word的数据复制到锁记录中然后将锁记录放入线程A的虚拟机栈中。
4然后将Mark Word中的前30位指向线程A中锁记录的地址并且对象Mark Word的锁标志位设置为“00”即表示此对象处于轻量级锁定状态将线程A唤醒线程A就知道自己持有了轻量级锁。 4.2.2 在轻量级锁模式下多线程是怎么竞争锁和释放锁的
1线程A和线程B同时竞争锁在轻量级锁模式下都会创建Lock Record锁记录放入自己的栈帧中。
2同时执行CAS操作将Mark Word前30位设置为自己锁记录的地址谁设置成功了谁就获取到锁。 4.2.3 轻量级锁模式下获取锁失败的线程应该会怎么样
获取不到会自旋回看3.4讲解的线程获取锁失败后的自旋操作好处
4.2.4 轻量级锁的释放
就将自己的Lock Record中的Mark Word备份的数据恢复回去即可恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。
1通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
2如果替换成功整个同步过程就完成了。
3如果替换失败说明有其他线程尝试过获取该锁此时锁已膨胀那就要在释放锁的同时唤醒被挂起的线程。
4.3 重量级锁、轻量级锁和偏向锁之间转换 4.4 其他优化
4.4.1 适应性自旋Adaptive Spinning
从轻量级锁获取的流程中我们知道当线程在获取轻量级锁的过程中执行CAS操作失败时是要通过自旋来获取重量级锁的。问题在于自旋是需要消耗CPU的如果一直获取不到锁的话那该线程就一直处在自旋状态白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数例如让其循环10次如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋简单来说就是线程如果自旋成功了则下次自旋的次数会更多如果自旋失败了则自旋的次数就会减少。
4.4.2 锁粗化Lock Coarsening
锁粗化的概念应该比较好理解就是将多次连接在一起的加锁、解锁操作合并为一次将多个连续的锁扩展成一个范围更大的锁。
4.4.3 锁消除Lock Elimination
锁消除即删除不必要的加锁操作。根据代码逃逸技术如果判断到一段代码中堆上的数据不会逃逸出当前线程那么可以认为这段代码是线程安全的不必要加锁。
4.4.4 总结 本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化但是这两种锁也不是完全没缺点的比如竞争比较激烈的时候不但无法提升效率反而会降低效率因为多了一个锁升级的过程这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比 锁 优点 缺点 适用场景 偏向锁 加锁和解锁不需要额外的消耗和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。 轻量级锁 竞争的线程不会阻塞提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。 同步块执行速度非常快。 重量级锁 线程竞争不使用自旋不会消耗CPU。 线程阻塞响应时间缓慢。 追求吞吐量。 同步块执行速度较长。