java 里面 的锁

Wesley13
• 阅读 942
A、乐观锁、悲观锁
B、偏向锁、轻量级锁、重量级锁
C、互斥锁、自旋锁、适应性自旋
D、可重入锁、读写锁
E、公平锁、非公平锁
F、总线锁、缓存锁(linux操作系统底层,由CPU提供的锁)

G、锁优化:减少锁持有时间、减小锁粒度、锁分离、锁粗化、锁消除

信号量与互斥量:信号量用于线程同步,互斥量用户保护资源的互斥访问
==================================================================================

Java对象头
Java Object Model中定义,锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。
第一个字长度的区域用来标记同步,GC以及hash code等,官方称之为 mark word。第二个字长度的区域是指向到对象的Class。
在2个word中,mark word是偏向锁、轻量级锁实现的关键。

synchronized锁流程

第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 
第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据Mark Word里面现有的Thread Id,通知之前线程暂停,之前线程将Mark word的内容置为空。 
第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的Mark Word的内容修改为自己新建的记录空间的地址的方式竞争Mark Word, 
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败
第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己
 
总结:
偏向锁,其实是无锁竞争下可重入锁的简单实现
==================================================================================


一、重量级锁
因为Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到内核态中,因此状态转换需要耗费很多的处理器时间,对于代码简单的同步块,状态转换消耗的时间有可能比用户代码执行的时间还长,所以synchronized是Java语言中一个重量级(Heavyweight)锁

>>  阻塞操作由操作系统完成的(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

二、轻量级锁
轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头Mark Word,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

总结:轻量级锁是通过CASCPU原语(Compare-And-Swap)来避免进入开销较大的互斥操作(重量级锁)。

三、偏向锁
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。

>>  当一个线程访问同步块并获取锁时,会在对象头和线程栈帧中的锁记录里存储锁偏向的线程ID(通过最开始的一次CAS),以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(看看当前是否还处于偏向锁的层次,因为锁会升级的),如果设置了,则当前仍处于偏向锁层次,只是还没有线程此刻占有锁,尝试使用CAS将对象头的偏向锁指向当前线程(释放锁时,会将对像头中纪录线程id的这个位置置空,以便其他线程获取该偏向锁);如果没有设置,表示当前可能已经升级到轻量锁甚至重量锁了,则使用CAS竞争锁。

>>  偏向锁:主要应用于单线程(没有多个线程)情况下对共享资源的使用。是在无竞争场景下完全消除同步,连CAS也不执行(CAS本身仍旧是一种操作系统同步原语,始终要在JVM与操作系统之间来回,有一定的开销)。它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。如果有其他的线程访问,则偏向锁升级为轻量级锁(注意:锁升级之后,无法降级)

==================================================================================

四、互斥锁
所谓互斥锁,指的是一次最多只能有一个线程持有的锁

互斥锁:只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞等待资源可用。

互斥锁:是一种信号量,常用来防止两个进程或线程在同一时刻访问相同的共享资源。可以保证以下三点:
    原子性:把一个互斥量(Mutex)锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量。
    唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。
    非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

    从以上三点,我们看出可以用互斥量来保证对变量(关键的代码段)的排他性访问。

五、自旋锁
从 实现原理上来讲,互斥量Mutex属于sleep - waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待(这个就是互斥锁)。而Spin lock则不然,它属于busy - waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止(这个就是自旋锁)。

所以,自旋锁一般用在多核的服务器上

适应性自旋
从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。


六、可重入锁
当一个线程在获取了锁之后,该线程可以多次获取自己所持有的锁(这时候仅仅是把状态值进行累加)。这就是可重入锁
被累加的状态值需要被减到0,这个线程才算真正的把锁给释放了。
synchronized、Reentrant都属于可重入锁

参考:http://blog.csdn.net/yanyan19880509/article/details/52345422 


七、读写锁(ReentrantReadWriteLock)
当有写线程时,则写线程独占同步状态。

当没有写线程时只有读线程时,则多个读线程可以共享同步状态。

在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)
仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

综上:
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;
写锁可以“降级”为读锁;
读锁不能“升级”为写锁。

总结:
读写锁还是很实用的,因为一般场景下,数据的并发操作都是读多于写,在这种情况下,读写锁能够提供比排它锁更好的并发性。
在读写锁的实现方面,本来以为会比较复杂,结果看完源码的感受也是快刀切西瓜,看来AQS的设计真的很棒,在AQS的基础上构建的组件实现都很简单。

==================================================================================

