synchronized 锁的原理

synchronized 的基本认识

  在多线程并发编程中 synchronized 一直是元老级角色,不少人都会称呼它为重量级锁。可是,随着 Java SE 1.6 对synchronized 进行了各类优化以后,有些状况下它就并不那么重,Java SE 1.6 中为了减小得到锁和释放锁带来的性
能消耗而引入的偏向锁和轻量级锁。这块在后续咱们会慢慢展开

synchronized 的基本语法

  synchronized 有三种方式来加锁,分别是
  1. 修饰实例方法,做用于当前实例加锁,进入同步代码前要得到当前实例的锁
  2. 静态方法,做用于当前类对象加锁,进入同步代码前要得到当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要得到给定对象的锁。不一样的修饰类型,表明锁的控制粒度

synchronized 的应用

  修改前面的案例,使用 synchronized 关键字后,能够达到数据安全的效果
 
    
public class Demo {
private static int count = 0;

public static void inc() {
synchronized (Demo.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}

public static void main(String[] args)
throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> Demo.inc()).start();
}
Thread.sleep(3000);
System.out.println("运行结果" + count);
}
}
 

思考锁是如何存储的

  能够思考一下,要实现多线程的互斥特性,那这把锁须要哪些因素?
  1. 锁须要有一个东西来表示,好比得到锁是什么状态、无锁状态是什么状态
  2. 这个状态须要对多个线程共享那么咱们来分析,synchronized 锁是如何存储的呢?观察synchronized 的整个语法发现,synchronized(lock)是基于lock 这个对象的生命周期来控制锁粒度的,那是否是锁的
存储和这个 lock 对象有关系呢?因而咱们以对象在 jvm 内存中是如何存储做为切入点,去看看对象里面有什么特性可以实现锁

对象在内存中的布局

  在 Hotspot 虚拟机中,对象在内存中的存储布局,能够分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

 

 

synchronized 锁的升级

  在分析 markword 时,提到了偏向锁、轻量级锁、重量级锁。在分析这几种锁的区别时,咱们先来思考一个问题使用锁可以实现数据的安全性,可是会带来性能的降低。不使用锁可以基于线程并行提高程序性能,
可是却不能保证线程安全性。这二者之间彷佛是没有办法达到既能知足性能也能知足安全性的要求。hotspot 虚拟机的做者通过调查发现,大部分状况下,加锁的代码不只仅不存在多线程竞争,并且老是由同一个线程
屡次得到。因此基于这样一个几率,是的 synchronized 在JDK1.6 以后作了一些优化,为了减小得到锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。所以你们会发如今 synchronized 中,锁存在四种状态
分别是:无锁、偏向锁、轻量级锁、重量级锁; 锁的状态根据竞争激烈的程度从低到高不断升级。

偏向锁的基本原理

  前面说过,大部分状况下,锁不只仅不存在多线程竞争,而是老是由同一个线程屡次得到,为了让线程获取锁的代价更低就引入了偏向锁的概念。怎么理解偏向锁呢?当一个线程访问加了同步锁的代码块时,会在对象头中存
储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不须要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。若是相等表示偏向锁是偏向于当前线程的,就不须要再尝试得到锁了

偏向锁的获取和撤销逻辑

  1. 首先获取锁 对象的 Markword,判断是否处于可偏向状态。(biased_lock=一、且 ThreadId 为空)
  2. 若是是可偏向状态,则经过 CAS 操做,把当前线程的 ID写入到 MarkWord
    a) 若是 cas 成功,那么 markword 就会变成这样。表示已经得到了锁对象的偏向锁,接着执行同步代码块
    b) 若是 cas 失败,说明有其余线程已经得到了偏向锁,这种状况说明当前锁存在竞争,须要撤销已得到偏向
     锁的线程,而且把它持有的锁升级为轻量级锁(这个操做须要等到全局安全点,也就是没有线程在执行字
     节码)才能执行
  3. 若是是已偏向状态,须要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID
    a) 若是相等,不须要再次得到锁,可直接执行同步代码块
    b) 若是不相等,说明当前锁偏向于其余线程,须要撤销偏向锁并升级到轻量级锁

