面试官常问的synchronized关键字底层原理

synchronized关键字底层原理

首先阅读这篇文章之前你需要知道:

什么是并发问题?

举个例子:

我们去银行取钱,我们卡里有500元,假如一个人是通过网银操作,另一个人是通过ATM操作,即模拟两个线程。网银准备取500,读取银行卡信息,这时余额是500元,可以取,取完之后余额会变成500-500=0元。

但是在我们取出钱之前,我们ATM的小伙伴也准备取500,此时余额是500元可以取钱,并且成功取出了,余额变成0元。此时网银的小伙伴也取到钱,用0元覆盖了0元,即一共取出了1000元。

当然实际中即便相差几微秒银行也不可能让我们多取钱的。

简单说明:

两个线程同时操作一个数据,这时候A线程读取到了数据并进行修改,但是在修改前,B线程也对该数据进行了修改,这时候我们的A线程才完成修改,覆盖了B线程修改结果。

所以我们的并发问题出现的条件就是多个线程同时操作一个共享的资源时才可能发生。

什么是synchronized

官方解释:

  • 同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。

一句话总结出Synchronized的作用:

  • 能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果

Synchronized的地位:

  • synchronized是Java的关键字,是最基本的互斥同步手段。

那么synchronized到底是怎么保证线程的安全的呢?

  • synchronized 是利用锁机制来实现同步的!

锁机制有如如下两个特征:

  • 互斥性:即在同一个时间允许一个线程持有某个对象锁,通过这种特征来实现线程的协调机制,这样在同一个时间只有一个线程对需要同步到代码块(复合操作)进行访问。互斥性我们有常常成为操作的原子性。
  • 可见性:必须保证在锁被释放之前,对共享变量所做的修改进行更新,对于随后获取该锁的对象的另一个线程是可见的(即在获得锁时赢获得最新共享变变量的值),否则另一个线程可能是在本地缓存的另一个副本上继续操作从而引起不一致。

synchronized的用法

synchronized关键字可以写在方法的修饰符上,也可以作为一个代码块,包裹一些需要被同步执行的代码。

方法上

//定义在普通方法上
public synchronized void method{}
//定义在静态方法上
public synchronized static void method{}

方法内

class SynchronizedDemo{
    
    public static void main(String[] args){
        Object object=new Object();
        //传入一个普通对象
        synchronized(object){
            ...
        }
        //传入一个class
        synchronized(SynchronizedDemo.class){
            ...
        }
        //传入this
		synchronized(this){
            ...
        }        
    }
}

其中我们无论是传入一个class,object还是this,本质上还是传入一个Java对象,实际上我们的方法也是隐式的使用了this或者.class。

按照分类我们可以分为:

  • 对象锁:以对象作为锁,比如synchronized传入一个普通对象或者this

  • 类锁:以class作为锁,本质也是对象锁

synchronized的原理

监控工具分析

我们创建5个线程,让每个线程睡个2分钟,通过Java自带的一些方式进行监控

@Test
public void test(){
    for(int i=0;i<5;i++){
        new Thread(()->{
            try{
                Thread.sleep(1000*120);
            }
            catch(Exception e){
                //
            }
        }).start();
    }
}

线程堆栈分析(互斥)

在我们Java安装目录的bin我们可以知道一个叫JConsole的文件,这是Java自带的监控工具。

我们0号线程获得了执行权,由于在sleep,所以状态是TIMED_WAITING

image-20200904143650847

我们看一下其他线程,都是BLOCKED状态,并标注了锁的拥有者是Thread0

image-20200904143700174

2分钟之后,线程Thread-0执行完毕,死亡。Thread4抢到执行权。

image-20200904143726987

jstack pid指令分析

可以使用这个指令获得当前虚拟机配置,具体大家可以查网上的专门这个指令的文章。

image-20200904143933102

通过上述过程,我们可以看到多个线程访问同一个synchronized代码块的资源时,只有一个线程是处于执行状态(图中是TIMED_WAITING状态),其他线程都是BOKCED状态,那么我们synchronized关键字是如何实现的呢?

