美文网首页
synchronized原理

synchronized原理

作者: 在岁月中远行 | 来源:发表于2023-03-11 16:24 被阅读0次

1 synchronized简介

java中的关键字,在jvm层面上围绕着监管锁(Monitor Lock)的实体建立的,java利用锁机制实现线程同步的一种方式。

synchronized属于隐式锁,相比于显式锁如ReentrantLock不需要自己写代码去获取锁和释放锁。

synchronized属于可重入锁,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再此得到该对象的锁的,即synchronized块中的synchronized还是能马上获得该锁的。

synchronized为非公平锁,即多个线程去获取锁的时候,会直接去尝试获取,如果能获取到,就直接获取到锁,获取不到的话进入等待队列。

jdk1.6之前,synchronized属于重量级锁(悲观锁),jdk1.6优化之后被进行了大幅度优化,支持锁升级制度缓解加锁和解锁造成的性能浪费,锁的级别采用:偏向锁->轻量级锁->重量级锁。

2 synchronized的使用方法

它的使用方式主要为2种,分别是:

2.1 对普通方法加锁。即为对当前实例对象加锁,同一个类创建的不同对象调用该方法所获取的不同的锁,所以不会造成影响。

2.2 对静态方法加锁,静态方法属于类,同一个类创建的不同对象调用该方法是互斥的,此时的锁对象是class对象。

另外就是对方法块加锁:

锁是括号里面的对象,对给定对象加锁,进入同步代码前要获得给定对象的锁。

3 synchronized保证的特

1 原子性:synchronized依靠两个字节码指令monitorenter和monitorexit,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问。

2 可见性:JMM(java内存模型)规定,内存主要分为主内存和工作内存两种,每个线程拥有不同的工作内存,线程工作时会从主内存中拷贝一份变量到工作内存中。有时存在工作内存中的变量无法及时刷新到主内存中,或者工作内存无法及时获取主内存的最新值,导致共享变量在不同线程间处于不可见性,由此JMM对synchronized做了2条规定:

3.1 线程解锁前,必须把变量的最新值刷新到主内存中。

3.2 线程加锁前,先清空工作内存中的变量值,从主内存中重新获取最新值到工作内存中。

3 有序性:有时候编译器和处理器为了提升代码效率,会进行指令排序,但是这样可能对单线程没有啥影响,多线程会可能有影响。而synchronized保证了被修饰的程序在同一时间只能被同一线程访问,所以也就是保证了有序性。

4 synchronized实现对代码块加锁,需要依靠两个指令monitorenter和moniterexit,在进入代码块前执行monitor指令,在离开代码块前执行monitorexit指令。

对方法加锁并不依靠monitorenter和monitorexit指令,JVM可以从常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法。如果ACC_SYNCHRONIZED被设置了,则执行线程率先持有monitor锁,然后再执行方法,执行结束(或者发生异常并抛到方法之外时)释放monitor锁。

5 synchronized锁升级原理

jdk1.6之前是标准的重量级锁(悲观锁),jdk1.6之后进行了大幅度优化,支持锁升级制度缓解加锁和解锁造成的性能浪费,锁的状态总共有4种,无锁,偏向锁,轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁到轻量级锁,再升级到重量级锁,并且锁只能升级不能降级。

偏向锁:

经过大量研究发现,大多数情况下锁是不存在多线程竞争的,而且总是会由同一线程多次获得,因此为了减少同一线程加锁解锁的代价而引入偏向锁。而偏向锁的核心思想就是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作即可再次获取锁,这样就省去了大量有关锁申请的操作,从而也就提升程序的性能。所以对于没有锁竞争的场合,偏向锁有很好的优化效果,但是在多线程竞争锁的场合,偏向锁就失效了,这种场合就不应该使用偏向锁,否则得不偿失,偏向锁失败后,将会升级为轻量级锁。

轻量级锁:

轻量级锁是由偏向锁升级而来,它考虑的情况是竞争锁的线程不多,而且线程持有锁的时间也不长的情景,因为阻塞线程需要CPU从用户态转到内核态,代价极大,如果刚刚阻塞不久这个锁就被释放了,性能的浪费就太大了,因此这个时候干脆不阻塞线程,让它CAS自旋等待锁释放。自旋锁:

虚拟机为了避免多线程的竞争而使线程马上在操作系统层面挂起,还会进行一项称为自旋锁的优化手段,这是基于大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程过去浪费性能,因此自旋锁会假设在较短的时间内,当前的线程便可获取锁,因此虚拟机会让当前想要获取锁的线程做几个空循环原地等待(自旋),默认情况下自旋的次数是10次,经过若干次循环后,如果得到锁,就顺利进入临界区。但是如果自旋次数到了持有锁的线程还没释放锁,那么就会将线程在操作系统层面挂起,这就是自旋锁提升效率的优化方式。不过需要注意的是,自旋会消耗cpu,所以轻量级锁适用于那些同步代码块执行很快的场景。

重量级锁:

当轻量级锁膨胀到重量级锁之后,意味着线程只能被真正的挂起阻塞,然后等待被唤醒。

各个锁的优缺点:

锁粗化:

理论上说,编程时我们会尽量将锁限制在尽量小的范围内,仅在共享数据的实际作用域中才进行同步,目的是使需要同步的操作尽可能缩小,缩短阻塞时间。

但是加锁解锁会消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,锁粗化就是此时我们可以扩大加锁的范围,避免反复加锁和解锁。

锁消除:

JAVA虚拟机在编译时,通过对运行上下文的扫描,经过逃逸分析,去掉不可能存在共享资源竞争的锁,从而提高性能和响应时间。

锁膨胀:

就是锁由无锁,偏向锁,轻量级锁,重量级锁升级这个过程。

6 synchronized和Lock的区别:

synchronized编码更简单,锁机制由JVM维护,在竞争不激烈的情况下性能更好。Lock功能更强大更灵活,竞争激烈时性能更好。

性能不一样:资源竞争激烈的情况下,Lock性能会比synchronized好,竞争不激烈的情况下,synchronized比lock性能好,synchronized会根据锁的竞争情况,从偏向锁->轻量级锁->重量级锁升级,而且编写更简单。

锁机制不一样:synchronized是在JVM层面实现的,系统会监控锁的释放与否。Lock是jdk实现的,需要手动释放,在finally块中释放。

用法不一样:synchronized可以在代码块上,方法上。lock只能写在代码里,不能直接修饰方法。

synronized是非公平锁,Lock支持公平锁,但默认是非公平的。

lock的实现类ReentrantLock提供了lcokInteruptibly的功能,可以中断争夺锁的操作,抢锁的时候会判断是否被中断,中断会直接抛出异常,退出抢锁。

而synchronized只有抢锁的过程,不可干预,直到抢到锁之后,才可以编码控制锁的释放。

快速反馈锁:ReentrantLock提供了tryLcok()和tryLock(times)的功能,不等待或者限定时间等待获取锁,更灵活。可以避免死锁的发生。

读写锁:ReentrantReadWriteLock类实现了读写锁的功能,读锁可以并发地读取,写锁只能独占,而synchronized全是独占锁。

相关文章

网友评论

      本文标题:synchronized原理

      本文链接:https://www.haomeiwen.com/subject/qyvzldtx.html