干货满满的synchronized详解!!!

大家好,我是wave,这次来和大家详细的聊一聊Synchronized这个关键字,希望大家看完可以对synchronized关键字有一个非常全面的了解。

Synchronized基本操作

synchronized主要有三种使用方式:

修饰实例方法: 给一个类上的方法添加synchronized关键字,这个锁会作用于这个类当前的实例对象上,进入同步代码前要获得 当前对象实例的锁

public synchronized void method(){
        // do something
    }

修饰静态方法: 由于静态方法属于类,就相当于给一个类加了锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

public synchronized static void method(){
        // do something
    }

修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

public class Solution {
    public void method(){
        synchronized (this){
            //给当前对象加锁
        }
        synchronized (Solution.class){
            //给Solution这个类加锁
        }
        Object lock = new Object();
        synchronized (lock){
            //给Object这个对象加锁
        }
    }
}

synchronized在JDK早期是一个非常重量级的锁,依靠JVM的指令对代码块进行加锁,所以synchronized是JVM层面的锁,而Lock是用Java代码实现的,所以可以看作是API层面的锁。但是后续JDK对synchronized进行了非常多的优化和升级,目前synchronized与Lock的效率已经相差不多了。

synchronized修饰代码块

synchronized修饰代码块是用两个指令monitorenter、monitorexit完成的,口说无凭,我带大家看一看字节码吧~

public class Solution {
   public void method(){
       synchronized (this){
           System.out.println("synchronized修饰代码块");
       }
   }
}
  • 首先写一段这样的代码,然后可以通过javap来查看它的字节码。如果使用IDEA的话可以经过一点小小的配置就可以在控制台直接输出字节码了,先给大家看看IDEA应该如何配置
  • File->setting->Tools->External->左上角的+号,里面中间的三个配置分别为:
  • $JDKPath$binjavap.exe
  • -verbose -p -c $FileClass$
  • $OutputPath$
  • 然后编译一下程序,按运行旁边的绿色小锤子就可以了,再右键打开External tool找到刚才配置的工具,就可以在控制台打印出字节码了。
干货满满的synchronized详解!!!
IDEA配置
干货满满的synchronized详解!!!
打开工具
干货满满的synchronized详解!!!
字节码结果

现在我说synchronized加在同步代码块上是用JVM指令来实现的就没啥问题了吧~当JVM执行到monitorenter的时候线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

synchronized修饰方法

public class Solution {
    public synchronized void method(){
        System.out.println("synchronized 修饰方法");
    }
}
干货满满的synchronized详解!!!
修饰方法

可以看到修饰一个方法就会有一个ACC_SYNCHRONIZED标识,代表这个方法是同步方法。

synchronized锁升级

JDK1.6的时候对synchronized进行了大量的优化,synchronized的锁现在有三种形态:偏向锁、轻量级锁、重量级锁。有这三种锁可以适应不同的应用场景,在几乎没有并发的时候偏向锁效率高于轻量级锁与重量级锁,在并发不是特别高的时候锁升级为了轻量级锁,轻量级锁的效率又会高于重量级锁。

  • 那么偏向锁、轻量级锁、重量级锁又是什么?
  • 偏向锁:我认为偏向锁的关键就是“”,偏向于第一个访问的线程。也就是说在无竞争的环境下,有一个线程访问的同步代码块,那么这个锁就会偏向这个线程,下次有线程访问的时候就会判断是不是之前访问过的线程访问,这样就会少一次cas的开销。因为第一次有线程访问同步代码块的时候会用cas把线程id写入mark word中。偏向锁会有一个延迟,程序刚启动的5s内不会出现偏向锁,这个我等会儿给大家证明一下,计算过hashcode值的对象不会加偏向锁,因为对象头没有空间放线程id了。
  • 轻量级锁:轻量级锁体现轻量的点就在于自旋,如果线程访问轻量级锁的同步代码块,会cas判断线程id是否一致,不一致会自旋一定的时间一直进行cas,如果cas成功就还是轻量级锁。如果失败了,然后轻量级锁就会升级为重量级锁。
  • 重量级锁:jvm层面的两个标识,加锁解锁都会阻塞其他线程。
  • Cas操作有三个参数,一个是旧值的地址,一个是旧值,还有一个是新值,如果从旧值地址取出来的值和旧值相等,那么就把旧值改为新值。
  • mark word是对象头的组成部分,对象头主要是有mark word与klass word,mark word通常会存放线程id信息、垃圾回收年限、锁的状态这些信息。klass word存放的是一个指针,指向对象的实例,也就是在堆里面的地址。通常这个指针为64bit,但是一般都会压缩成32bit。
干货满满的synchronized详解!!!
mark word
  • 从上面这张图中我们主要关注一下最后面的那2bit,这两个bit就代表了锁的状态。所以说一个对象有五种状态,无锁、偏向锁、轻量级锁、重量级锁、GC中(表示正在进行垃圾回收)。
  • 理论上2bit只能表示4种状态,那么为什么会有五种状态呢?细心的朋友就能发现偏向锁前一个bit也是用来表示锁状态的,也就是说还有1bit是用来判断这个锁是否偏向。
  • 所以在对象头中无锁是01,轻量级锁是00,GC是11,重量级锁是10,偏向锁是101
干货满满的synchronized详解!!!
锁升级详细流程图
  • 这是一张网上广为流传的锁升级图,看懂这张图绝对面试锁升级可以让面试官刮目相看,但是个人觉得这张图还是有点晦涩难懂。想要更深入的了解锁升级的过程就研究一下这张图吧~(如果看不清其实网上随便搜搜都能搜出来。。。当然也可以在公众号留言:锁升级图)

