面试必备系列JUC(4) -- synchronized超详解


前言

岳不群是个江湖人,而笑傲江湖中的江湖,只和选择有关。岳不群,林平之,令狐冲,都是因其选择而成其人。令狐冲若是陷入林平之的境地,也不会成为林平之,正如他若在岳不群的境地,也不会成为岳不群。
今天我们不去评判岳不群的真伪,依旧把他当作那个立志发扬气宗的君子剑吧。
在这里插入图片描述


一、synchronized基本认识

令狐冲:师傅,徒儿前段时间去福报厂面试了,那个黑脸面试官问我知道synchronized是啥不?了解原理吗?徒儿一时没有回答出来。。。

令狐冲:师傅能不能给徒儿讲解一下。。。

岳不群:synchronized是Java中的一个关键字,在多线程共同操作共享资源的情况下,可以保证在同一时刻只有一个线程可以对共享资源进行操作,从而实现共享资源的线程安全。

令狐冲:那师傅能说说Synchronized的特性吗?是不是也满足JMM模型?

岳不群:是的,冲儿,依旧满足原子性、可见性、有序性。这也是使用synchronized的原因。

原子性synchronized可以确保多线程下对共享资源的互斥访问,可以实现原子性。
可见性synchronized保证对共享资源的修改能够及时被看见
有序性synchronized可以有效解决重排序问题

二、synchronized的使用

岳不群:冲儿,在应用Sychronized关键字时,有几个注意点你一定要记牢了

一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁;
synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁

令狐冲:师傅,徒儿都已经记下了,写代码的时候,绝对不会再犯这些错误!

岳不群:冲儿,你知道synchronized有几种用法吗?

令狐冲:这个我知道,**Synchronized一共有三种用法:修饰普通方法、修饰静态方法、修饰代码块。**这部分我可以讲讲。

2.1 修饰普通方法

令狐冲:师傅你看这个例子:

//同步非静态方法 ,当前线程的锁便是实例对象methodName
    public synchronized  void methodName() {
        try {
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName()+"   bbb");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

令狐冲:当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁, 所以无法访问该对象的其他synchronized实例方法。

岳不群:但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同。

岳不群:此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了。

令狐冲:哦哦,师傅刚刚点播完之后,我这些对synchronized修饰普通方法的使用更加熟悉了。

2.2 修饰静态方法

令狐冲:师傅我再说下synchronized如何修饰静态方法的吧。

//同步静态方法
    public synchronized static void methodName() {
        try {
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName()+"   bbb");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(SynchronizedDemo::methodName).start();
        }
    }

令狐冲:当synchronized作用于静态方法时,其锁就是当前类的class对象锁。 由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。

岳不群:不错,需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,知道为什么吗?

令狐冲:因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。师父我说的对吗?

岳不群:说的很对!

2.3 修饰代码块

令狐冲:师父,我再讲讲synchronized如何修饰代码块吧,其实修饰代码块的锁对象一共有两种,一种是当前对象this,一种是当前类class。