对于HashMap这些集合框架的原理,我们可以从源码中找答案,但是对于这种Java关键字,我们要从JVM指令中寻找答案。

JVM指令分析

对象锁

创建Java文件

class SynchronizedDemo{
	public static void main(String[] args){
		Object object=new Object();
		synchronized(object){
			System.out.println(123);
		}
	}
}

javap 反编译命令

C:\Users\Faker\Desktop       
λ javac SynchronizedDemo.java
                             
C:\Users\Faker\Desktop       
λ java SynchronizedDemo 
123
C:\Users\Faker\Desktop       
λ javap -c SynchronizedDemo 

反编译结果

我们看到加入了synchronzed关键字之后我们的字节码文件中出现了monitorenter和monitorexit指令(不加synchronize关键字包裹是不出现的,这里我不贴图了)

image-20200913192400054

什么是monitorenter和monitorexist?为什么有一个monitorenter但是有两个monitorexit呢?

monitorenter:

当我们进入同步代码块的时候会先执行monitorenter指令,每一个对象都会和一个monitor关联,监视器被占用时会被锁住,其他线程无法来获取该monitor。当其他线程执行monitorenter指令时,它会尝试去获取当前对象对应的monitor的所有权。
两个重要成员变量:

  • owner: 当一个线程获取到该对象的锁,就把线程当前赋值给owner。

  • recursions:会记录线程拥有锁的次数,重复获取锁当前变量会+1。

monitorenter执行流程:

  1. 若monitor的进入次数为0,线程可以进入,并将monitor进入的次数设为1,当前线程成为montiro的owner;
  2. 若线程已拥有monitor的所有权,允许它重入monitor,进入一次次数+1 ;
  3. 若其他线程已经占有monitor,当前尝试获取monitor的线程会被阻塞,直到进入次数为变0,才能重新被再次获取。

monitorexit:

能执行monitorexit指令的线程,一定是拥有当前对象的monitor所有权的。当执行monitorexit指令计数器减到为0时,当前线程就不再拥有monitor所有权。其他被阻塞的线程即可再一次去尝试获取这个monitor的所有权。

上面编译出来的指令,其实monitoreexit是有两个:需要保证如果同步代码块执行抛出了异常,则也需要释放锁对象。

类锁

我们知道static静态方法上加入同步修饰,默认使当前类的class作为锁。

class SynchronizedDemo{
	public static void main(String[] args){
		run();
	}

	public static synchronized void run(){
		System.out.println(123);
	}
}

反编译结果

我们看到方法内并没有出现monitorenter和monitorexit,但是我们方法的标记flags中,我们看到了一个ACC_SYNCHRONIZED标识,表示这个方法是个同步的方法,我们执行这个方法前会自动判断是否上锁,若果没有就获得执行权并上锁,在执行完或者发生异常都会释放锁。

image-20200913193635781

synchronized优化

通过上面的讨论现在我们对Synchronized应该有所印象了,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。这种方式肯定效率低下,每次只能通过一个线程,既然每次只能通过一个,这种形式不能改变的话,那么我们能不能让每次通过的速度变快一点了。打个比方,去收银台付款,之前的方式是,大家都去排队,然后去纸币付款收银员找零,有的时候付款的时候在包里拿出钱包再去拿出钱,这个过程是比较耗时的,然后,支付宝解放了大家去钱包找钱的过程,现在只需要扫描下就可以完成付款了,也省去了收银员跟你找零的时间的了。同样是需要排队,但整个付款的时间大大缩短,是不是整体的效率变高速率变快了?这种优化方式同样可以引申到锁优化上,缩短获取锁的时间,伟大的科学家们也是这样做的,令人钦佩,毕竟java是这么优秀的语言(微笑脸)。

总结

我们发现synchronized底层使用锁机制来解决并发问题,底层使用Java对象锁来实现的,而我们synchronized使用的锁有哪些类型,哪些性质,我们下回分解。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值