`

你所不知道的有关Java 和Scala中的同步问题

    博客分类:
  • java
阅读更多
原文:Things You Didn’t Know About Synchronization in Java and Scala

在实际应用中所有的服务端程序都需要在多线程之间进行某种同步。大多数同步已经有框架完成了,比如我们的web服务器,DB客户端和消息框架。Java和Scala提供了大量的组件用来实现稳定的多线程程序。包括对象池,并发集合,高级锁,执行上下文等。

为了更好的理解这些组件,我们深入了解一下最常用的同步原语——对象所。这个是用synchronized 关键字来实现的,在Java中它是非常流行的多线程原语。这也是其他更复杂模式的基础,比如线程池和连接池,并发集合等。

Synchronized 关键字主要用在以下两个场景:

作为方法的修饰,此方法在同一个时间只能被一个线程执行。
把一个代码块声明为临界区,任何时间只有一个线程能访问。
锁指令
同步代码块有两个专门的字节码指令,MonitorEnter 和 MoniterExit。这个不同于其它锁机制,比如java.util.concurrent包是有Java代码和本地调用来是实现的。

这些作用在对象上的指令是有开发人员在同步块中显示说明的。对于同步方法,锁被加到了“this”对象上。对于静态方法,锁被加到了类对象上。

同步方法有时候会引起坏的结果。其实一个例子是在不同的同步方法间会引起隐式依赖,因为它们共享同一把锁 。更坏的场景是在基类里(可能是一个第三方库)申明了一个同步方法,在子类里又增加了新的同步方法。这造成了不同层次之间的隐式同步依赖,会降低吞吐率甚至是死循环。这时应该使用私有的锁对象来防止偶然的共享,或者不使用锁。

编译器和同步
有两个字节码的指令用来实现同步原语。这并不常见,因为大多数字节码指令是互相独立的,通常通过把值放在线程的操作数堆栈上来互相“通信”。要加锁的对象也是从操作数堆栈装载的,通过引用变量或者方法返回对象把他们放在操作堆栈上。

如果只有其中一个指令被调用了,而另外一个没被调用,会发生什么?Java编译器不会产生只调用MonitorExit而没有调用MonitorEnter的代码。即使Java编译产生了这样的代码 ,在JVM看来这个代码也是非法的。这会让MonitorExit指令抛出一个IllegalMonitorStateException 异常。

一个更危险的例子是MonitorEnter加了锁,但却没有被对应的MonitorExit释放。这种情况下线程一直拥有锁,从而导致其他想获取这把锁的线程一直被阻塞。

为了防止发生一直被阻塞,Java编译器会以以下方式产生代码:一旦进入同步块或者方法,一定能执行到MonitorExit。在临界区抛出异常可以导致这个问题。


编译器使用的机制非常简单,当异常发生时如果没有经过MonitorExit,就增加catch语句来释放锁。

另一个问题是在enter和exit之间的锁对象存储在何处。注意多个线程可以同时执行同步块,使用不同的锁对象。如果锁对象是方法调用的结果,那么JVM极有可能会再次执行它,因为它可能会改变对象状态,或者不会返回同一个对象。如果是同一个对象,那么在monitor执行前这个变量和域已经被改变。

监视变量。为了计算,编译器为方法增加了一个隐式的变量,用来存储锁状态。这个是一个聪明的解决方案,因为它只增加了很小的开销就维护了锁对象,而不是使用并发栈把锁对象隐射到线程(这个结构需要同步)。我是在编译Takipi栈分析算法时发现这个新变量的。

注意这所有的工作都是有Java的编译器完成的。JVM可以非常完美处理只调用MonitorEnter来进入临界区而不用退出(或者相反),或者为方法使用不同的对象。

JVMLevel的锁
让我们更深入的看一下锁在JVM里是怎么实现的。为此我们将会查看HotSpot SE7的实现,因为这个实现每个VM都可能不一样。 因为加锁会影响代码的吞吐率,JVM加入了很大的优化来提高加锁解锁的效率。

其中一个强大机制是线程锁偏向。锁特性是每个Java对象都具有的,很想系统的hashcode或者定义类的引用。不管类的类型是什么,这个都成立(甚至你可以使用一个原始数组作为锁)。

这些类型的数据都存在对象的头部(也被称为对象的标记)。这些数据中的一部分用来描述对象锁的状态。这包括描述对象的锁状态(加锁/没加锁)的比特标志位,一个指向现在拥有锁的线程。

为了节省对象头部的空间,Java的线程对象会被分配在VM栈的低位,这可以减少地址长度从而节约对象头部的比特数(64位的只需要54位,32位的只需要23位)。

64位的比特位分配情况:


锁算法
当JVM尝试去获取一个对象的锁时,会采用从乐观到悲观的步骤来获取。

当线程成为对象锁的拥有者时,这就算加锁成功了。线程是否把指向自己的引用存入对象的头部决定着加锁是否成功。

获取锁的步骤。第一步是使用CAS操作来尝试获取锁。这个操作非常高效,因为常常有与之对应的CPU指令(比如 cmpxchg)。 CAS操作和OS线程停止程序作为对象的同步原语。

如果锁是空闲或者说这个所可以被这个线程优先获取,那么线程就立即获取了这个锁。如果CAS失败了,那么JVM先会自旋一轮,然后线程会睡眠,直到下次CAS。如果这些最初的尝试失败了,线程会把自己放入阻塞状态,并进入竞争锁的列表,开始一系列的自旋。

释放锁。通过执行MonitorExit指令来退出临界区,锁的拥有者会尝试着检查是否可以唤醒正在等待这把锁的线程 。这个过程被称为选择一个继承者。这可以增加活跃度,阻止出现当锁释放后仍有线程在等待这把锁。

调试服务端多线程问题有点难度,因为它们非常依赖调试时机和OS的特性。这也是让我们实现TAkipi的原因之一。

延伸阅读
1.如果你对JVM的锁是如何的实现很感兴趣,可以查看这个源代码 http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/42ca4002efc2/src/share/vm/runtime/synchronizer.cpp

2. 所有关于Java同步API的介绍和技术 http://docs.oracle.com/javase/tutorial/essential/concurrency/index.html

3. Scala的并发 http://twitter.github.io/scala_school/concurrency.html

(全文完)如果您喜欢此文请点赞,分享,评论。
分享到:
评论

相关推荐

    面向 Java 开发人员的 Scala 指南

    对于Java开发者而言,学习Scala不仅能帮助他们在现有技能的基础上进一步提升,还能为解决现代软件工程中的挑战提供新的工具和技术。Scala以其独特的设计理念和强大的功能集,已经成为越来越多开发者的首选语言之一。...

    Scala语言分析报告

    这种模型避免了传统多线程中的同步问题,简化了并发编程的复杂性。 Scala的函数式编程特性如高阶函数、不可变数据结构和柯里化,使得编写简洁、无副作用的代码成为可能,这对于构建大规模并发应用尤其有益。同时,...

    Scala程序设计:Java虚拟机多核编程实战1

    《Scala程序设计:Java虚拟机多核编程实战》是一本专为程序员和有经验的Java开发者设计的书籍,旨在引导读者掌握Scala语言,利用其功能强大的特性进行并发编程。Scala是一种融合了函数式和面向对象编程特点的静态...

    Scala-ts–Scala至TypeScript的代码生成器

    Scala 是一种静态类型的、面向对象和函数式的编程语言,它运行在 Java 平台上,利用了 JVM 的强大功能。TypeScript 则是 JavaScript 的超集,添加了静态类型和其他高级特性,用于构建大型、可维护的前端应用程序。 ...

    图灵书籍(Scala程序设计(第2版).pdf+Scala程序设计-JAVA虚拟机多核编程实战.pdf)

    2. **函数式编程**:Scala中的纯函数、不可变数据结构、柯里化、尾递归和Monad等概念。 3. **面向对象编程**:Scala如何融合面向对象和函数式编程,包括特质(trait)和混合类型(mixin)。 4. **并发编程**:Scala...

    Scala 程序设计 中+英文

    5. ** Actors 和 Concurrency**:Scala内置了Actors模型,用于构建并发和分布式系统,帮助开发者避免线程同步问题。 6. **高级特性**:涵盖类型参数、抽象类型、特质、包对象、混合类型和匿名函数等,这些特性使...

    actors in scala

    由于Scala可以与Java无缝互操作,因此Scala中的actors模型对Java程序员来说也是一个值得研究和应用的并发模型。此外,鉴于Scala的actors模型和Akka库在并发与分布式系统领域中的应用越来越广泛,本书也适合那些希望...

    scala编程入门教材

    8. ** Actors和Concurrent Programming**:Scala提供Actors模型支持并发编程,这是一种轻量级的线程模型,能够简化多线程同步问题。 9. **Akka框架**:虽然不是Scala语言的一部分,但Akka是一个流行的用Scala编写的...

    Scala编程完整版

    Scala编程完整版 Scala,全称“Scalable Language”...无论你是Java开发者希望扩展技能,还是对函数式编程感兴趣,Scala都值得你投入时间和精力去学习。《Scala编程完整版》这本书将是你探索这个语言世界的宝贵资源。

    scala 2.12.4

    解压缩后,将`bin`目录添加到你的系统路径中,这样你就可以在命令行中使用`scala`命令来启动REPL(Read-Eval-Print Loop)或编译Scala源代码。同时,`lib`目录包含了Scala运行库,供其他项目引用。 总的来说,Scala...

    Scala 基础.zip

    6. ** Actors 和 Concurrency**:Scala的Actors模型提供了一种安全的并发处理方式,它通过消息传递来管理共享状态,降低了线程同步的复杂性。 7. **集合库**:Scala的集合库非常强大,提供了各种高效的集合实现,如...

    Scala programming language.pdf

    通过上述知识点的介绍,我们可以看出Scala不仅具备现代编程语言所应有的一切特性,而且在设计上特别强调简洁性、灵活性和安全性。无论是面向对象编程还是函数式编程,Scala都能提供强大的支持。此外,Scala与JVM的...

    搭建eclipse+scala+maven

    - Scala-IDE和Maven都能够处理Scala和Java代码的交叉编译,确保在构建过程中正确地处理引用关系。 遵循以上步骤,你就成功地建立了一个Eclipse+Scala+maven的开发环境,可以进行Scala Web项目的开发。这个环境不仅...

    官网下载好的 scala-2.11.8.rar

    学习Scala不仅能够让你掌握一种强大而现代的编程语言,还能带你进入函数式编程的世界,提升解决问题的思路和能力。无论你是Java开发者希望拓宽视野,还是对函数式编程感兴趣,Scala都是一个值得投入时间和精力的学习...

    IDEA 创建scala工程并打指定的依赖包

    通过上述步骤,你已经学会了如何在IDEA中创建Scala项目并打包指定依赖。这对于Scala项目的开发和部署非常有帮助。记得定期更新IDEA版本和Scala插件,以获得最佳的开发体验。此外,合理配置依赖可以显著提高项目的...

    scala核心编程总结

    虽然可变集合在某些场景下可能带来便利,但在多线程环境中应谨慎使用,以避免数据竞争和同步问题。Scala提供的可变集合包括`ArrayBuffer`、`Array`等。 #### 示例分析 为了更好地理解Scala集合类的强大之处,考虑...

    intellij-ide17的scala插件

    通过安装这个插件,用户可以在IntelliJ IDEA中编写、运行和调试Scala代码,享受到与Java开发类似的高效体验。以下是该插件的一些核心功能: 1. **语法高亮与代码提示**:Scala插件为Scala语法提供了颜色编码,使...

    基于Scala与Java的轨道交通数据计算服务设计源码

    项目中,Scala源文件占据绝大多数,显示了Scala在数据处理领域的应用优势,这可能得益于Scala语言简洁的语法和强大的函数式编程特性。与此同时,Java源文件虽然数量较少,但依旧承担了关键的服务功能模块,表明Java...

    官网scala-2.11.8.tar.gz

    - ** Actors模型**:Scala内置对Akka框架的支持,允许并发和分布式计算,而不会遇到线程同步的问题。 - **复合性管理**:通过特质(Traits)和柯里化(Currying),Scala提供了一种组织复杂代码结构的方式。 - **...

    scala-2.11.12.msi 安装程序

    Scala是一种强大的、现代的编程语言,它在IT领域中被广泛应用,特别是在大数据处理和分布式计算中,例如Apache Spark就是基于Scala开发的。标题中的"scala-2.11.12.msi"指的是Scala的特定版本2.11.12的Windows安装...

Global site tag (gtag.js) - Google Analytics