JOL打印对象头

说了这么多虚无缥缈的东西,能不能直观的看一看什么样的场景下synchronized会有锁升级呢?来,Java中有一个jar包叫jol-core,可以用这里面的API把对象的对象头打印出来。

无锁

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import static java.lang.System.out;

public class JOLExample1 {
    public static void main(String[] args) throws Exception {
        A a = new A();
        out.println(VM.current().details());
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

干货满满的synchronized详解!!!
无锁
  • 这里例子中对象A并没有加任何锁,所以肯定是无锁状态的,从控制台打印的结果也可以看到,VALUE的值就是对象头的值,前两位打印结果就是01,确实是无锁。怎么样,我没骗你吧~
  • 可能细心的小伙伴又发现了,我们前面说的是对象头最后两位是代表锁状态,为什么打印出来的又是前两位代表锁状态了呢?
  • 这里涉及到一个计算机中的概念叫大端模式与小端模式,现在的计算机都是小端模式,也就是字数据的低字节则存放在高地址,所以就产生了这样的结果。

偏向锁

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import static java.lang.System.out;
//没有竞争,理论上是偏向锁
public class JOLExample2 {
    static A a;
    public static void main(String[] args)throws Exception{
        a = new A();
        out.println("befre lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
        sync();
        out.println("after lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
    public static void sync() throws InterruptedException {
        synchronized (a){
            System.out.println("我也不知道要打印什么");
        }
    }
}

可以看到上面这个例子虽然给A加了synchronized,但是并没有竞争,根据我们面的分析就是偏向锁了,所以打印出来会是101吗?

干货满满的synchronized详解!!!
偏向锁1
  • 神奇的发现还是01,表示无锁。为什么呢?这里是因为偏向锁会有一个4s的延迟,修改虚拟机参数或者睡眠5s就可以看到101了
  • (XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0)
public class JOLExample2 {
    static A a;
    public static void main(String[] args)throws Exception{
        Thread.sleep(5000);//先停一个5s
        a = new A();
        out.println("befre lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
        sync();
        out.println("after lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
    public static void sync() throws InterruptedException {
        synchronized (a){
            System.out.println("我也不知道要打印什么");
        }
    }
}
干货满满的synchronized详解!!!
偏向锁2

看到没有,千呼万唤的偏向锁终于出来了

轻量级锁

public class JOLExample3 {
    static A a;
    public static void main(String[] args)throws Exception{
        Thread.sleep(5000);
        a = new A();
        out.println("befre lock");
         new Thread(()->{
            sync();
        }).start();
         Thread.sleep(5000);//保证上面这个线程执行完成
        out.println("after lock");
        //out.println(ClassLayout.parseInstance(a).toPrintable());
        sync();
        out.println("lock over");
        out.println(ClassLayout.parseInstance(a).toPrintable());

    }
    public static void sync() {
        synchronized (a){
            try {
                out.println(ClassLayout.parseInstance(a).toPrintable());
            }catch (Exception e){
            }

        }
    }
}
干货满满的synchronized详解!!!
轻量级锁

我们从图中可以看出来,一个线程给a加了锁之后还是偏向锁,因为没有竞争。而有另外一个线程又给a加了锁之后,就会变成轻量级锁了(注意这里其实还是没有竞争),退出synchronized代码块之后又会变成无锁的状态,因为轻量级锁会有一个锁撤销的过程。

重量级锁

public class JOLExample3 {
    static A a;
    public static void main(String[] args)throws Exception{
        Thread.sleep(5000);
        a = new A();
        out.println("befre lock");
         new Thread(()->{
            sync();
        }).start();
         //Thread.sleep(5000);//保证上面这个线程执行完成
        out.println("after lock");
        //out.println(ClassLayout.parseInstance(a).toPrintable());
        sync();
        out.println("lock over");
        out.println(ClassLayout.parseInstance(a).toPrintable());

    }
    public static void sync() {
        synchronized (a){
            try {
                out.println(ClassLayout.parseInstance(a).toPrintable());
            }catch (Exception e){
            }

        }
    }
}
干货满满的synchronized详解!!!
重量级锁

只需要在前面轻量级锁的案例中把睡眠5s注释就可以产生两个线程的竞争,即主线程与新建的线程发生竞争。可以看到打印的结果就是10,表示重量级锁。

synchronized的底层实现

synchronized底层
  • 首先来看看它是由那些部分组成的
  • Wait Set:那些调用wait方法被阻塞的线程放置在这里
  • contention List:竞争队列,所有请求锁的线程首先被放在竞争队列
  • entry list:contention中那些有资格的会被移入contention list
  • ondeck:任意时刻,最多有一个线程在竞争资源,该线程为ondeck
  • owner:当前获取到资源的线程
  • 再来详细说一下synchronized的大致工作流程
  • JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
  • Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
  • Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
  • OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完(Linux 内核下采用pthread_mutex_lock 内核函数实现的)。
  • Synchronized 是非公平锁。Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
  • 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

ENDING

看完这些大家应该对synchronized有一个非常全面且深入的了解吧~希望面试官问到你synchronized的时候你能用这篇文章学到的知识“手撕”面试官。

本次分享就到这里结束了,对于这样一篇干货满满的文章不点赞你的心不会痛吗~


学习群成立啦~

往期推荐


原文始发于微信公众号(FingerDance):干货满满的synchronized详解!!!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/26769.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!