Synchronized原理解析

1.Synchronize的使用示例

1.静态方法使用synchronized修饰,表示锁住的是当前类的class对象(this.class)由于class对象会一直存在,所以这种形式可以用在两个代码块的同步,而不用担心所对象被回收;

2.普通方法用synchronized修饰,表示锁住的是当前对象(this);

3.synchronize代码,块锁住的是在括号里指定的对象。

 

2.Synchronized介绍

在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为 重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些 情况下它并不那么重了,Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

3.一些概念

阻塞的代价:

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个 线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的cpu 资源。如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比 用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问 题,JVM从 1.6开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

Markword:

Hotspot中对象的存储结构:

其中对象头的第一部分就是markword。

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。

4.synchronized的原理

在JDK6之前的我们就不多提了,它是重量级的锁,完全保证顺序执行,如果获取锁失败线程会阻塞直到锁被释放再继续去竞争,其中竞争的过程是非公平的。

在JDK6之后,synchronized变成了一个可以进化的锁,这个锁会根据情况不断变化加锁机制,引入了自旋锁,偏向锁,轻量级,大幅度提升了效率。

*:锁的进化我们无法感知,都是JVM自动帮我们做的,不需要我们干预

 

下面我一步一步讲解synchronized是如何被优化的,是如何从偏向锁到重量级锁的。

在当我们创建一个对象并且当有线程进入同步代码块对这个对象加锁的时候,首先创建出来的是一个偏向锁,然后这个偏向锁会膨胀为轻量级锁,最终可能会变成重量级锁,类似于数据库中间隙锁的降级,一步步根据情况退化。

大体过程如图:

 

偏向锁(锁资源没有竞争的时候的锁形态):

偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过 程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。
也就是说:
在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:

Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.

如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.

如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。

如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。

如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销(升级成轻量级锁),但大部分情况下是偏向撤销。

重新偏向

一个同步代码块已经退出,但是hotspot中退出同步代码块的时候并不会更新对象头的信息,线程ID还是原来的线程。

由于这种情况,当前另一个线程的同步代码块再去获得这个锁的时候,会先判断之前的线程是否已经退出同步代码块,如果退出了的话,就会将所对象中的线程ID修改为当前线程的ID,也就是这个锁对象重新偏向当前线程了。

锁撤销

由于偏向锁失效了,那么接下来就得把该锁撤销,锁撤销的开销花费还是挺大的,其大概的过程如下:

1.在一个安全点停止拥有锁的线程。

2.遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。

3.唤醒当前线程,将当前锁升级成轻量级锁。
锁膨胀

刚才说了,当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。这也是我们经常所说的锁膨胀


线程获得锁之后就不会再有解锁等操作了,这样可以省略很多开销。假如有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。
为什么要这样做呢?因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。这也是为什么会有偏向锁出现的原因。
在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。

所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭

轻量级锁:

轻量级锁主要有两种:

自旋锁

自适应自旋锁

所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。
经验表明,大部分同步代码块执行的时间都是很短很短的,也正是基于这个原因,才有了轻量级锁这么个东西。

自旋锁的一些问题:

  1. 如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu,这会让人很难受。
  2. 本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。

基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁
默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改。

自旋锁是在JDK1.4.2的时候引入的

所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。
其大概原理是这样的:
假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释 放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数
另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。

轻量级锁也被称为非阻塞同步乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待,串行执行。

重量级锁

轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor监视器来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁

重量级锁流程:

由于sybchronized是非公平的锁,所以在锁池的唤醒是随机的,不是按照先进先出的顺序唤醒的
synchronized被优化之前就是一个重量级锁。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

自旋锁:

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

 

但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。

 

如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

 

自旋锁的优缺点

 

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

 

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。

自旋锁时间阈值

 

自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

 

JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值