并发(二):synchronized关键字

详细原地址并发(二):synchronized关键字

详细原地址并发(二):synchronized关键字

详细原地址并发(二):synchronized关键字

在这里插入图片描述

大纲内容
设置锁的意义
synchronized原理
synchronized锁升级

问题1:多线程场景下,如何争夺Monitor锁对象?
问题2:为什么Java中任意的对象都可以作为锁?

上文问题:为什么在DCL单例模式下,加了synchronized锁,代码块退出后,还要禁止指令重排序? 难道不是在持有锁的线程内,等重排序完之后,才会释放锁吗?

设置锁的意义
多线程场景下,存在多个线程访问同一个共享且可变的变量,这个变量称之为临界资源。
共享:存在主内存中的共享数据,共享数据对于所有线程而言都是共享的。
可变:各个线程把共享内存拷贝到自己的工作内存中,可以对该变量进行修改,修改完后,可同步进主内存中。

当多个线程访问一个共享并可变的变量时,将共享变量拷贝到自己的工作内存进行操作,线程间的工作内存相互隔离,线程间不存在线程不安全问题,但写回主内存时,可能造成线程不安全问题。
比如共享变量 int i=0;

线程A对i+1,线程B对i+1,加锁后正确的结果是2,但此时线程A和线程B可能都是拿到的是i=0,写回时,结果可能为1。得到了错误的结果就是线程不安全问题。

在并发情况下,首先我们要考虑的是线程安全问题,并发不一定是千亿级并发量,可能两个线程同时访问共享变量,也会造成线程不安全问题。

解决并发
单机系统采用synchronized和lock锁,有序性访问临界资源,把并行改成了串行,即在同一时刻,只能由同一个线程,访问共享临界资源。
分布式系统,常见的采用redis锁。

synchronized原理
synchronized内置锁是一种对象锁,锁的是对象头,可以用来保证多线程下,持有该对象锁的线程可以访问临界资源,其他未持有锁的线程会阻塞,必须等待持有对象锁的线程释放完锁后,才能尝试去获取锁,获取成功后才能访问临界资源。
synchronized是可重入锁,它的底层原理是通过一个Monitor对象来实现的,该对象又是以操作系统的互斥锁来实现的,wait/notify等方法也依赖于monitor对象,这就是为什么wait/notify只能在同步代码块执行的原因了。

synchronized是通过内部对象,Monitor监视器锁来实现的,基于进入monitorenter和monitorexit来判断是否持有锁,两个指令之间就是同步代码块,字节码文件中会生成monitorenter和monitorexit两个指令来表示进入同步代码块和离开同步代码块。

monitorenter
getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
ldc #4
invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
aload1
monitorexit
// JVM存在同步消除,当JVM发现只有一个线程访问同步代码块,表示同步消除
// 会加一个monitorExit指令
一个对象就是一把锁,是一对一的关系的,synchronized在JVM里的实现是基于进入和退出monitor对象来实现代码块同步的。

图片

MonitorEnter:每个对象都是一个监视器对象,当monitor被占有时,说明被线程持有该对象锁,线程执行MonitorEnter指令时,会尝试去获取monitor的使用权,每个线程都有执行MonitorEnter指令的权利,但只会有一个线程获取到Monitor的使用权。
1:如果monitor的进入数为0,则该线程成功获取到monitor的使用权,并将计数器+1。
2:如果当前线程已经占有monitor的使用权,表示可重入,及计数器自增+1
3:如果其他线程已经占有monitor的使用权,那么该线程会进入阻塞队列中,直到monitor的计数器为0时,表明持有锁的线程释放了锁,其他该线程才再尝试去获取monitor锁。

MonitorExit:执行monitorExit一定是Monitor监视锁的持有者,谁获取到Monitor的使用权,谁才能用monitorExit,执行一次该指令,计数器就减1,直达计数器为0时,说明获取Monitor锁的线程释放完锁。

Monitor监视锁的实现是依赖于操作系统的互斥锁(Mutex lock)来实现的
synchronized锁是Monitor监视器锁来实现的,而该锁的底层是操作系统的互斥锁来实现的。

个人理解:一个对象(实例)就是一把锁。
同步实例方法:锁的是当前实例对象。
同步类方法:锁的是当前类对象。
同步代码块:锁的是括号中的对象。

注意点:同步代码块时,括号中的对象不能是字符串和整型数字,因为每次都是生成了一个新的对象!比如(1)->其实每次都new Integer(1);

