Linux上的的Java线程同步机制
现如今,一个服务端应用程序几乎都会使用到多线程来提升服务性能,而目前服务端还是以linux系统为主。一个多线程的java应用,不管使用了什么样的同步机制,最终都要用JVM执行同步处理,而JVM本身也是linux上的一个进程,那么java应用的线程同步机制,可以说是对操作系统层面的同步机制的上层封装。这里我说的操作系统,主要是的非实时抢占式内核(non-PREEMPT_RT),并不讨论实时抢占式内核(PREEMPT_RT) 的问题,二者由于使用场景不同,因此同步机制也会存在差异或出现变化。
线程同步意指同一个代码块或资源,抢占式内核在调度多个线程时,同时只能允许一个线程访问该资源。也就是编码时要对这部分资源或代码块进行加锁,使得一个线程执行获得锁之后,其他线程无法在获取该锁进而操作相应的资源。内核提供了许多lock机制,但考虑内核调度与进程上下文切换所造成的资源开销浪费,如何在合适的场景使用合适的锁就变得非常重要。
Linux OS的LOCK机制
Linux内核提供的lock原语(locking primitives 指lock方式)大致可以分为三类:
在non-PREEMPT_RT内核上,CPU local locks是基于禁止抢占调度和中断的原语lock机制。当一个进程希望在同一个CPU上持续运行,限制只访问同一个CPU的数据,这时只需要使用local locks,而不需要使用全局锁(global locks)就可以达成这一目的。由于使用场景限制,暂时不讨论这种锁机制。
Sleeping locks
CPU通过把线程状态又TASK_RUNNING状态改为WAIT状态,从而令收到lock保护的关键区同时只能由一个线程访问,而且线程则需要进入WAIT状态直至当前执行关键区代码的线程释放lock,在WAIT期间,线程不再参与任务调度,因此存在上下文切换而导致的开销。mutex,semephore均是常用的Sleeping locks。
上文中,当一个进程的WAIT时间相对于进程调度的时间很短时会造成性能问题,因为进程可能在频繁的任务调度上耗费大量时间。而Spinning locks则可以有效避免这一问题,当内核执行到Spinning locks关键区时,其他竞争线程不会被置为WAIT状态,而是在一个循环(spin)中保持随时活跃状态,因此不会产生上下文切换或调度的问题。
OS的其他同步操作
除了上述的lock算法实现线程同步,另外操作还提供lock-free的方式实现同步。操作系统中提供一组原子操作指令,例如compare-and-swap,test-and-set,fetch-and-add,read-modify-write。他们都是在一组原子操作中完成对目标内存的读和写动作,以此来防止线程之间的race condition。
CAS
由操作系统提供的一组原子操作用于线程同步,用于实现一些复杂的lock-free或wait-free的算法。
function cas(p: pointer to int, old: int, new: int) is
if *p ≠ old
return false
*p ← new
return true
CAS保证目标int的值一直是最新的,CAS操作必须返回是否执行了写入操作,这可以通过返回一个boolean或者返回目标int的值来实现。线程通过接收这个返回来决定,如果执行失败,通常线程休眠一段时间,所以CAS通常也类似于Spinlock的方式,如果执行成功,则可以视为本次竞争资源成功,可以执行关键区的代码,执行完之后通过set操作,再将目标值还原,在此过程中,由于目标值被本线程成功更新,因此其他线程是不能成功执行CAS操作的。CAS可能由于ABA 问题引起的异常情况。
CAS不会引起用户态与内核态的状态切换,因此效率比Lock要更高。
TAS
Test-and-set,使用原子操作,修改内存值并返回对应内存修改前的值,当一个线程在执行TAS操作时,其他线程不能同时操作对应内存。这与CAS是类似的机制,只是CAS直接比较操作为TRUE时才执行修改操作,同时CAS操作对象至少时32bit的内存,而TAS则可以在bit位上进行操作。
Java应用中的一些同步机制
Java应用层中一些常用的同步机制,一般是对底层lock或lock-free同步机制得一些封装。
AQS
AQS是Java中的一套线程同步框架,依赖于FIFO的等待队列来实现同步或lock机制,对于大多数依赖于一个atomicint来表示状态的同步场景都可以使用AQS框架。Mutex,Semaphore,ReentrantReadWriteLock都利用了AQS框架。
Synchronized
最常用得java同步原语,Jvm通过Object Markword 实现不同场景下的synchronized优化算法。Jvm为每个object关联一个 intrinsic lock(monitor),就是在执行lock操作时,将对应markword复制到线程stack上的lockrecord frame中。 synchronized有如下几种状态
- 无锁状态
- biased lock状态,线程通过CAS操作获取biased锁,此后markword将记录threadid,之后该线程重复获取lock时,将不再实际执行lock操作。JDK15之后已不再推荐使用biased lock
- lightweight状态,通过CAS获取锁,失败时将升级为heavyweight lock
- heavyweight状态,重量锁,线程进入WAIT状态
常用的一些同步方法
这些常用的方法通常由于平台或者语言不同,底层有不同的实现。
信号量是一个在多线程间共享的integer变量,通过acquire/release对semaphore进行增减操作,当integer值为0时,进行acquire的线程将进入等待队列,进入WAIT状态。
通常有如下两种类型的semaphore
- Counting Semaphores 用于同时有多个线程执行关键取代码,例如控制并发数(例如hystrix的semaphore模式)
- Binary Semaphores 和counting类型类似,但counting值只能0或1
Mutex
从语义上来说,Mutex和BianrySemaphore没有太大区别,我们可以用BinarySemaphore实现Mutex,反之亦然。二者的区别主要是在使用方式和场景上,Semaphore是基于Signal机制,而Mutex则是基于Lock机制,mutex主要用于对共享资源的同步保护,lock只能由一个线程拥有。
Condition基于Lock使用得条件锁,为线程提供在关键区执行时能够挂起等待知道满足一个条件。通过Lock.newCondition()创建一个新的Condition对象,只能在lock于unlock之间使用。
参阅
1.The Linux Kernel Lock: types and their rules
2.Local locks - Linux kernel 5.8
6.AQS AbstractQueuedSynchronizer
相关文章
- 袋鼠云平台代码规范化编译部署的提效性改进实践
- IDEA没有新建jsp文件按钮
- 多版本并发控制 MVCC
- Java开源博客系统AngelBlog发布
- 项目管理构建工具——Maven(基础篇)
- 使用 Java 操作 Redis
- Maven使用与学习
- Java学习路线之redis
- Java开源企业信息化平台O2OA移动端代码开源发布
- Android Pie SDK与Kotlin更合拍
- 当世界上只剩下一个Java程序员
- 我来告诉你,一个草根程序员如何进入BAT
- 少走弯路,给Java 1~5 年程序员的建议
- 使用Kotlin高效地开发Android App(一)
- 2018年在Java、Web和移动开发方面值得关注的12大开源框架
- 用Java调用Oracle存储过程的示例代码解析
- Java连接DB2数据库开发应用程序的编程步骤
- 一个将SQL语句嵌入到Java应用程序中的实例
- 缓存穿透、缓存并发、热点缓存之最佳招式
- 介绍几种大型的Oracle/SQL Server数据库免费版