并发编程—synchronized

临界区、临界资源

临界区:一段代码块内如果对共享资源的多线程读写操作,这段代码块称为临界区。

临界资源:共享资源为临界资源。

竞争条件

多线程在临界区内执行,由于代码执行顺序不同而导致结果无法预测,称之为发生了竞争条件。

避免临界区的竞争条件发生,可以采用下面方案:

  • 阻塞式:synchronized、Lock

  • 非阻塞式:原子变量

Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥:保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步:由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized的使用

synchronized 同步块式Java提供的一种原子性内置锁,Java种每个对象都可以当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内置锁,也叫监视锁

synchronized 实际是用对象锁保证了临界区内的代码的原子性。
synchronized 底层原理

synchronized 是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。JVM内置锁在1.5之后版本做了重大的优化,如:锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致 用户态内核态两个态之间来回切换,对性能有较大影响。
Monitor(管程/监视器)

管程(Monitor):管理共享变量以及对共享变量操作的过程,让它们支持并发。。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

MESA模型

管程种引入了条件变量的概念,而且每个条件变量都对应一个等待队列,条件变量和等待队列的作用是解决线程之间的同步问题。

wait()

MESA管程有一个编程范式:

while(条件不满足) {
   wait();
}
唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。
notify()和notifyAll()

满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():

  • 所有等待线程拥有相同的等待条件;

  • 所有等待线程被唤醒后,执行相同的操作;

  • 只需要唤醒一个线程。

Java语言的内置管程synchronized

Java参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简。MESA模型中,条件变量可以有多个,Java语言内置的管程里只有一个条件变量。

Monitor机制在Java中的实现

java.lang.Object类定义了wait()、notify()、notifyAll()方法,这些方法的具体实现,依赖于ObjectMonitor实现,这是JVM内部基于C++实现的一条机制。ObjectMonitor其主要数据结构如下(hotspot源码ObjectMonitor.hpp):

ObjectMonitor() {
    _header       = NULL; //对象头  markOop
    _count        = 0;  
    _waiters      = 0,   
    _recursions   = 0;   // 锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;    
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认的策略(QMode=0):如果EntryList为空,则将cxq中的元素按原有的顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。EntryList不为空,直接从EntryList中唤醒。
对象的内存布局

Hotspot虚拟机中,对象在内存中存储布局可分为三块区域:对象头(Header)实例数据(Instance Data)对齐填充

  • 对象头:hash码、对象所属的年代、对象锁、锁状态标志、偏向锁(线程)ID、偏向时间、数组长度(数组对象才有)等。

  • 实例数据:存放类的属性数据信息,包括父类的属性信息。

  • 对齐填充:由于虚拟机要求对象起初地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

对象头详解

HotSpot虚拟机的对象头包括:

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

  • Klass Pointer:对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。

  • 数组长度(只有数组对象有):如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度,4字节。

Mark Word如何记录锁状态

Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。

synchronized锁优化

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值