Java 并发编程 以及引申出来的原理(1)-- synchronized

(前言)

本章就讲synchronized的用法,以及由此而来的一些引申玩意。

(一)synchronized  基本含义 & 用法

synchronized(英 ['sɪŋkrənaɪzd])字面含义:“同步”。简而言之,就是被修饰的同步代码块(方法)在同一时间内只有一个线程运行。

通常情况下,用来修饰普通成员方法、静态成员方法、代码块。实际中比较常见的做法是用来修饰代码块。

在使用意义上来说,synchronized修饰的范围(代码块、类静态方法、类成员方法)就像一段独木桥一样。这独木桥同一时间内,保证只能有一人在桥上行走,有点千军万马最后还得同步到一条路上的意思。

为了实现这个目的,synchronized使用(借用)了一个排他锁:需要上桥的线程,拿到唯一的钥匙,上锁,执行工作;在该线程没有结束这段独木桥之旅以前,任何其他企图上桥的线程因为没有钥匙,无法上桥,只能暂时等待。待执行线程下桥后,归还钥匙,后续其他(也可能还是本线程)有机会得到钥匙上桥。

synchronized 能够简单方便的解决并发编程中一些常见问题:例如安全实现多个线程对同一个资源的共同访问。

(二)synchronized  究竟是谁上了谁的锁

synchronized 本身并没有一个锁。或者说,使用synchronized关键字并不会导致JVM为你生成一把新锁。它借用了万物之父Object中自带的锁(有地方称该锁为  对象的内部锁)。

这里需要引入object的内存模型:


(图:(实例)(类)对象内存结构)

实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

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

对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。图中LockWord后的三位数据(图中001)作为锁状态数据(记录锁的状态、类型(见后续))。当对象的内部锁被一个某个线程持有时,lockword中的信息会指向该线程持有的Monitor Record地址。


(图:mark word包含的数据结构)

那Monitor Record是一个什么东西?如果我们理解同步操作时是线程拿到了一个对象的内部锁,也就不难分析出 Monitor Record应该是和线程有关。

Monitor Record是线程私有的数据结构,每一个线程都有一个可用monitor record列表(多个MoniterRecord),同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor record关联(对象头中的LockWord指向monitor record的起始地址,由于这个地址是8byte对齐的所以LockWord的最低三位可以用来作为状态位(就是上图中的001)),同时monitor record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。如下图所示为Monitor Record的内部结构:

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;

EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。

RcThis:表示blocked或waiting在该monitorrecord上的所有线程的个数。

Nest:用来实现重入锁的计数。

HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。


 

(三) JVM对同步操作的的优化

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。

不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。以上几种锁的升级策略:偏向锁 》  轻量级锁 》重量级锁。


锁粗化(Lock Coarsening)

也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。

锁消除(Lock Elimination)

通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。

轻量级锁(Lightweight Locking)

这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。

偏向锁(Biased Locking)

是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。

自旋锁

自旋锁其实是一种操作(适应性自旋(Adaptive Spinning))。轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

 

(四)synchronized 基于实现原理的效率分析

 在JDK6 以后由于各种优化策略,使synchronized的效率在相当多的场景下并不会太差。

当仅有少量线程(绝大部分时间里只有一个线程执行时,甚至没有线程执行),同步代码块执行时间较短时,synchronized的效率还是可以接受的。(偏向锁、自旋锁策略有效)

但是在大量并发线程竞争的情况下,由于大量采用重量级锁的原因,会导致效率急剧下降。

 

 

 

 

参考文档:

http://www.cnblogs.com/javaminer/p/3889023.html

http://blog.csdn.net/u012465296/article/details/53022317

https://www.cnblogs.com/mingyao123/p/7424911.html


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值