在多线程并发编程中synchronized一直是元老级角色,不少人都会称呼它为重量级锁。可是,随着Java对synchronized进行了各类优化以后,有些状况下它就并不那么重了。本文详细介绍Java中为了减小得到锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁java
这三种锁由轻到重排序为:偏向锁<轻量级锁<重量级锁编程
想要了解Java中的锁,咱们首先须要了解一些基础知识数组
1、锁类型安全
锁从宏观上分类,分为悲观锁与乐观锁。多线程
乐观锁并发
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采起在写时先读出当前版本号,而后加锁操做(比较跟上一次的版本号,若是同样则更新),若是失败则要重复读-比较-写的操做。框架
java中的乐观锁基本都是经过CAS操做实现的,CAS是一种更新的原子操做,比较当前值跟传入值是否同样,同样则更新,不然失败。jvm
悲观锁函数
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,因此每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。高并发
2、Java 线程的实现以及切换开销
对于计算机实现线程主要有3种方式:使用内核线程实现(一对一)、使用用户线程实现(一对N)、使用用户线程加轻量级进程混合实现(N对M)。对于Sun JDK来讲,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,由于Windows和Linux系统提供的线程模型就是一对一的。然而使用内核线程实现的一对一模型是基于内核线程实现的,因此各类线程操做,如建立、析构及同步,都须要进行系统调用。而系统调用的代价相对较高,须要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。由于用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态须要传递给许多变量、参数给内核,内核也须要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工做。
所以,频繁的线程切换将会严重拖慢咱们的系统性能,耗费不少CPU处理时间,而且对于简单的同步代码块,获取锁和释放锁的时间可能比用户代码的执行时间还要长,这样就显得很是糟糕。
然而synchronized在线程争用不到锁的时候将会线程阻塞,而且获取锁的时候还须要从阻塞状态醒来,这就是两次切换开销。所以为了解决这种频繁的线程切换致使的性能问题,Java引入了偏向锁和轻量级锁。
3、Java对象头Markword
Java对象头里的。若是对象是数组类型,则虚拟机用3个字宽
(Word)存储对象头,若是对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽
等于4字节,即32bit,以下表所示。
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构以下表所示。
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储如下4种数据,以下表所示。
在64位虚拟机下,Mark Word是64bit大小的,其存储结构以下表所示。
markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,以下表所示:
状态 | 标志位 | 存储内容 |
---|---|---|
未锁定 | 01 | 对象哈希码、对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
膨胀(重量级锁定) | 10 | 执行重量级锁定的指针 |
GC标记 | 11 | 空(不须要记录信息) |
可偏向 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 |
4、Java中的锁
Java SE 1.6为了减小得到锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争状况逐渐升级。锁能够升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提升得到锁和释放锁的效率,下文会详细分析。
(1)偏向锁
HotSpot[1]的做者通过研究发现,大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,为了让线程得到锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。若是测试成功,表示线程已经得到了锁。若是测试失败,则须要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):若是没有设置,则使用CAS竞争锁;若是设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,因此当其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,而后检查持有偏向锁的线程是否活着,若是线程不处于活动状态,则将对象头设置成无锁状态;若是线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么从新偏向于其余线程,要么恢复到无锁或者标记对象不适合做为偏向锁,最后唤醒暂停的线程。下图中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。
偏向锁的适用场景
始终只有一个线程在执行同步块,在它没有执行完释放锁以前,没有其它线程去执行同步块,在锁无竞争的状况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候须要撤销偏向锁,撤销偏向锁的时候会致使stop the word操做;
在有锁的竞争时,偏向锁会多作不少额外操做,尤为是撤销偏向所的时候会致使进入安全点,安全点会致使stw,致使性能降低,这种状况下应当禁用,高并发的应用会禁用掉偏向锁。
关闭偏向锁
偏向锁在Java 6和Java 7里是默认启用的,可是它在应用程序启动几秒钟以后才激活,若有必要可使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。若是你肯定应用程序里全部的锁一般状况下处于竞争状态,能够经过JVM参数关闭偏向锁:-XX:-
UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
(2)轻量级锁
轻量级锁原理很是简单,若是持有锁的线程能在很短期内释放锁资源,那么那些等待竞争锁的线程就不须要作内核态和用户态之间的切换进入阻塞挂起状态,它们只须要等一等(自旋),等持有锁的线程释放锁后便可当即获取锁,这样就避免用户线程和内核的切换的消耗。
轻量级锁加锁
线程在执行同步块以前,JVM会先在当前线程的栈桢中建立用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。而后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。若是成功,当前线程得到锁,若是失败,表示其余线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁
轻量级解锁时,会使用原子的CAS操做将Displaced Mark Word替换回到对象头,若是成功,则表示没有竞争发生。若是失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是两个线程同时争夺锁,致使锁膨胀的流程图。
由于自旋会消耗CPU,为了不无用的自旋(好比得到锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其余线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁以后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
轻量级锁的优缺点
轻量级锁尽量的减小线程的阻塞,这对于锁的竞争不激烈,且占用锁时间很是短的代码块来讲性能能大幅度的提高,由于自旋的消耗会小于线程阻塞挂起再唤醒的操做的消耗,这些操做会致使线程发生两次上下文切换!
可是若是锁的竞争激烈,或者持有锁的线程须要长时间占用锁执行同步块,这时候就不适合使用轻量级锁了,由于轻量级锁在获取锁前一直都是占用cpu作无用功,同时有大量线程在竞争一个锁,会致使获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操做的消耗,其它须要cup的线程又不能获取到cpu,形成cpu的浪费。因此这种状况下咱们要关闭轻量级锁;
JVM对于自旋周期的选择,jdk1.5这个限度是必定的写死的,在1.6引入了适应性轻量级锁,适应性轻量级锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷状况作了较多的优化
若是平均负载小于CPUs则一直自旋
若是有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
若是正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
若是CPU处于节电模式则中止自旋
自旋时间的最坏状况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
自旋时会适当放弃线程优先级之间的差别
轻量级锁的开启
JDK1.6中-XX:+UseSpinning开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;
5、重量级锁synchronized
在JDK1.5以前都是使用synchronized关键字保证同步的,synchronized的做用相信你们都已经很是熟悉了;
它能够把任意一个非NULL的对象看成锁。
synchronized实现逻辑以下图:
它有多个队列,当多个线程一块儿访问某个对象监视器的时候,对象监视器会将这些线程存储在不一样的容器中。
Contention List:竞争队列,全部请求锁的线程首先被放在这个竞争队列中;
Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
Owner:当前已经获取到所资源的线程被称为Owner;
!Owner:当前释放锁的线程。
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),可是并发状况下,ContentionList会被大量的并发线程进行CAS访问,为了下降对尾部元素的竞争,JVM会将一部分线程移动到EntryList中做为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(通常是最早进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck须要从新竞争锁。这样虽然牺牲了一些公平性,可是能极大的提高系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有获得锁资源的仍然停留在EntryList中。若是Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻经过notify或者notifyAll唤醒,会从新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操做系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,若是获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
synchronized与static synchronized 的区别
一个是实例锁(锁在某一个实例对象上,若是该类是单例,那么该锁也具备全局锁的概念),一个是全局锁(该锁针对的是类,不管实例多少个对象,那么线程都共享该锁)。实例锁对应的就是synchronized关键字,而类锁(全局锁)对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
synchronized是对类的当前实例(当前对象)进行加锁,防止其余线程同时访问该类的该实例的全部synchronized块,注意这里是“类的当前实例”, 类的两个不一样实例就没有这种约束了。
那么static synchronized刚好就是要控制类的全部实例的并发访问,static synchronized是限制多线程中该类的全部实例同时访问jvm中该类所对应的代码块。实际上,在类中若是某方法或某代码块中有 synchronized,那么在生成一个该类实例后,该实例也就有一个监视块,防止线程并发访问该实例的synchronized保护块,而static synchronized则是全部该类的全部实例公用得一个监视块,这就是他们两个的区别。也就是说synchronized至关于 this.synchronized,而static synchronized至关于Something.synchronized.
那么,假若有Something类的两个实例x与y,那么下列各组方法被多线程同时访问的状况是怎样的?
a. x.isSyncA()与x.isSyncB()
b. x.isSyncA()与y.isSyncA()
c. x.cSyncA()与y.cSyncB()
d. x.isSyncA()与Something.cSyncA()
这里,很清楚的能够判断:
a,都是对同一个实例(x)的synchronized域访问,所以不能被同时访问。(多线程中访问x的不一样synchronized域不能同时访问)
若是在多个线程中访问x.isSyncA(),由于仍然是对同一个实例,且对同一个方法加锁,因此多个线程中也不能同时访问。(多线程中访问x的同一个synchronized域不能同时访问)
b,是针对不一样实例的,所以能够同时被访问(对象锁对于不一样的对象实例没有锁的约束)
c,由于是static synchronized,因此不一样实例之间仍然会被限制,至关于Something.isSyncA()与 Something.isSyncB()了,所以不能被同时访问。
那么,第d呢?,书上的 答案是能够被同时访问的,答案理由是synchronzied的是实例方法与synchronzied的类方法因为锁定(lock)不一样的缘由。
synchronized方法与synchronized代码快的区别
synchronized methods(){} 与synchronized(this){}之间没有什么区别,只是synchronized methods(){} 便于阅读理解,而synchronized(this){}能够更精确的控制冲突限制访问区域,有时候表现更高效率。
6、锁的优缺点对比
下表是锁的优缺点的对比。
参考:
https://blog.csdn.net/zqz_zqz/article/details/70233767
《Java并发编程的艺术》
《深刻理解Java虚拟机 JVM高级特性与最佳实战》