大家好,我是程序视点的小二哥!今天我们来聊聊Java中的锁!
-
synchronized怎么用?
-
锁是什么?
-
偏向锁是什么?
-
锁如何升级?何为膨胀?
-
自旋锁何解?
-
互斥锁怎么来的?
-
何时要禁用偏向锁和轻量级锁?
带着上面疑问,我们一起来解“锁”疑惑!以上问题会分成几篇文章来讲,方便大家记忆!欢迎持续关注【程序视点】,这样就不会错过之后的精彩内容啦!
前言
每一个刚接触多线程并发编程的同学,当被问到,如果多个线程同时访问一段代码,发生并发的时候,应该怎么处理?
我相信闪现在脑海中的第一个解决方案就是用synchronized,让这段代码同一时间只能被一个线程执行。
synchronized的疑惑
我们也知道,synchronized关键字可以用在方法上,也可以用在代码块上,如果要使用synchronized,我们一般就会如下使用:
public synchronized void doSomething() { //do something here}
或者
synchronized(LockObject) { //do something here}
那么实际上,synchronized关键字到底是怎么加锁的?锁又长什么样子的呢?
关于锁,还有一些什么样的概念需要我们去认识,去学习,去理解的呢?
以前在学习synchronized的时候,就有文章说, synchronized是一个很重的操作,开销很大,不要轻易使用,我们接受了这样的观点,但是为什么说是重的操作呢,为什么开销就大呢?
到java1.6之后,java的开发人员又针对锁机制实现了一些优化,又有文章告诉我们现在经过优化后,使用synchronized并没有什么太大的问题了,那这又是因为什么原因呢?到底是做了什么优化?
那今天我们就尝试着从锁机制实现的角度,来讲述一下synchronized在java虚拟机上面的适应场景是怎么样的。
由于java在1.6之后,引入了一些优化的方案,所以我们讲述synchronized,也会基于java1.6之后的版本。
锁对象
首先,我们要知道锁其实就是一个对象,java中每一个对象都能够作为锁。 所以我们在使用synchronized的时候,
-
对于同步代码块,就得指定锁对象。
-
对于修饰方法的synchronized,默认的锁对象就是当前方法的对象。
-
对于修饰静态方法的synchronized,其锁对象就是此方法所对应的类Class对象。
我们知道,所谓的对象,无非也就是内存上的一段地址,上面存放着对应的数据,那么我们就要想,作为锁,它跟其它的对象有什么不一样呢?怎么知道这个对象就是锁呢?怎么知道它跟哪个线程关联呢?它又怎么能够控制线程对于同步代码块的访问呢?
Markword
可以了解到在虚拟机中,对象在内存中的存储分为三部分:
-
对象头
-
实例数据
-
对齐填充
其中,对象头填充的是该对象的一些运行时数据,虚拟机一般用2到3个字宽来存储对象头。
-
数组对象,会用3个字宽来存储。
-
非数据对象,则用2个字宽来存储。
其结构简单如下:
从上表中,我们可以看到,锁相关的信息,是存在称之为Markword中的内存域中。
拿以下的代码作为例子,
synchonized(LockObject) { //do something here}
在对象LockObject的对象头中,当其被创建的时候,其Markword的结构如下:
从上面Markword的结构中,可以看出 所有新创建的对象,都是可偏向的(锁标志位为01),但都是未偏向的(是否偏向锁标志位为0)
偏向锁
当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。
这说明此对象就要被当做一个锁来使用,那么其Markword的内容就要发生变化了。 其结构其会变成如下:
可以看到,
-
锁的标志位还是01
-
“是否偏向锁”这个字段变成了1
-
hash值变成了线程ID和epoch值
也就是说,这个锁将自己偏向了当前线程,心里默默地藏着线程id, 在这里,我们就引入了“偏向锁”的概念。
在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:
-
Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.
-
如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码
-
如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
-
如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
-
如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。
以下是Java开发人员提供的一张图:
“偏向锁”是Java在1.6引入的一种优化机制,其核心思想在于,可以让同一个线程一直拥有同一个锁,直到出现竞争,才去释放锁。
因为经过虚拟机开发人员的调查研究,在大多数情况下,总是同一个线程去访问同步块代码,基于这样一个假设,引入了偏向锁,只需要用一个CAS操作和简单地判断比较,就可以让一个线程持续地拥有一个锁。 也正因为此假设,在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。
下篇预告
在上面,我们说到,一旦出现竞争,也即有另外一个线程也要来访问这一段代码,偏向锁就不适用于这种场景了。
如何解决的呢?下篇文章将带大家了解锁膨胀、锁撤销、轻量级锁等内容!持续关注,这样就不会错过之后的精彩内容啦!
如果这篇文章对你有帮助的话,别忘了【点赞】【分享】支持下哦~