【学习底层原理系列】Java底层-synchronized锁-1

在技术论坛中,经常看到一种言论:面试造火箭,干活拧螺丝。我们平时写的大部分代码的确是CRDU,再提一个层次,也无非就是揉进去复杂一些的业务逻辑,把一堆的CRDU组合起来。

那么问题来了:我们提倡的研究“底层技术”,难道仅仅是为了面试?或是为了平时码农们聊天时装大佬吗?

当然不是!

小端随着工作年限的增加,深有感悟:

技术是我们程序员的工具箱。

CRDU是我们的默认工具。

平时的点滴积累就是在不断的丰富自己的工具箱,增加工具种类。

而深挖技术细节,就是在更深入的掌握每一个工具的特性。

在工作中遇到问题时,如果一直使用默认工具,那么随着问题域的越来越大,总会遇到捉襟见肘的尴尬。

如果工具箱中不断的加入得心应手的工具,嗯,办法就会总比问题多。

以下面即将讲的synchronized锁为例子,如果对它没有清晰的了解,那么在解决线程安全性问题时,第一反应是尽量避免使用;如果实在避免不掉而用之,也是简单的模仿,这样自己其实心里也是没底的,也不清楚带来的开销有多大影响,甚至不清楚是否能真正解决问题,只能期盼上线后,一切平安...

小端会写一个系列,以面试中的问题作为切入点(毕竟这种问题涉及到的,是大部分技术人员比较关注,平时也经常使用到的技术),深入技术底层进行分析,搭建自己的知识体系。期望对看到这篇文章的您,有所启发。

好,下面进入正题。

如果有人让你讲讲synchronized的实现细节,那么,

恭喜你,这是一个能体现技术深度的好机会!

面试官:synchronized关键字用过吗?讲讲你对他的了解...

有没有一种被他虐我千百遍的感觉?这个知识点也算是面试中的必现题型了。

不过,以小端的经验,有的面试官浅尝辄止,有的面试官则穷追猛打。前一种,可能面试官自己也不太熟悉,我们辛辛苦苦准备的东西,刚开个头,就被叫停了,不尽兴有没有;而后一种,一般会不断的深挖,一直问到我们的知识盲区。

  1.在第一类面试官面前,我们需要引导,需要争取足够的时间把他的知识盲区讲清楚,借此展示出自己的知识深度。

  2.在第二类面试官面前,大部分时间是知识体系对等的交流,这就需要我们做到回答时能提纲掣领,一旦深入细节有条理。

 那么问题来了,如何做到呢?

小端利用十一假期,重新梳理和总结相关知识点,试图勾勒出一个5层金字塔结构(后面称为S金字塔),来帮助大家构建自有的完整知识体系。

希望在“大场面”中,你也能做到:任你风起云涌,我自岿然不动!

先放出S金字塔,一睹为快:

      

抛出大纲很重要

我们大多数人,更习惯于平铺直叙的方式,描述一个相对复杂的知识结构。

殊不知对于你的听众来说,其实这是一种负担。

对方要全程集中注意力,要在听后面内容的时候,不断的重复回忆前面听到的内容,只有这样,才能在听到最后时,构建出相对完整的思维模型,才能方便理解对话的真正含义。

否则就会产生压迫感,对自己不熟悉的知识更甚【上述第一种面试官】。

这是人的天性使然。

如果我们在对话的刚开始,就先抛出一个简明的大纲,先帮助对方建立起完整的模型,然后我们再针对每一个分支,做专项描述,让对方只需要边听边对照大纲印证,减少了中间记忆和回忆的环节,无疑会大大减轻对方的压力。

所以,针对这个问题,小端一般会首先告诉对方:我将从关键字的应用(初入山门)、字节码层面的细节(入室弟子)、内部组件(大师兄)、以及组件工作的流程(长老)四个方面来做解释。

PS:可能有的同学要问了,怎么少了一层,S塔尖呢?恩...正如字面,我们只是芸芸众生,这个高度需要潜心研究,还是留给大神以及有志于成为大神的同学吧。

第一层:我们看到的样子-synchronized关键字的应用

