详解Synchronized关键字

synchronized是Java语言提供的同步控制关键字,下面我们看看它的具体用法 

synchronized的三种应用方式

synchronized关键字最主要有以下3种应用方式

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized底层实现原理

synchronized关键字经过编译之后,会在同步块的前后形成monitorenter和monitorexit两个字节码指令,这两条指令都需要一个reference类型的参数来指明要锁定和解锁的对象。根据Java虚拟机的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有该对象的锁,把锁的计数器加1(synchronized是可重入的锁),相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放了;如果获取失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

synchronized是一种阻塞同步方案,它属于一种悲观的并发策略,总是认为只要不做正确的同步措施(加锁),那就肯定出问题,无论共享数据是否真的会出现竞争,它都要加锁。该方案的主要问题就是进行线程阻塞和唤醒时所带来的性能问题,我们知道Java的线程是映射到操作系统的原生线程上的,如果阻塞和唤醒一条线程都需要操作系统的帮忙,这就需要从用户态转换到核心态中,这会消耗很多的处理器时间。

自JDK1.6开始,HotSpot虚拟机开发团队对synchronized锁进行了各种优化,大幅提高其并发性能

对synchronized关键字进行的锁优化

1.自旋锁和自适应锁

我们知道,java’线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。 
而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

2.锁消除

什么叫锁消除呢?就是把不必要的同步在编译阶段进行移除。 
那么有的小伙伴又迷糊了,我自己写的代码我会不知道这里要不要加锁?我加了锁就是表示这边会有同步呀? 
并不是这样,这里所说的锁消除并不一定指代是你写的代码的锁消除,我打一个比方: 
在jdk1.5以前,我们的String字符串拼接操作其实底层是StringBuffer来实现的(这个大家可以用我前面介绍的方法,写一个简单的demo,然后查看class文件中的字节码指令就清楚了),而在jdk1.5之后,那么是用StringBuilder来拼接的。我们考虑前面的情况,比如如下代码:

String str1="qwe";
String str2="asd";
String str3=str1+str2;

底层实现会变成这样:

StringBuffer sb = new StringBuffer();
sb.append("qwe");
sb.append("asd");

我们知道,StringBuffer是一个线程安全的类,也就是说两个append方法都会同步,通过指针逃逸分析(就是变量不会外泄),我们发现在这段代码并不存在线程安全问题,这个时候就会把这个同步锁消除。

3,.锁粗化

在用synchronized的时候,我们都讲究为了避免大开销,尽量同步代码块要小。大部分情况下,该原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁地进行加锁和解锁操作会导致不必要的性能损耗。这时就可以考虑把加锁的范围扩大。

4.轻量级锁

要理解轻量级锁首先要从对象头说起。

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下

其中对象头中包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄等。官方称这部分数据为“Mark Word”,它是实现锁的关键。另外一部分用于存储指向方法区对象类型数据的指针。Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位。以下是32位JVM的Mark Word默认存储结构

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

加锁

轻量级锁的加锁过程如下,线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS操作(乐观的并发策略)尝试将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,并且Mark Word的锁标志位将变成“00”;如果该操作失败,虚拟机首先检查对象Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,可以直接进入同步代码块执行,否则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

解锁

轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。

轻量级锁提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内是不存在竞争的”,如果没有竞争,轻量级锁使用CASc操作避免了使用互斥量的开销。

5.偏向锁

如果说轻量级锁是在无竞争的情况下使用CAS操作消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。

偏向锁的获取过程如下:

当一个线程访问同步块并获取锁时,虚拟机会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程ID记录在对象头的Mark Word之中,如果CAS操作成功,持有偏向锁的线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

偏向锁的撤销:

偏向锁使用了一种等到竞争出现才释放锁的机制,当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。

小结

JDK1.6中,锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。

参考资料

Lock和synchronized的区别和使用

详解synchronized与Lock的区别与使用

发布了30 篇原创文章 · 获赞 30 · 访问量 8242

猜你喜欢

转载自blog.csdn.net/IT_GJW/article/details/81636164
今日推荐