Synchronized锁
synchronized
-
由对象头中的 对象标志 根据锁标志位的不同而被复用 以及锁升级策略
-
能用无锁 就不要用锁,能锁代码块 就不锁整个方法, 能用对象锁 就不用类锁. 尽可能让锁的粒度更小,以提高并发效率
-
每个对象\类 都是一把锁, 底层是Monitor锁
-
本质是依赖于操作系统的Mutex Lock实现,操作系统实现线程之间切换 需要从用户态到内核态, 成本高
-
Monitor监视器是由ObjectMonitor实现的, 底层是C++ 的ObjectMonitor.hpp
-
Monitor 与 java对象如何关联的
- 1、如果一个java对象被某个线程锁住,则该java对象的mark word 字段中的LockWard指向monitor的起始地址
- 2、monitor的owner字段存放拥有 关联对象锁的线程id
-
用法
- 同步块
- 同步方法
synchronized 底层演变
-
java的线程
- 调用start方法启动一个线程,实际上是映射到操作系统原生线程之上的, 如果要阻塞或唤醒一个线程就需要操作系统的介入, 需要在用户态和核心态之间切换, 这种切换会消耗大量的系统资源, 因为用户态和内核态都有各自专用的内存空间
-
jdk5以前,synchronized属于重量级锁,效率低下,因为监视器(monitor)是依赖于底层 操作系统的Mutex Lock(系统互斥量) 来实现的, 挂起线程和恢复线程都需要转入内核态去完成. 阻塞和唤醒一个线程需要操作系统切换CPU状态完成, 这种状态的切换需要耗费处理器时间,如果同步代码块中内容过于简单, 这种切换的时间可能比用户代码执行时间还长, 时间成本高
-
Mutex Lock
- Monitor是在jvm底层实现的,底层代码是c++。本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以synchronized是Java语言中的一个重量级操作
-
jdk6之后, 为了减少获得锁和释放锁所带来的性能消耗, 引入了轻量级锁和偏向锁, 需要有个逐步升级的过程,别一开始就直接到重量级锁
锁介绍
-
无锁态
- 只有一个线程,无竞争
-
偏向锁
-
当一段同步代码一直被同一个线程多次访问,由于只有一个线程, 那么该线程在后续访问时便会自动获取锁
-
由来
- 实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程
-
理论
- 在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。
- 如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
- 假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
-
一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入Monitor去竞争对象了。
-
偏向锁的相关参数
- 偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟, 所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动
- 开启: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 关闭: -XX:-UseBiasedLocking
-
-
轻量级锁
-
本质就是自旋锁, 有线程来参与锁的竞争,但是获取锁的冲突时间极短
-
理论
- (1). 轻量级锁是为了在线程近乎交替执行同步块时提高性能
- (2). 主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋再阻塞
- (3). 升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
- (4). 假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况- 如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位
- 如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
- 如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位
-
-
重量级锁
- monitor实现, 进入之前monitorenter ,之后 monitor
-
比较
-
Displaced MarkWord: JVM会为每个线程在当前线程的栈桢中创建用于存储锁记录的空间
说明 | 无锁 | 偏向锁 | 轻量级锁 | 重量级锁 | |
---|---|---|---|---|---|
加锁 | - | 锁标志位 001 | 将当前线程ID记录到MarkWord、锁标志位 101 | 一个线程获取锁 会把锁的markWord复制到自己的Displaced MarkWord,然后其他线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针, 成功则获取锁,失败则自旋 锁标志位 000 | - |
解锁 | MarkWord 当前线程ID清除 | 释放锁时,CAS将Displaced MarkWord的内容复制回MarkWord | |||
markWord存储 | 指向的是线程ID | 线程栈中Lock Record的指针 | 堆中的monitor对象的指针 |
锁升级过程
-
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
-
轻量锁 -> 重量级
- JDK6之前 CAS次数达到10次, 就会升级重量级锁
- JDK6之后 自适应次数
- 线程如果自旋成功了,那下次自旋的最大次数会增加
- 很少成功,下次会减少自旋的次数
锁消除
- 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
- 一个线程自己加锁,没必要
锁粗化
- 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
拓展信息
- jdk15已经去掉了偏向锁, 原因是因为维护成本较高, 费力不讨好
- 如果在jdk15还想用到偏向锁,可以手动设置jvm参数: -XX:+UseBiased
- 锁升级后与hashcode的关系
-
无锁状态下, Mark Word中可以存储对象的hash code值,当对象的hashcode方法被第一次调用时,JVM会生成对应的hashcode值 存储到Mark Word中
-
对于偏向锁,在线程获取偏向锁时, 会用ThreadId 和 epoch 覆盖hash code所在的位置. 如果一个对象的hashCode方法已经被调用过了,这个对象不能被设置偏向锁.
- 如果可以, 那MarkWord中的hashcode必然会被覆盖,就会导致 两次hashcode方法不一样
-
升级成了轻量级锁,JVM会在当前线程的栈桢中创建一个锁记录空间,用户存储锁对象的MarkWord拷贝, 该拷贝中是含有hashcode的
-
升级成重量级锁, MarkWord保存的是重量级锁指针(Monitor对象指针), ObjectMonitor类中有字段记录非加锁状态下的MarkWord, 锁记录释放后会将信息写会对象头
-
特殊场景
- 当一个对象已经计算过hash code, 它就无法进入偏向锁状态, 会跳过偏向锁,直接进入轻量级锁
- 偏向锁过程中遇到一致性hash请求, 立马撤销偏向模式, 膨胀为重量级锁
-