这层是基础。

在多线程编程时,synchronized经常被用来解决互斥导致的线程安全性问题。用法有两种个,一种用在方法声明中:

public synchronized void run() {
        //...
}
View Code

另一种用在方法体代码块中:

1 public void increaseCount() {
2         //……………………
3         synchronized (this) {
4             for (int i = 0; i < 5; i++) {             
5  System.out.println(Thread.currentThread().getName() + ":" + count++);
6                
7             }
8         
9     }
View Code

 如果修饰的是静态方法,锁对象是其所在的类对象;如果修饰的是实例方法,锁对象是当前的实例对象。

第二层:机器看到的样子-编译成字节码的细节

Java代码编译为字节码指令后,方法声明中的synchronized对应生成ACC_SYNCHRONIZED关键字。

1 public synchronized void doSth();
2     descriptor: ()V
3     flags: ACC_PUBLIC, ACC_SYNCHRONIZED
4     Code:
5       stack=2, locals=1, args_size=1
6          0: ...                 
7          3:...
8          5: return
View Code

方法体中的synchronized会对应生成monitorenter,monitorexit。

 1 public void doSth1();
 2     descriptor: ()V
 3     flags: ACC_PUBLIC
 4     Code:
 5       stack=2, locals=3, args_size=1
 6          0: ldc           #5                  
 7          2: dup
 8          3: astore_1
 9          4: monitorenter
10          5: getstatic     #2                  
11          8: ldc           #3                  
12         10: invokevirtual #4                  
13         13: aload_1
14         14: monitorexit
15         15: goto          23
16         18: astore_2
17         19: aload_1
18         20: monitorexit
19         21: aload_2
20         22: athrow
21         23: return
View Code

当执行到monitorenter关键字时,会申请同步锁;执行到monitorexit关键字时,会释放同步锁。

这里需要注意,monitorexit有两个的作用可以理解为,try...catch...finally,保证在正常执行流程和其他非正常流程时,都能释放锁。

第三层:真实的组成:偏向锁+轻量级锁+重量级锁

在传统重量级锁模型中,加锁解锁是很消耗系统资源的操作。因为加锁解锁操作,涉及到线程的阻塞和唤醒,而阻塞唤醒,是依靠操作系统来实现的,也就需要程序从用户态切换到内核态。

在这种情况下,Java虚拟机做出了最直接的优化-自适应自旋。在加锁失败、以及被唤醒后未获取到锁的时候,进入自旋,以期能在一定的时间内,其他线程释放锁进而加锁成功。这是用CPU的消耗来尽可能避免阻塞唤醒操作的初级解决方案。

这里隐含着一个前提:加锁是最终目的。所以做的任何优化,只是在加锁这件事上,尽量提效。那么怎么来优化呢?

不知你有没有思考过这种问题:我们在写代码的时候是无法确定这段代码是否一定存在线程安全问题的,那么我们采取一切从严标准写,也就是明确标识加锁。然后让虚拟机在执行时,根据运行时的状态来决定加锁不加锁,岂不是完美?

是的!虚拟机已经做到了:在无线程竞争的场景,或者多线程近乎于交替执行的场景,是不需要加锁的(传统的重量级锁)。

这也就印证了一种说法,synchronized锁性能不好,后来经过优化后,性能得到了极大的提升。本质是,在jdk1.6版本中,引入了偏向锁,轻量级锁、重量级锁三层模型.

优化后,虚拟机加锁的策略,可以简单描述成: 

如果只有一个线程调用同步代码,显然没有必要加锁。可以通过偏向锁,只需要一次CAS操作。如果重入,都是一些值比较操作,性能消耗极低。

如果多线程近乎于交替执行同步代码,仅需要在每次加锁解锁时,做CAS修改(其实CAS的主要目的是发现竞争)。

如果的确存在多线程竞争情况,再升级为依赖重量级锁来保障。

第四层:组件间的关系-协同

 那么上述组件是如何协同工作的呢?

可以说很复杂!复杂到小端不得不用独立一篇来做详细介绍。

原文地址:https://www.cnblogs.com/xyang/p/11631866.html