synchronized
Java SE 1.6前重量级锁,Java SE 1.6 中为了减小得到锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。java
synchronized 的基本语法
synchronized 有三种方式来加锁,分别是安全
- 修饰实例方法,做用于当前实例加锁,进入同步代码前要得到当前实例的锁
- 静态方法,做用于当前类对象加锁,进入同步代码前要得到当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要得到给定对象的锁。 不一样的修饰类型,表明锁的控制粒度
synchronized的存储
Mark word 记录了对象和锁有关的信息,当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操做都和 Mark word 有关系。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。线程在获取锁的时候,实际上就是得到一个监视器对象(monitor) ,monitor 能够认为是一个同步对象,全部的Java对象都携带 monitor,多个线程访问同步代码块时,至关于去争抢对象监视器修改对象中的锁标识。jvm
synchronized 锁的升级
偏向锁
当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不须要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。若是相等表示偏向锁是偏向于当前线程的,就不须要再尝试得到锁了。性能
偏向锁的获取和撤销逻辑
- 首先获取锁对象的 Markword,判断是否处于可偏向状态。(biased_lock=一、且 ThreadId 为空)
- 若是是可偏向状态,则经过 CAS 操做,把当前线程的 ID写入到 MarkWord
- 若是 cas 成功,那么 markword会记录ThreadId,同时置偏向标志位1。表示已经得到了锁对象的偏向锁,接着执行同步代码块
- 若是 cas 失败,说明有其余线程已经得到了偏向锁,这种状况说明当前锁存在竞争,须要撤销已得到偏向锁的线程,而且把它持有的锁升级为轻量级锁(这个 操做须要等到全局安全点,也就是没有线程在执行字节码)才能执行
- 若是是已偏向状态,须要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID
- 若是相等,不须要再次得到锁,可直接执行同步代码块
- 若是不相等,说明当前锁偏向于其余线程,须要撤销偏向锁并升级到轻量级锁
偏向锁的撤销
偏向锁的撤销并非把对象恢复到无锁可偏向状态(由于偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程当中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。对原持有偏向锁的线程进行撤销时,原得到偏向锁的线程有两种状况:操作系统
- 原得到偏向锁的线程若是已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态而且争抢锁的线程能够基于 CAS 从新偏向当前线程
- 若是原得到偏向锁的线程的同步代码块还没执行完,处于临界区以内,这个时候会把原得到偏向锁的线程升级为轻量级锁后继续执行同步代码块
在咱们的应用开发中,绝大部分状况下必定会存在 2 个以上的线程竞争,那么若是开启偏向锁,反而会提高获取锁的资源消耗。因此能够经过 jvm 参数 UseBiasedLocking 来设置开启或关闭偏向锁。
轻量级锁
轻量级锁的基本原理
轻量级锁的加锁和解锁逻辑
锁升级为轻量级锁以后,对象的Markword也会进行相应的的变化。 升级为轻量级锁的过程:线程
- 线程在本身的栈桢中建立锁记录LockRecord。
- 将锁对象的对象头中的MarkWord复制到线程的刚刚建立的锁记录中。
- 将锁记录中的Owner指针指向锁对象。
- 将锁对象的对象头的MarkWord替换为指向锁记录的指针。
自旋锁
轻量级锁在加锁过程当中,用到了自旋锁
所谓自旋,就是指当有另一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个得到锁的线程释放锁以后,这个线程就能够立刻得到锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就至关于在执行一个啥也没有的for循环。因此,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就可以得到锁了。
自旋锁的使用,其实也是有必定的几率背景,在大部分同步代码块执行的时间都是很短的。因此经过看似无异议的循环反而能提高锁的性能。可是自旋必需要有必定的条件控制,不然若是一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认状况下自旋的次数是 10 次,能够经过 preBlockSpin 来修改。
在 JDK1.6 以后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。 若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也是颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。指针
轻量级锁的解锁
轻量级锁的锁释放逻辑其实就是得到锁的逆向逻辑,经过CAS操做把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,若是成功表示没有竞争。若是失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁。
对象
重量级锁
当轻量级锁膨胀到重量级锁以后,意味着线程只能被挂起阻塞来等待被唤醒了。 每个 JAVA 对象都会与一个监视器 monitor 关联,咱们能够把它理解成为一把锁,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。
monitorenter 表示去得到一个对象监视器。 monitorexit 表示释放 monitor 监视器的全部权,使得其余被阻塞的线程能够尝试去得到这个监视器。 monitor 依赖操做系统的MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会致使系统在用户态与内核态之间来回切换,严重影响锁的性能。
任意线程对 Object(Object 由 synchronized 保护)的访问,首先要得到 Object 的监视器。若是获取失败,线程进入同步队列,线程状态变为 BLOCKED。当访问 Object 的前驱(得到了锁的线程)释放了锁,则该释放操做唤醒阻塞在同步队列中的线程,使其从新尝试对监视器的获取。blog
总结
偏向锁
此时当 Thread#1 进入临界区时,JVM 会将 lockObject 的对象头 Mark Word 的锁标志位设为“01”,同时会用 CAS 操做把 Thread#1 的线程 ID 记录到 Mark Word 中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于 Thread#1,若接下来没有其余线程进入临界区,则 Thread#1 再出入临界区无需再执行任何同步操做。也就是说,若只有Thread#1 会进入临界区,实际上只有 Thread#1 初次进入临界区时须要执行 CAS 操做,之后再出入临界区都不会有同步操做带来的开销。
轻量级锁
偏向锁的场景太过于理想化,更多的时候是 Thread#2 也会尝试进入临界区, 若是 Thread#2 也进入临界区可是Thread#1 尚未执行完同步代码块时,会暂停 Thread#1而且升级到轻量级锁。Thread#2 经过自旋再次尝试以轻量级锁的方式来获取锁
重量级锁
若是 Thread#1 和 Thread#2 正常交替执行,那么轻量级锁基本可以知足锁的需求。可是若是 Thread#1 和 Thread#2同时进入临界区,那么轻量级锁就会膨胀为重量级锁,意味着 Thread#1 线程得到了重量级锁的状况下,Thread#2就会被阻塞。队列
Synchronized 结合 Java Object 对象中的wait,notify,notifyAll
wait/notify/notifyall 基本概念
wait:表示持有对象锁的线程 A 准备释放对象锁权限,释放 cpu 资源并进入等待状态。
notify:表示持有对象锁的线程A准备释放对象锁权限,通知 jvm唤醒某个竞争该对象锁的线程X 。线 程A synchronized 代码执行结束而且释放了锁以后,线程X 直接得到对象锁权限,其余竞争线程继续等待(即便线程X同步完毕,释放对象锁,其余竞争线程仍然等待,直至有新的notify,notifyAll被调用)。 notifyAll:notifyall 和 notify 的区别在于,notifyAll会唤醒全部竞争同一个对象锁的全部线程,当已经得到锁的线程A释放锁以后,全部被唤醒的线程都有可能得到对象锁权限 须要注意的是:三个方法都必须在 synchronized 同步关键字所限定的做用域中调用,不然会报错java.lang.IllegalMonitorStateException ,意思是由于没有同步,因此线程对对象锁的状态是不肯定的,不能调用这些方法。另外,经过同步机制来确保线程从wait方法返回时可以感知到notify线程对变量作出的修改。