//修饰非静态方法
public void methodName() {
    //修饰代码块,this=当前对象(谁调用就指待谁)
    synchronized (this) {
        try {
            TimeUnit.SECONDS.sleep(5);
            System.out.println(Thread.currentThread().getName() + "   bbb");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

岳不群:例子写的不错呀。这就是synchronized(this|object) {},在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。 类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。

令狐冲:师父,您再看下synchronized的锁对象为class类的情况:

//修饰非静态方法
public void methodName() {
     //修饰代码块,使用Class类
     //使用ClassLoader 加载字节码的时候会向堆里面存放Class类,所有的对象都对应唯一的Class类
     //SynchronizedDemo.class 这里拿到的就是堆里面的Class类,也就是所有的Class的对象都共同使用这个synchronized
     synchronized (SynchronizedDemo.class) {
         try {
             TimeUnit.SECONDS.sleep(5);
             System.out.println(Thread.currentThread().getName() + "   bbb");
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     }
 }

岳不群:写的很好。synchronized(类.class) {}:在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。

令狐冲:师傅我晓得了。但是我对synchronized为什么会有这些特性还是挺好奇的。。

岳不群:那我就给你讲讲synchronized的原理吧,这个很重要,面试官就爱问!!

三、synchronized实现原理

3.1 同步代码块解析

令狐冲: 师父,我已经知道了synchronized咋用的了,面试的时候也问了,但是他问我背后的原理,我还是答不出来。。

岳不群:不要着急,我讲给你听。咱们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的。

public class SynchronizedTest {
    public void method(){
        synchronized (this){
            System.out.println(" method  start");
        }
    }
}

岳不群:然后反编译下:

岳不群:你只关注红色方框里的monitorenter和monitorexit即可。你能看出来啥吗?

令狐冲:听说,synchronized的底层是监视器锁,监视器锁的底层是互斥锁,那monitorenter就是互斥入口,monitorexit就是互斥出口吧?

令狐冲:并且Monitorenter和Monitorexit指令,会让对象在执行的时候,使其锁计数器加1或者减1。

岳不群:说的很对,每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,一共有三种情况:

1)如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

​ 2)如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

​ 3)如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

令狐冲:那monitorexit怎么会有两个呢?

岳不群:monitorexit用于释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

岳不群:monitorexit有两个,一个是正常出口,一个是异常出口

令狐冲:原来是这样啊,那个面试官问我我当时没有回答出来。

岳不群:通过这两段描述,你应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象, 这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

3.2 同步方法分析

令狐冲:师傅,你前面讲了synchronized在同步代码块中的原理,那在同步方法中,也是这个样子吗?

岳不群:稍微有点不一样,你看下这个例子:

public class SynchronizedTest {
    public synchronized void method() {
        System.out.println(" method start");
    }
}

将代码反编译一下,得到以下结果:

令狐冲:从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。

岳不群:你观察的很仔细,JVM就是根据ACC_SYNCHRONIZED标示符来实现方法的同步的,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。

岳不群:在方法执行期间,其他任何线程都无法再获得同一个monitor对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

令狐冲:这下我都明白了,下次面试的时候,我就这样回答。

岳不群:只回答到这还是不够的,你再看下对象、对象监视器、同步队列以及执行线程状态之间的关系:
在这里插入图片描述
岳不群:这个图非常重要,冲儿你看出什么了吗?

令狐冲:从这图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

岳不群:看样你真的理解了!

3.3 synchronized的可重入原理

令狐冲:师傅,我记得之前读JVM书籍的时候,书上说synchronized是可重入锁,这个是咋回事呢?我没懂。。

岳不群:从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。

岳不群:在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

令狐冲:额,嗯,,,,,迷迷糊糊的

岳不群: 还是看个例子吧:

public class Accounting implements Runnable{
    static Accounting instance=new Accounting();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000;j++){
            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }
    public synchronized void increase(){
        j++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

岳不群:在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现。

令狐冲:哦哦,原来是这样呀。

岳不群:需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1.

岳不群:你要记住,Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

令狐冲:多谢师父,我记住了。synchronized原理和使用,都get了。

常考的面试题

1.Synchronized可以作用在哪里? 分别通过对象锁和类锁进行举例。
2. Synchronized本质上是通过什么保证线程安全的? 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。
3. Synchronized和Lock的对比,和选择?
4. Synchronized在使用时有何注意事项?
5. Synchronized修饰的方法在抛出异常时,会释放锁吗?
6. 多个线程等待同一个snchronized锁的时候,JVM如何选择下一个获取锁的线程?
7. 知道synchronized的监视器锁是咋回事吗?

===================================================
字节内推:
字节内推〉字节校招开启。简历砸过来!!!!!!!
200多个岗位,地点:北京 上海 广州 杭州 成都 深圳。。

字节内推码:B1RHWFK
官网校招简历投递通道:https://jobs.toutiao.com/campus/m/position?referral_code=B1RHWFK

===================================================
微信公众号:猿侠令狐冲

公众号定期更新文章!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值