admin 管理员组文章数量: 1086019
Synchronized 同步锁详解
大家好 我是积极向上的湘锅锅💪💪💪
1. 简介
Synchronized是JAVA的关键字,是基于JVM层面的独占式的悲观锁,同时也属于可重入锁
经常来用于解决多个线程之间访问访问资源的同步性,保证在同一时刻只有一个线程访问这个资源
2. 如何使用
-
修饰实例方法
当修饰实例方法的时候,锁的是该实例对象,进入同步代码前要获得当前对象实例的锁,使用的指令是monitor,线程试图获取锁也就是获取 对象监视器 monitor 的持有权每个对象都有一个monitor对象,加锁就是在竞争monitor 对象,方法加锁是通过一个标记位来判断的
-
修饰静态方法
也就是给当前类加锁,会作用于类的所有实例,不管再怎么new对象,这个静态方法始终只有一份,所以设想一个场景,一个线程A调用该类的非静态synchronized方法,一个线程B要调用该类的静态synchronized,这个是可以成功的,不会发生互斥现象 -
修饰代码块
这个加锁锁的就是咱们的实例对象,如果要进入代码块,就必须要先获得该对象的锁,同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置monitorenter会尝试获取锁,如果锁计数器为0,则获取锁成功,获取后奖锁计数器为置为1monitorexit会释放锁,将锁计数器重新置为0,让其他线程来获取锁
总结:
如果修饰是实例方法,代码块,JVM 会尝试获取实例对象的锁
如果是静态方法,JVM 会尝试获取当前 class 的锁
3. 核心组件
- Wait Set:那些调用wait方法的被阻塞的线程被放置在这里
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中
- OnDeck:任意时刻,只有一个线程正在竞争锁资源,该线程称为OnDeck
- Owener:当前已经获取到锁的线程称为Owener
- !Owener:当前释放锁的线程
4. Synchronized 实现
-
JVM每次从队列的尾部取出一个数据用于竞争候选者(OnDeck),但是在并发情况下,Contention List会被大量的线程进行CAS访问,所有会将一部分线程放到Entry List里面去
-
当Owener进行unlock解锁的时候,会将将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)
-
Owener线程并不直接把锁给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”
-
OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中
处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,当然不同情况的阻塞也不同
Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源
5. Synchronized 的劣势
我们知道Synchronized的monitor是基于操作系统的Mutex Lock实现的,而java是无法直接跟操作系统交互的,所以需要从用户态到内核态的转变,而这个转变是很花时间的,但是从Java1.6开始,synchronized 进行了很多的优化,有自旋锁、适应性自旋锁、偏向锁、轻量级锁、锁消除、锁粗化等技术来减少锁操作的开销。
6. Synchronized的锁优化
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁
-
自旋锁(轻量级锁)
原理是什么呢,其实就是在竞争锁资源的时候,发现锁已经被其他线程占有了,而如果持有锁的线程能在很短时间内释放锁资源,那么那些等待获取锁的线程就不必再做内核态到用户态的切换进入阻塞挂起状态,就只需要等一等(自旋),等锁释放之后去获取锁优缺点:
对于那些锁竞争不激烈的情况,且占用锁时间非常短,那么自旋锁就可以很大程度的提升性能,只要自旋的消耗小于线程阻塞再挂起的消耗,那就是有意义的
当然线程自旋是要消耗cpu的,我们需要设置一个自旋等待的最大时间,不然会一直占用cpu资源,当然,如果同一时间有很大的线程同时来获取锁,会导致锁的获取时间过长,这也是会导致线程自旋的消耗大于线程阻塞再挂起的消耗,所以这俩种情况同时成立,我们就要关闭自旋锁 -
适应性自旋锁
顾名思义,自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?是由前一次再同一个锁上的自旋时间以及锁的拥有者的状态决定,基本认为一个线程上下文切换的时间是最佳的一个时间,当然JVM还做了一些优化,感兴趣的可以了解一下 -
偏向锁
在某些不存在多线程竞争锁的情况,就可以使用偏向锁,目的是消除这个线程重入(CAS)的开销,看起来得到了偏护,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,只偏向锁需要在第一次置换ThreadID 的时候依赖一次 CAS 原子指令,而一旦出现多线程竞争的情况就必须撤销偏向锁 -
轻量级锁
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
偏向锁与轻量级锁:
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能
7. synchronized 锁升级原理:
- 在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
- 再次进入的时候会先判断threadid 是否与其线程 id 一致如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
- 执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级
本文标签: Synchronized 同步锁详解
版权声明:本文标题:Synchronized 同步锁详解 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1686729610a30215.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论