八、公平锁 、非公平锁
公平锁就是等待竞争锁的多个线程严格按照FIFO顺序(先进先出)获取锁
非公平安锁全由程序员自己设计,比如可以按优先级,也可以按运行次数等规则来竞争锁
ReentrantLock 和 ReentrantReadWriteLock 都有公平锁和非公平锁两种选择



九、总线锁
在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。

十、缓存锁(http://www.2cto.com/os/201608/541708.html)
第二个机制是通过缓存锁定保证原子性。
在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

==================================================================================
锁优化的思路和方法总结一下,有以下几种。
减少锁持有时间
减小锁粒度
锁分离
锁粗化
锁消除

1、减少锁持有时间:尽量只在有线程安全要求的程序上加锁
2、减小锁粒度:将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最典型的减小锁粒度的案例就是ConcurrentHashMap
3、锁分离:最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能
4、锁粗化:将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁
5、锁消除:锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。(锁消除是在编译器级别的事情)
点赞
收藏
评论区
推荐文章
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
java中的锁
记录一下公平锁,非公平锁,可重入锁(递归锁),读写锁,自旋锁的概念,以及一些和锁有关的java类。公平锁与非公平锁:公平锁就是在多线程环境下,每个线程在获取锁时,先查看这个锁维护的队列,如果队列为空或者自身就是等待队列的第一个,就占有锁。否则就加入到等待队列中,按照FIFO的顺序依次占有锁。非公平锁会一上来就试图占
Wesley13 Wesley13
3年前
java 面试知识点笔记(十一)多线程与并发
自适应自旋锁:(java6引入,jvm对锁的预测会越来越精准,jvm也会越来越聪明)1.自选次数不再固定2.由前一次在同一个锁上的自旋时间及锁拥有者的状态来决定(如果在同一个锁对象上自旋等待刚刚成功获取过锁并且持有锁的线程正在运行中,jvm会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间,相反jvm如果可能性很小会省掉自旋过程,
Wesley13 Wesley13
3年前
java并发相关(四)——关于synchronized的可重入性,线程切换实现原理与是否公平锁
一、可重入性  关于synchronized的可重入性的证明,我们可以通过A类内写两个同步方法syncA(),syncB()。然后syncA内调用syncB,调用syncA发现代码可正常执行,来证明这一点。  当处于无锁阶段时,划掉,都重入了不可能处于无锁。  当处于偏向锁阶段时,由之前对偏向锁的解释可知,偏向当前线程id是,当前线程可直
Wesley13 Wesley13
3年前
java面试题汇总,不断更新中。。。
JVM,并发,锁相关:1.请你谈谈对volatile的理解,volatile是否存在伪共享问题。2.cas你知道吗?3.原子类AtomicInteger的ABA问题谈谈?原子更新引用知道吗?4.公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁。5.CountDownLatch、CyclicBarrier、S
Wesley13 Wesley13
3年前
MySQL锁
<br1\.表锁表锁分为写锁,读锁,二者读读不阻塞,读写阻塞,写写阻塞<br<br2\.行锁行锁分为共享锁,排他锁,即读锁和写锁多粒度锁机制自动实现表、行锁共存,InnoDB内部有意向表锁意向共享锁(IS):事务在给一个数据行加共享锁前必须先取得该表的IS锁。
Wesley13 Wesley13
3年前
Mysql 乐观锁 和悲观锁
平时看博客或技术文章的时候,经常被各种锁搞得晕晕乎乎,包括在自旋锁、可重入锁、公平锁等等、乐观锁、悲观锁、行锁、表锁、意向锁、排它锁等。前段时间终于把Java多线程相关的锁有机会学习了一遍。现在开始整理mysql相关的锁概念。先从乐观锁和悲观锁开始聊聊。首先要知道,乐观锁和悲观锁不是真实存在的锁,只是两种抽象概念性的东西,就相当于Java中的接口,只
Wesley13 Wesley13
3年前
JDK里的自旋锁
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区。JDK里面自旋锁的实现有SynchronousQueue 和LinkedTransferQueue。 本文只是自己对源码的简单理解。先说公平锁,先等待的线程先获得数据。SynchronousQueue的内部类TransferQueue实现了公平锁。
Wesley13 Wesley13
3年前
Java中所有锁介绍
在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下:1.公平锁/非公平锁2.可重入锁/不可重入锁3.独享锁/共享锁4.互斥锁/读写锁5.乐观锁/悲观锁6.分段锁7.偏向锁/轻量级锁/重量级锁8.自旋锁上面是很多锁的名词,这些分类并不是全是指锁的