超细Java中synchronized解析(锁升级机制)

synchronized是java中的同步锁,其作用是解决多线程情况下访问共享内存的问题,其保证同一段代码块同时只能有一个线程执行。

那么为什么要有synchronized呢

这就要从cpu的结构说起,我们都知道cpu的三级缓存,通常一级缓存和二级缓存是核心独享的,而三级缓存是多核心共核的。

当cpu从主存中读数据时,需要先将主存中的数据读入到缓存中,而cpu修改数据时,修改的是缓存中的数据,这时别的线程从主存中读取数据读到的就是旧数据了,这就是可见性问题。

当编绎代码时,CPU或者编绎器可能会对指令进行重排序,导致实际执行顺序和代码的编写顺序不一致,这就是有序性问题。

这有一个原子性问题,如对一个int变量执行修改:a = 2; 虽然这是一行代码,但它并不是原子操作。

synchronized能保证同一时间只能有一个线程执行代码段,这就保证了原子性。

而synchronized在编绎之后就是monitor enter和monitor exit两个指令。monitor enter就是加锁,加锁的时候会使用读屏障,强行从主存中重新读取数据,保证读到的数据是最新的。monitor exit就说解锁,解锁的时候会使用写屏障,保证强制将cpu缓存中的变量数据同步到主存中,能够保证线程修改后的数据对其他线程立刻可见。这就保证了可见性。

synchronized能够通过内存屏障防止指令重排序,这就保证了有序性。

那synchronized怎么用呢?

修饰普通方法锁的就是this,this就是当前对象,一个对象用一把锁。

修饰静态方法锁的就是类的class对象,即类的所有对象共用一把锁。

修饰局部代码块就是当前代码块。

synchronized的锁升级

synchronized在jdk1.6之后进行了优化,引入了锁升级机制。无锁->偏向锁 -> 轻量级锁 -> 重量级锁。

为什么要做锁升级呢,这是因为synchronized低层用的还是内核的mutex机制,而mutex会导致线程的阻塞和切换,javae的线程模型和内核线程模型是一对一的。每次切换线程都需要从用户态切换到内核态,开销很大,这也是synchronized在jdk1.6之前称为重量级锁的原因。

在jdk1.6之后对synchronized进行了优化,即然线程切换比较慢,那在低并发的情况下,锁竞争比较少的情况下可以不让线程阻塞,减少内核态和用户态之间的切换,这就可以提高性能。当并发量比较高,锁竞争比较激烈的时候再让线程阻塞,这就产生了锁升级的过程。

大多数时候一个系统都不存在锁竞争,经常就只有一两个线程去拿锁,高并发往往发生在一些固定的时间点,比如说周未,双11,618期间,其它时间并发量并不大,为了在低并发的时候降低获取锁的代价,为了提高低并发时候的性能,所以就做了锁升级。

锁升级的过程是一个渐进的过程,其随着并发的上升逐步升级,

锁升级的过程

刚开始的时候我们的系统是无锁状态,当有第一个线程来访问同步代码块时,JVM将对象头的Mark word锁标志位设置为偏向锁,然后将线程id记录到mark word里面,这时该线程进入同步代码块时就不需要其他的同步操作了,执行非常的轻、非常的快。

为什么要有偏向锁呢,偏向锁考虑的是只有一个线程抢锁的场景,那什么时候升级到轻量级锁呢?

当第二个线程来抢锁就升级为轻量级锁,第二个线程拿不到锁就采用cas加自旋的方式不断重新尝试获取锁,这就避免了内核态的切换。另外自旋不是没有代价的,自旋虽然避免了内核态的切换却会让cpu一直空转,短时间的空转是没有问题的,但是一但任务执行时间较长,或大量线程抢锁导致大量线程自旋,就会导致大量的cpu浪费。

轻量级锁的自旋还是自适应性的自旋,自旋时间是根据上一次自旋的时间来决定的。

轻量级锁考虑的是竞争锁的线程不多,而且线程持有锁的时间也不长的一个场景。一旦线程持有锁的时间过长,这会大大增加锁升级到重量级锁的概率。

所以轻量级锁只放在了两个线程竞争的场景下使用。

轻量级锁什么时候升级到重量级锁呢?

当第二个线程自旋到一定次数之后还是没拿到锁,或者当有更多的线程来抢锁时,那就升级为重量级锁。重量级锁使用了内核的mutex机制,导致内核态和用户态的切换,即重量级锁。

我们已经知道,轻量级锁的自旋不适用于大并发量的情况(会导致cpu浪费),所以synchronized就会升级到重量级锁。把没拿到锁的线程都阻塞住。

当升级到重量级锁的时候,对象头的Mark word的指针就会指向锁监视器。

锁监视器主要用来负责记录锁的拥有者,记录锁的重入次数,负责线程的阻塞唤醒,锁监视器就是一个对象:

class ObjectMonitor{

void* _owner; // 持有锁的线程

WaitSet _WaitSet; // 等待池(管理调用wait()方法的线程)

EntryList _EntryList; // 锁池(管理因竞争锁失败而阻塞的线程)

int _recurisons; //记录锁的重入次数

...

}

_recurisons:记录锁的重入次数,synchronized的可重入性就是使用这个字段实现的,持有锁的线程每重入一次就加一,释放一次就减一。当为0时,就完全释放该锁。

_WaitSet: 等待池,当持有锁的线程调用wait()方法让出锁时,进入waiting状态,放入等待池中,当某个线程调用了notify唤醒了这个waiting的线程,那这个线程就从waiting状态变成了blocking状态,然后再被放入到锁池中,等待锁释放重新去抢锁。

_EntryList: 锁池,管理因竞争锁失败而阻塞的线程。

锁竞争失败的线程和调用wait方法的线程它们两个是有本质区别的,竞争锁失败那它的目标是尽快获取锁执行任务,这是锁的互斥问题。等待池放的是主动放弃锁的线程,暂时还不想要锁,这个线程等待被其它线程唤醒之后配合其他线程去完成某项任务,线程状态是waiting或者time waiting,这些waiting的线程是想等某个资源到位了然后再被notify唤醒,然后放入锁池中准备去抢锁做任务,这是线程通信问题。所有等待池和锁池它们的目标和要解决的问题完全是不一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值