大家好,我是程序视点的小二哥!今天我们来聊聊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的时候,
  1. 对于同步代码块,就得指定锁对象。
  2. 对于修饰方法的synchronized,默认的锁对象就是当前方法的对象。
  3. 对于修饰静态方法的synchronized,其锁对象就是此方法所对应的类Class对象。
我们知道,所谓的对象,无非也就是内存上的一段地址,上面存放着对应的数据,那么我们就要想,作为锁,它跟其它的对象有什么不一样呢?怎么知道这个对象就是锁呢?怎么知道它跟哪个线程关联呢?它又怎么能够控制线程对于同步代码块的访问呢?
Markword
可以了解到在虚拟机中,对象在内存中的存储分为三部分:
  1. 对象头
  2. 实例数据
  3. 对齐填充
其中,对象头填充的是该对象的一些运行时数据,虚拟机一般用2到3个字宽来存储对象头。
  1. 数组对象,会用3个字宽来存储。
  2. 非数据对象,则用2个字宽来存储。
其结构简单如下:
长度
内容
说明
32/64bit
Markword
hashCode,GC分代年龄,锁信息
32/64bit
Class Metadata Address
指向对象类型数据的指针
32/64bit
Array Length
数组的长度(当对象为数组时)
从上表中,我们可以看到,锁相关的信息,是存在称之为Markword中的内存域中。
拿以下的代码作为例子,
synchonized(LockObject) { //do something here}
在对象LockObject的对象头中,当其被创建的时候,其Markword的结构如下:
bit fields
 
是否偏向锁
锁标志位
hash
age
0
01
从上面Markword的结构中,可以看出 所有新创建的对象,都是可偏向的(锁标志位为01),但都是未偏向的(是否偏向锁标志位为0)
偏向锁
当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。
这说明此对象就要被当做一个锁来使用,那么其Markword的内容就要发生变化了。 其结构其会变成如下:
bit fields
  
是否偏向锁
锁标志位
threadId
epoch
age
1
01
可以看到,
  1. 锁的标志位还是01
  2. “是否偏向锁”这个字段变成了1
  3. hash值变成了线程ID和epoch值
也就是说,这个锁将自己偏向了当前线程,心里默默地藏着线程id, 在这里,我们就引入了“偏向锁”的概念。
在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:
  1. Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.
  2. 如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码
  3. 如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
  4. 如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
  5. 如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。
以下是Java开发人员提供的一张图:
 
 
“偏向锁”是Java在1.6引入的一种优化机制,其核心思想在于,可以让同一个线程一直拥有同一个锁,直到出现竞争,才去释放锁。
因为经过虚拟机开发人员的调查研究,在大多数情况下,总是同一个线程去访问同步块代码,基于这样一个假设,引入了偏向锁,只需要用一个CAS操作和简单地判断比较,就可以让一个线程持续地拥有一个锁。 也正因为此假设,在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。
下篇预告
在上面,我们说到,一旦出现竞争,也即有另外一个线程也要来访问这一段代码,偏向锁就不适用于这种场景了。
如何解决的呢?下篇文章将带大家了解锁膨胀、锁撤销、轻量级锁等内容!持续关注,这样就不会错过之后的精彩内容啦!
如果这篇文章对你有帮助的话,别忘了【点赞】【分享】支持下哦~
posted on 2024-12-18 12:13  程序视点  阅读(37)  评论(1编辑  收藏  举报