架构师课程第八周总结

用户头像
dongge
关注
发布于: 2020 年 07 月 29 日
架构师课程第八周总结

万丈高楼平地起,回归到操作系统、数据结构和算法、锁、文件 I\O 的问题上,可以更好的理解架构的性能问题。

操作系统

关于操作系统,极客时间有《趣谈Linux操作系统》、《Linux实战技能100讲》。趣谈 Linux 涵盖了更为全面的操作系统知识。



架构师课程中,通过程序运行时架构、多任务运行环境、进程的运行期状态、进程 VS 线程、线程栈、Java Web应用的多线程运行时视图、线程安全、临界区、阻塞导致高并发系统崩溃、避免阻塞引起崩溃做了讲解了操作系统。

数据结构与算法

关于数据结构与算法,《数据结构与算法之美》是很好的课程。

关于锁,课程中讲解锁原语CAS、偏向锁、轻量级锁、重量级锁。多 CPU 情况下的锁、总线锁与缓存锁、公平锁与非公平锁、可重入锁;独享锁/互斥锁、共享锁、读写锁;乐观锁、悲观锁、分段锁、自旋锁。



锁原语CAS要注意在执行时必须是连续的,在执行过程中不允许被中断。Java 通过在对象头中修改 Mark Word 实现加锁。





偏向锁:偏向锁主要用来优化同一线程多次申请同一个锁的竞争。

轻量级锁:当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。

自旋锁:轻量级锁 CAS 抢锁失败,线程将会被挂起进入阻塞状态。如果正在持有锁的线程在很短的时间内释放资源,那么进入阻塞状态的线程无疑又要申请锁资源。

重量级锁:自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。



CPU 情况下的锁

总线锁与缓存锁:保证复杂情况下的内存一致性的机制。

公平锁与非公平锁:非公平锁是首先就CAS来获取一次,成功就拿到锁,失败就放入队列;公平锁不会有这步操作,直接放入队列。

可重入锁:在同一个线程在前面方法中已获取锁了,再进入该线程的其他方法获取锁,此时不会因为之前获取锁而阻塞。



独享锁/互斥锁

共享锁

读写锁:针对这种读多写少的场景,Java 提供了另外一个实现 Lock 接口的读写锁 RRW(ReentrantReadWriteLock)



乐观锁:在操作共享资源时,它总是抱着乐观的态度进行,它认为自己可以成功地完成操作。但实际上,当多个线程同时操作一个共享资源时,只有一个线程会成功,那么失败的线程呢?它们不会像悲观锁一样在操作系统中挂起,而仅仅是返回,并且系统允许失败的线程重试,也允许自动放弃退出操作。

悲观锁:Synchronized 和 Lock 实现的同步锁机制,这两种同步锁都属于悲观锁。

分段锁:



这些锁的概念如果一个个记忆,几乎是要崩溃的。那有什么好办法记忆吗?



在 JDK1.5 之前,Java 是依靠 Synchronized 关键字实现锁功能来做到这点的。Synchronized 是 JVM 实现的一种内置锁,锁的获取和释放是由 JVM 隐式实现。



到了 JDK1.5 版本,并发包中新增了 Lock 接口来实现锁功能,它提供了与 Synchronized 关键字类似的同步功能,只是在使用时需要显式获取和释放锁。

Lock 同步锁是基于 Java 实现的,而 Synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。因此,在锁竞争激烈的情况下,Synchronized 同步锁在性能上就表现得非常糟糕,它也常被大家称为重量级锁

特别是在单个线程重复申请锁的情况下,JDK1.5 版本的 Synchronized 锁性能要比 Lock 的性能差很多。



到了 JDK1.6 版本之后,Java 对 Synchronized 同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了 Lock 同步锁。



在 JDK1.6 中,JVM 将 Synchronized 同步锁分为了偏向锁、轻量级锁、自旋锁以及重量级锁,优化路径也是按照以上顺序进行。JIT 编译器在动态编译同步块的时候,也会通过锁消除、锁粗化的方式来优化该同步锁。



通过上面的介绍,我们了解了 JDK 版本进化多对锁性能的提升。我们将偏向锁、轻量级锁、自旋锁以及重量级锁四个概念联系到了一起。这些锁可以通过下图深入理解。红线流程部分为偏向锁获取和撤销流程





下图中红线流程部分为升级轻量级锁及操作流程





