微信公众号: IT一刻钟
大型现实非严肃主义现场
一刻钟与你分享优质技术架构与见闻,作一个有剧情的程序员
关注可第一时间了解更多精彩内容,按期有福利相送哟。
蜀国有一个皇帝叫蜀道难,他比较难伺候,别的皇帝早朝都是在大殿上同时接见全部大臣,共商国是。他不同,他说早朝大家不要有事没事都跑过来叽叽喳喳,有事则来,无事则该干啥干啥去,而后安排太监天天早上在大门口守着,每次只容许一个大臣进来汇报状况。
“你敢多放进来一个就砍脑壳的干活。“
太监赶忙下跪,说“谪!“。
第一天,太监传话钦天监求见,皇帝允了,钦天监上殿报曰:”臣禀报,昨日我司夜观星象,西方忽现王星忽明忽暗,恐戎狄那边有乱。“
“朕知道了,退下吧”。一日无事。
次日,太监传话钦天监求见,皇帝允了。一日无事。
第三天,太监传话钦天监求见......一日无事。
第四天,钦天监......一日无事。
第五天,皇帝不耐烦了,和贾太监说,钦天监这老家伙成天是否是闲着没事,之后他来了不用给我禀报,直接放他上殿讲,讲完让他走吧。
国泰民安的日子依旧过着,天天只有钦天监一我的来报告,贾太监每次看到是钦天监来了,也懒得搭理了,直接放他进去了。(这就是偏向锁,稍后我细细道来)
又一日,钦天监如往常进殿报道,贾太监站在门口打着盹,突然耳边传来一个声音:
“贾太监,帮我禀告圣上,工部李尚书求见。”
“emmm...进去吧...嗯?等等,尚书大人你先等等,钦天监在里面,你等会再来求见吧。”太监一阵后怕,寻思着钦天监还在里面呢,这要是放进去了,我这脑壳可就没了,果真嗜睡误事。
过了一下子,李尚书回来询问求见,被告知钦天监还没走,只好又离去。
又过了一下子,李尚书又回来询问求见,正巧钦天监走了,太监进殿传话说工部李尚书求见,皇帝宣觐见,李尚书进殿上报了一番东南连连大雨,已派人去监察水利,修缮河堤。(这就是轻量级锁)
忽一日,西戎狄和北匈奴同时对帝国西方和北方发难,前线战事消息如片片雪花纷纷涌入京城,瞬间殿外来了一群大臣有要事禀告。
一下子这个来问贾公公我能够进去了吗?一下子那个来问贾公公我能够进去了吗?
把贾太监累的哟,一天下来光说“稍后再来”都把嘴皮子磨破了,没几日,贾太监就跪在皇帝面前哭泣道:“圣上啊,快想一想办法呀,奴才这身子骨就要交代在门口了。”
皇帝一听,说你傻啊,叫他们一个个在门外排队啊,谁叫你要他们稍后来求见的。
贾太监细思大喜,以为有理,第二天在门口竖起一个牌子“禀报要事者,这边排队”,贾太监不再用一我的对着一群人反复回话,只须要每次出来一个,而后传话放进去一个,就能够了。(这就是重量级锁)
上面这个故事,分别讲述了synchronized内部四种级别的状态,分别是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。程序员
咱们首先从重量级锁开始讲,重量级锁是经过互斥量(Mutex)来实现的,即一个线程进入了synchronized同步块,在未完成任务时,会阻塞后面的全部线程。
就像上面的故事所讲的,要禀告要事的大臣只能在大殿门口外一个接一个的阻塞排队。
之因此称它为重量级锁,是由于Java线程是映射到操做系统的原生线程上的,若是要阻塞或唤醒一个线程,都须要依靠操做系统从当前用户态转换到核心态中,这种状态转换须要耗费处理器不少时间,对于简单同步块,可能状态转换时间比用户代码执行时间还要长,致使实际业务处理所占比偏小,性能损失较大。
固然这个在虚拟机层面进行了一些好比自旋等待,锁粗化等等的优化,避免陷入频繁的切换状态。在这里我就不细讲了,有兴趣的能够关注我,我后续再和各位看官讲上一讲。数组
轻量级锁是JDK6引入的,它的轻量是相较于经过系统互斥量实现的传统锁,轻量锁并非用来取代重量级锁的,而是在没有大量线程竞争的状况下,减小系统互斥量的使用,下降性能的损耗。
轻量级锁是经过CAS(Compare And Swap)机制实现的,即若是锁被其余线程所占用,当前线程会经过自旋来获取锁,从而避免用户态与核心态的转换。
就像上面故事所说的,大殿中钦天监在汇报工做,工部尚书要求见,并不须要贾太监每次都进去问一下皇帝,惹得皇帝龙颜大怒,而是大臣本身隔一段时间便来询问贾太监能不能进去,不能就稍后再来问,直到能够进去为止。微信
偏向锁也是JDK6引入的,它存在的依据是“大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到”。它是经过记录第一次进入同步块的线程id来实现的,若是下一个要进入同步块的线程与记录的线程id相同,则说明这个锁由此线程占有,能够直接进入到同步块,不用执行CAS。
就像故事中的,若是天天只有钦天监一我的来的话,就不用贾太监禀告了,贾太监每次一看到钦天监,寻思着,哟,钦天监呢,您自个儿直接进去吧,说完自个儿出来吧。
若是说轻量锁是为了消除系统互斥量带来的性能损耗,那么偏向锁就是为了消除CAS带来的性能损耗,使之在无竞争的状况下消除整个同步,性能无限接近非同步。数据结构
要说这个问题,咱们须要先讲一下Java对象头,每一个对象都会有一个对象头,它分为三个部分:多线程
内容 | 说明 |
---|---|
Mark Word | 存储对象的hashcode或锁信息 |
Class Metadata Address | 存储到对象类型数据的指针 |
Array length | 数组的长度(若是当前对象是数组) |
从表格可见,synchronized锁的信息是存在对象头里一个叫Mark Word的区域里的,考虑到虚拟机的空间效率,Mark Word被设计成非固定的数据结构,会根据对象的状态复用存储空间来存储不一样的内容:架构
当JVM启用了偏向锁模式(JDK6以上默认开启),新建立对象的Mark Word是未锁定,未偏向但可偏向状态,此时Mark Word中的Thread id为0,表示未偏向任何线程,也叫作匿名偏向(anonymously biased)。性能
当第一个线程尝试进入同步块时,发现Mark Word中线程ID为0,则会使用CAS将本身的线程ID设置到Mark Word中,而且,在当前线程栈中由高到低顺序找到可用的Lock Record,将线程ID记录下。完成这些,此线程就获取了锁对象的偏向锁。
当该偏向线程再次进入同步块时,发现锁对象偏向的就是当前线程,会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数,而后继续执行同步块代码,由于线程栈是私有的,不须要CAS指令进行操做,因此在偏向锁模式下,同一个线程,只会执行一个CAS,以后获取释放锁只须要对Lock Record作操做,性能损耗基本能够忽略。
当另一个线程试图进入同步块时,发现Mark Word中线程ID与本身不相符,这个时候就会引起偏向锁的撤销,变成无锁不可偏向状态或轻量级锁状态,固然,这只是宏观上的描述,严格意义上讲是不许确的,由于里面还存在重偏向机制,这里就不过于深刻,在后续的文章中,我会专门出一篇文章,给各位看官详细介绍偏向锁究竟是怎么回事。优化
当锁对象变成无锁不可偏向状态时,多个线程运行到同步块之后,会检查锁对象状态值标志是否加锁,若是没有锁,就把锁对象的Mark Word信息拷贝存储到当前线程栈桢中Lock Record里,而后经过CAS尝试把对象的Mark Word的值改变成一个指向本身线程的指针。若是成功,则当前线程得到锁对象的轻量级锁,其余线程的CAS就会失败,由于锁对象的Mark Word已经变成一个新的指针了,必须等待线程释放锁,此时其余线程则经过自旋来竞争锁。当获取锁的线程执行完毕释放锁的时候,会将Lock Record里面以前拷贝的值还原到锁对象的Mark Word中。spa
当自旋次数超过JVM预期上限,会影响性能,因此竞争的线程就会把锁对象的Mark Word指向重锁,所谓的重锁,实际上就是一个堆上的monitor对象,即,重量级锁的状态下,对象的Mark Word为指向一个堆中monitor对象的指针。
而后全部的竞争线程放弃自旋,逐个插入到monitor对象里的一个队列尾部,进入阻塞状态。
当成功获取轻量级锁的线程执行完毕,尝试经过CAS释放锁时,由于Mark Word已经指向重锁,致使轻量级锁释放失败,这时线程就会知道锁已经升级为重量级锁, 它不只要释放当前锁,还要唤醒其余阻塞的线程来从新竞争锁。
大概流程以下图所示:
这里有一点需注意的是:锁只能升级,不能降级。操作系统
锁 | 优势 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不须要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 若是线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会堵塞,提升了程序的响音速度 | 始终得不到锁的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度很是快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
synchronized无非如下两种:
1.对象锁:修饰非静态方法,修饰代码块
2.类锁:修饰静态方法,修饰代码块
其中按照修饰类型来分,又能够分为代码块同步和方法同步
代码块同步锁的是对象,使用monitorenter和monitorexit指令实现的。虽然我知道多一行代码少一位看官的定理,可是这里仍是必须贴一张代码图,来证实我没有瞎说,是有理有据的“理据服”。
想要降服妖怪,就得先将其打回原形,因此咱们先对一段简单的代码进行反编译,获得它的字节码。
final Object lock = new Object(); public int subtr(int i){ synchronized (lock){ return i-1; } }
字节码:
能够看出,monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit插入到同步代码块结束的地方,正常状况下monitorenter和monitorexit是一对一的匹配,然后面又出现了一个monitorexit,是由于那里是异常处,用来保证方法执行异常的时候,能够自动解锁,而不会形成死锁。
方法同步的实现官方没有透露,咱们尝试对一个方法同步的代码进行反编译。
public synchronized int add(int i){ return i+1; }
字节码:
从字节码里也看不到monitorenter和monitorexit,智能发现flags那里,多了一个ACC_SYNCHRONIZED的标示,没什么头绪。不过我猜测,底层应该是锁方法所属的对象或类。
这就是synchronized的大体原理,打回原形以后来看,是否是就以为也不过如此?有什么疑问或更好的解读,能够在下方留言,咱们进行愉快友好的磋商交流。
若是以为有用,记得分享~