Java 并发机制的底层实现原理

一、volatile的实现原理

在多并发编程中volatile和synchronized有着很重要的作用,我们平常也用的很多,比如在Java单例模式中的建立对象时所涉及的双重检验等。
这里要讨论的是Java并发机制的底层实现原理。所以,接下来我们重点会放在原理上面展开。
1、volatile的实现原理
volatile修饰过的字段,在现象上看,我们可以观察到好像每个线程都是可以看到它的变化的,这是如何实现的?
如果你接触过操作系统的相关课程,那你应该了解进程/线程间的常见的几种通信方式。如果你没有接触过,那也没有太大的关系,接下来我说大概的说一下。
进程的通讯方式
这里张图片里列举的三种方式,是一般我们比较常见的进程间的通讯方式,不专业的说,因为线程可以看作是进程的进程(一个小型的进程),所以进程的通讯方式,我们同样可以运用在线程的通讯上。

顺带着为了照顾没有接触过操作系统相关课程的读者,我会简单的讲解一下这三种方式,如果你接触过,可以跳过这部分内容接着往下看。
共享存储空间
共享存储空间的方式一般还可以细分为基于数据结果的共享和基于存储区的共享。但这两种方式的差别仅仅在于对于数据的保存方式而已。简单而言,共享存存储空间的方式就是操作系统会提供一个共享空间,但进程对于操作共享空间的访问是互斥的(进程互斥的实现,我们也有很多策略,这里不展开,之后也许我还会在写一篇新的文章来说明)。
消息传递
消息传递可以分为间接和直接的消息传递,不过我们这里先来认识这种方式,在来看这两种方式的差别。
消息传递的方式可以认为是进程之间的数据以格式化的消息为单位,进程通过“发送消息和接受消息”的原语来进行数据交互(原语就是不可分割的操作,可以认为是下层给我们提供的库函数)。而消息又由消息头和消息体构成(这点很像网络中的报文)。
直接消息传递的方式是消息会直接放到接受进程的消息缓冲队列之上。
间接消息传递的方式则是先要经过一个中间实体(信箱),再转发到目标的进程消息缓冲队列中。
管道通讯
管道通讯是一种半双工通讯的方式,不过我们先介绍一下什么是管道。
管道可以认为是在内存中开辟的一块大小固定的缓冲区,用于链接读写进程的一个共享的文件。
由于管道通讯是一种半双工的通讯方式,如果想要实现双向同时通讯,就必须要开辟新的管道。
数据会以字符流的形式写入管道之中,只有当管道被写满时,才能被读取,也只有当管道之中的数据全部被取走之后,才能接着写入新的数据,而且管道之中的数据一旦被读取,那么就会被抛弃,所以我们要保障管道的访问,必须是互斥的,否则可能就会发送读取内容错误。

简单的介绍过后,让我回到Java之中。
在Java之中,我们是通过共享存储空间的方式来是实现线程间的通讯的(注意,这里不是之前讨论的进程了)。volatile就是通过对共享存储空间的操作来实现的。
在添加过volatile的字段变量进行写操作的代码经过编译之后,可以在其汇编代码之中会发现带有lock这样一个字段。而lock前缀的指令,在多核处理器下会做两件事情:
(1)把当前的处理器的缓存行中的内容写入到系统的内存当中。
(2)让其他的处理器的缓存行中的内容失效。
为什么这么做呢?我接着给大家画个图,从图中我们来看一下,Java之中的线程通讯的过程。
Java之中的线程通讯过程看了这个过程,我们应该就能够明白Java之中的线程通讯大概的样子。我们可以发现,缓存行中的内容只对缓存行对应的当前线程可见,而对其他的线程不可见。如果,我们的线程进行了一次读操作,从共享区域之中,读到了数据,然后进行数据的处理,而在处理的过程中,其他的线程对数据进行了修改,而本线程又没有再次读取共享区域内的新数据,那么就有可能会发生错误。而lock就迫使每个线程必须在一个线程写入共享区域的时候,重新去读取堆区之中的内容。
这也就是volatile的实现原则

二、synchronized的实现原理

在很久以前,synchronized被叫做重量级的锁,而在JavaSE 1.6以后就不能够如此简单的描述synchronized了。在接着讲这个故事之前,我们先要明白他干了一些什么事情。
1、简述synchronized的实现原理
synchronized的实现,在Java之中,是通过Monitor对象来实现的,通过Monitor来锁住某一段代码块,或者某个方法。对于Monitor对象的抢夺,就是对于锁的抢夺。我们可以通过对加了synchronized关键字的代码部分的汇编代码进行观察,发现会有monitorentermonitorexit两个字段将我们的某一些操作进行了一个包裹,而包裹的操作就是我们想要让线程互斥访问的代码块或方法(实际上我们可以更专业的称它为临界区)。
从用法上来看,我们发现volatile是对变量实现线程访问的互斥,synchronized是对代码块或是方法实现线程访问的互斥。但是看本质我们会发现,其实就是互斥的实现,这些内容我也许之后会单独出一篇来讲,这里展开就不是简述了。让我们先把目光移往别处,让我们接着讲之前提到的的故事。
2、synchronized的锁膨胀过程
我们先粗糙的描述一下这个过程,然后在细细地讨论一些每一个环节的一些细节,让我接着来给大家画图。
synchronized的锁膨胀过程这个过程大致描述起来就是这么的简单。让我们一起来了解一下这条过程链上的环节吧,无锁这里就不讨论了。
偏向锁
我们知道,如果一个线程想要去争夺锁的话,他必须要完成某些类似信息注册的工作。而完成这一部分的工作虽然小,但是仍然要耗费资源。如果每一次都是同一个线程在申请锁的话,那么这个过程就会重复很多次,而且在这么多次里面,也就只有最开始的那一次是具有意义的。而偏向锁本质上就是为了省略掉这些后面多出来的无意义的工作。
当一个线程尝试着去获取锁的时候,会在他的对象头之中(对象头大致可以分为三个区域,Mark Word、Class MateData Address、Array length当然最后一个只会是数组类型的对象才会有)和桟帧的锁记录里存储偏向的线程的ID。之后这个线程再次进入的时候,我们就只是简单的看一下他的对象头是否有着指着当前线程的偏向锁,有的话就直接可以获得锁。如果没有的话,就要使用CAS来竞争锁(CAS是一种乐观锁,比较和交换Compare And Swap,每次都认为我访问数据的时候,不会有其他的人来访问数据,而结束后我会拿处理之前数据和现在存储着的数据进行比较,如果一样则我就把我处理的数据存入,否则就不存入)。
轻量级锁
Java之中所采用的轻量级锁是一种自旋锁(其实这里涉及到一个很有意思的点,就是CPU的空转,我们在操作系统之中采用了记录型信号量来解决这个问题,也就是实现“让权等待”)。要用文字来描述他,我觉得想要讲清楚比较难,所以我也还是来画图解释。
自旋锁运作大致过程
自旋锁的工作方式大概就像是图中描述的这样。
重量级锁
重量级锁就是通过操作系统来进行上锁了,这个过程是十分耗费资源的,而且操作系统内置的锁是有限的,是十分宝贵的资源。

三、一点补充和总结

这里想补充一下原子性的实现是如何做到的,在硬件层面,我们通过总线锁和缓存锁来保障原子性。具体来说就是通过控制信号来强制控制每次在进行原子性操作的时候不会被打断。
而Java之中,的原子性操作是通过锁和循环CAS的操作来实现的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值