下图中红线流程部分为自旋后升级为重量级锁的流程:





JVM 中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。Monitor 是由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现。



当多个线程同时访问一段同步代码时,多个线程会先被存放在 ContentionList 和 _EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex,竞争失败的线程会再次进入 ContentionList 被挂起。

如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。





多线程对锁资源的竞争会引起上下文切换,还有锁竞争导致的线程阻塞越多,上下文切换就越频繁,系统的性能开销也就越大。由此可见,在多线程编程中,锁其实不是性能开销的根源,竞争锁才是。



锁的优化归根到底就是减少竞争。



为了提升性能,JDK1.6 引入了偏向锁、轻量级锁、重量级锁概念,来减少锁竞争带来的上下文切换,而正是新增的 Java 对象头实现了锁升级功能。



在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示:





Lock

在 JDK1.5 之后,Java 还提供了 Lock 同步锁。那么它有什么优势呢?相对于需要 JVM 隐式获取和释放锁的 Synchronized 同步锁,Lock 同步锁(以下简称 Lock 锁)需要的是显示获取和释放锁,这就为获取和释放锁提供了更多的灵活性。Lock 锁的基本操作是通过乐观锁来实现的,但由于 Lock 锁也会在阻塞时被挂起,因此它依然属于悲观锁。我们可以通过一张图来简单对比下两个同步锁,了解下各自的特点:





在这里可以学习到非公平锁、公平锁、可重入锁

Lock 锁的实现原理Lock 锁是基于 Java 实现的锁,Lock 是一个接口类,常用的实现类有 ReentrantLock、ReentrantReadWriteLock(RRW),它们都是依赖 AbstractQueuedSynchronizer(AQS)类实现的。



AQS 类结构中包含一个基于链表实现的等待队列(CLH 队列),用于存储所有阻塞的线程,AQS 中还有一个 state 变量,该变量对 ReentrantLock 来说表示加锁状态。



该队列的操作均通过 CAS 操作实现,我们可以通过一张图来看下整个获取锁的流程。





乐观锁的实现原理



CAS 是实现乐观锁的核心算法,它包含了 3 个参数:V(需要更新的变量)、E(预期值)和 N(最新值)。

只有当需要更新的变量等于预期值时,需要更新的变量才会被设置为最新值,如果更新值和预期值不同,则说明已经有其它线程更新了需要更新的变量,此时当前线程不做操作,返回 V 的真实值。



CAS 是调用处理器底层指令来实现原子操作,那么处理器底层又是如何实现原子操作的呢?



处理器和物理内存之间的通信速度要远慢于处理器间的处理速度,所以处理器有自己的内部缓存。如下图所示,在执行操作时,频繁使用的内存数据会缓存在处理器的 L1、L2 和 L3 高速缓存中,以加快频繁读取的速度。





现在的服务器通常是多处理器,并且每个处理器都是多核的。每个处理器维护了一块字节的内存,每个内核维护了一块字节的缓存,这时候多线程并发就会存在缓存不一致的问题,从而导致数据不一致。



这个时候,处理器提供了总线锁定缓存锁定两个机制来保证复杂内存操作的原子性。



当处理器要操作一个共享变量的时候,其在总线上会发出一个 Lock 信号,这时其它处理器就不能操作共享变量了,该处理器会独享此共享内存中的变量。但总线锁定在阻塞其它处理器获取该共享变量的操作请求时,也可能会导致大量阻塞,从而增加系统的性能开销。



于是,后来的处理器都提供了缓存锁定机制,也就说当某个处理器对缓存中的共享变量进行了操作,就会通知其它处理器放弃存储该共享资源或者重新读取该共享资源。目前最新的处理器都支持缓存锁定机制。



在《Java业务开发常见错误100例》,有着很好的锁使用的避坑指南。

https://time.geekbang.org/column/article/209520

总结

这篇文章本来想把操作系统、数据结构与算法、锁都写一遍。但是去学习锁的时候,被深深吸引。所以满篇全是刘超老师的《Java性能调优实战》的对锁的讲解。这门课程对锁的讲解真的很超值。推荐给大家。





发布于: 2020 年 07 月 29 日 阅读数: 42
用户头像

dongge

关注

还未添加个人签名 2017.10.19 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
作业请添加“极客大学架构师训练营”,便于分类
2020 年 07 月 29 日 17:44
回复
没有更多了
架构师课程第八周总结