在Java代码中,Monitor对象是由底层的C++实现的,具体代码如下
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数,就是计数器,计数器为0则标识可获取锁。
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //当前持有monitor锁对象的线程。
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 获取锁失败的线程会阻塞,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
count:是一个计数器,当值为0时,说明当前锁未被任何线程获取,当>0时,说明有一个线程持有当前锁,当count为10时,对于的monitorExit也要执行10次。

owner:持有当前Monitor对象的线程

EntryList:尝试争取锁的线程都位于该集合中,这是一个阻塞队列

WaitSet:当线程调用wait()方法时,会把线程放入条件等待队列中
为什么要引出条件等待队列和阻塞队列的概念?

ReentrantLock也具备阻塞队列和条件等待队列,该锁可以有多个条件等待队列,

而synchronized只能有一个条件等待队列。

问题1:多线程场景下,如果争夺Monitor锁对象?
1:当多个线程同时进入一个被synchronized修饰的代码块时,首先会进入EntryList集合中,当有线程成功获取锁对象时,owner属性指向该线程,同时count计数器+1。
2:当获取锁的线程调用wait()方法时,会将自己放进waitSet集合中,同时释放Monitor锁对象,owner指向null,同时count设置为0,在waitSet集合中等待被唤醒,其他线程尝试去获取锁。
3:若该线程执行完代码块,会释放monitor锁,同时owner指向为null,count设置为0,其他线程尝试去获取锁。

问题2:为什么Java中任意的对象都可以作为synchronized锁?
每个对象都有对象头,Monitor对象存在于每个Java的对象头中,对象头中都存在一个锁标志位,锁标识位记录着锁升级的标识,对象头由mark word,类型指针,数字长度组成,在JVM虚拟机中,对象在内存中存储的布局分为三部分:对象头,实例数据,对齐填充。

对象头
MarkWord:记录对象的hash码,对象所属的年龄代,对象锁,锁状态,偏向锁ID等

类型指针:指向方法区中的类信息指针。

数组长度:记录数组对象的长度。

实例数据:记录类的属性信息,包括父类的信息

对齐填充:对象起始地址必须是8字节的整数倍,目的是为了字节对齐。
后续会在JVM中详细讲解该知识点

synchronized锁升级
在对象头的MarkWord中存在锁标识,synchronized的锁对象是Monitor实现的,Monitor的底层锁是由操作系统的互斥锁实现的,如果频繁调用操作系统来加锁,会造成用户态和内核态的频繁切换,这也是我们常说的消耗锁资源。
用户态和内核态是操作系统的术语。
ssynchronized是存在锁升级的,在对象头的锁标识中,一共有四种状态,无锁,偏向锁,轻量级锁,重量级锁。顺序升级锁状态,锁一旦升级是不会出现降级的。

偏向锁:在大多数情况下,同一时刻并不存在大量多线程竞争的场景,而且大部分都是由一个线程多次获得,为了减少消耗锁的资源,当一个线程获取锁时,先将无锁升级成偏向锁,此时修改markWord中的锁标识为01,持有偏向锁的ID为当前线程,标识该锁是偏向锁,在大多数情况下,可能都是同一个线程连续的访问相同的锁,当这个线程再次进入代码块时,无需再做任何同步操作,直接执行代码块。

轻量级锁:当出现锁竞争时,偏向锁会撤销,升级成轻量级锁,修改markWord中的锁标识为00,标识该线程持有的锁是轻量级锁,在该线程的栈内创建锁记录,锁记录主要是保存markWord中的信息,同时让对象头中的markWord的锁记录指针指向该线程的栈,则说明该线程获取到轻量级锁,其他线程尝试获取轻量级锁时,会自旋(空死循环),

自旋的目的:多线程场景,尝试获取轻量级锁的线程获取失败,会进行自旋的优化手段,在大多数情况下,线程持有锁的时间不会很长,因此JVM虚拟机会让当前想要获取锁的线程,先等一等,如果超过一定次数,才升级为重量级锁,又操作系统从用户态转为内核台,把线程挂起,这个操作是耗时的。

重量级锁:操作系统从用户态转为内核态,挂起线程。

总结:偏向锁是比较MarkWord中的持有偏向锁的线程Id是否是当前线程,轻量级锁是比较MarkWord中的锁记录指针是否指向当前线程栈中的锁记录,重量级锁比较暴力,直接从操作系统中挂起线程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值