偏向锁的撤销

  偏向锁的撤销并非把对象恢复到无锁可偏向状态(由于偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程当中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。
对原持有偏向锁的线程进行撤销时,原得到偏向锁的线程
  有两种状况:
  1. 原得到偏向锁的线程若是已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态而且争抢锁的线程能够基于 CAS 从新偏向但前线程
  2. 若是原得到偏向锁的线程的同步代码块还没执行完,处于临界区以内,这个时候会把原得到偏向锁的线程升级为轻量级锁后继续执行同步代码块
  在咱们的应用开发中,绝大部分状况下必定会存在 2 个以上的线程竞争,那么若是开启偏向锁,反而会提高获取锁的资源消耗。因此能够经过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

 

轻量级锁的基本原理

轻量级锁的加锁和解锁逻辑

  锁升级为轻量级锁以后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:
  1. 线程在本身的栈桢中建立锁记录 LockRecord。
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚建立的锁记录中。
  3. 将锁记录中的 Owner 指针指向锁对象。
  4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。

自旋锁

  轻量级锁在加锁过程当中,用到了自旋锁所谓自旋,就是指当有另一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个
得到锁的线程释放锁以后,这个线程就能够立刻得到锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就至关于在执行一个啥也没有的 for 循环。
因此,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就可以得到锁了。自旋锁的使用,其实也是有必定的几率背景,在大部分同
步代码块执行的时间都是很短的。因此经过看似无异议的循环反而能提高锁的性能。可是自旋必需要有必定的条件控制,不然若是一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而
会消耗 CPU 资源。默认状况下自旋的次数是 10 次,能够经过 preBlockSpin 来修改在 JDK1.6 以后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自
旋的时间以及锁的拥有者的状态来决定。若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也是颇有可能再次成功,进而它将容许自旋等待持续相
对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

轻量级锁的解锁

  轻量级锁的锁释放逻辑其实就是得到锁的逆向逻辑,经过CAS 操做把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,若是成功表示没有竞争。若是失败,表示
当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

 

重量级锁的基本原理

  当轻量级锁膨胀到重量级锁以后,意味着线程只能被挂起阻塞来等待被唤醒了。

重量级锁的 monitor

  加了同步代码块之后,在字节码中会看到一个monitorenter 和 monitorexit。每个 JAVA 对象都会与一个监视器 monitor 关联,咱们能够把它理解成为一把锁,当一个线程想要执行一段被
synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。monitorenter 表示去得到一个对象监视器。monitorexit 表示释放 monitor 监视器的全部权,使得其余被阻塞的线程
能够尝试去得到这个监视器monitor 依赖操做系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会致使系统在用户态与内核态之间来回切换,严重影响锁的性能

重量级锁的加锁的基本流程

  任意线程对 Object(Object 由 synchronized 保护)的访问,首先要得到 Object 的监视器。若是获取失败,线程进入同步队列,线程状态变为 BLOCKED。当访问 Object 的
前驱(得到了锁的线程)释放了锁,则该释放操做唤醒阻塞在同步队列中的线程,使其从新尝试对监视器的获取。

回顾线程的竞争机制

  再来回顾一下线程的竞争机制对于锁升级这块的一些基本流程。方便你们更好的理解加入有这样一个同步代码块,存在 Thread#一、Thread#2 等多个线程
  synchronized (lock) {
    // do something
  }
  状况一:只有 Thread#1 会进入临界区;
  状况二:Thread#1 和 Thread#2 交替进入临界区,竞争不激烈;
  状况三:Thread#1/Thread#2/Thread3… 同时进入临界区,竞争激烈

偏向锁

  此时当 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

  前面咱们在讲 synchronized 的时候,发现被阻塞的线程何时被唤醒,取决于得到锁的线程何时执行完同步代码块而且释放锁。那怎么作到显示控制呢?咱们就须要
借 助 一 个 信 号 机 制 : 在 Object 对 象 中 , 提 供 了wait/notify/notifyall,能够用于控制线程的状态

wait/notify/notifyall 基本概念

  wait:表示持有对象锁的线程 A 准备释放对象锁权限,释放 cpu 资源并进入等待状态。
  notify:表示持有对象锁的线程 A 准备释放对象锁权限,通知 jvm 唤 醒 某 个 竞 争 该 对 象 锁 的 线 程 X 。 线 程 Asynchronized 代码执行结束而且释放了锁以后,线程 X 直
     接得到对象锁权限,其余竞争线程继续等待(即便线程 X 同步完毕,释放对象锁,其余竞争线程仍然等待,直至有新的 notify ,notifyAll 被调用)。
  notifyAll:notifyall 和 notify 的区别在于,notifyAll 会唤醒全部竞争同一个对象锁的全部线程,当已经得到锁的线程A 释放锁以后,全部被唤醒的线程都有可能得到对象锁权限
  须要注意的是:三个方法都必须在 synchronized 同步关键字 所 限 定 的 做 用 域 中 调 用 , 否 则 会 报 错java.lang.IllegalMonitorStateException ,意思是由于没有同步,因此
线程对对象锁的状态是不肯定的,不能调用这些方法。另外,经过同步机制来确保线程从 wait 方法返回时可以感知到感知到 notify 线程对变量作出的修改
相关文章
相关标签/搜索