转自:http://www.majin163.com/2014/03/17/synchronized1/
引言
JAVA是一门极易入门的语言,这一点尤其表现在JAVA中对象锁的使用和多线程编程上。所谓对象锁,就是可以直接在JAVA的任意Object加 锁(synchronized),也可以在通过任意Object进行线程的阻塞(Object.wait())和唤醒(Object.notify() or Object.notifyAll()),这种面向对象的锁与C系中的Mutex和Semaphore相比,一来省去了创建锁对象的麻烦,二来这种更加抽 象的封装使锁的使用更加人性化和便利。
然而这种便利带来了另外一个问题:说到C系中的mutext和Semaphore,都知道这是对操作系统中信号量的封装,其原理只要学过操作系统的 人都会非常清楚,因此这种锁虽然使用起来略麻烦,但是原理透明,这就为后期锁调优提供了可能。而JAVA中的synchronized虽然提供了更加友好 抽象的互斥原语,却很少有JAVA程序员了解synchronized背后的原理,甚至你会发现,JAVA面试官在考察你对JVM的了解程度时,基本上问 的都是GC相关的问题。
拿我个人来说,JAVA开发做了四五年,对synchornized可以说驾轻就熟,但是当被问到这些问题时,我只能无言以对:
- synchronized到底有多大开销?与CAS这样的乐观并发控制相比如何?
- 怎样使用synchronized更加高效?
- 与ReentrantLock(JDK1.5之后提供的锁对象)相比有什么优势劣势?
- 程序员可以对synchronized做哪些优化?
要回答这些问题,就需要对synchrnonized背后的原理一探究竟,在查阅了一些资料后,我惊讶地发现synchornized实现远比我想 象的复杂地多,一个简单的synchronized过程,可能会涉及到自旋锁(spinlock)、自适应自旋锁(adaptive spinlock)、轻量锁(lightweight lock)、偏向锁(biased lock)以及粗量锁(heavyweight lock),看起来synchronized是把各种复杂的锁过程封装在一起,帮助开发者无脑使用,在这一点上与C系可谓两个极端。
本文将分两个部分,第一部分初探篇,介绍synchronized的使用方法和原理。第二部分深探篇,将介绍synchronized背后的实现原理,带大家理解各种不同锁优化实现之间的转换,最后根据synchronized的实现原理,回答上文提到的四个问题。
另外,对于对象上的阻塞和唤醒,本文也会进行部分讲解。
本文内容很多来自互联网中的分享,由我进行了总结和发散性思考,对本文贡献较多的链接会贴在下一篇文章末尾。
初探篇
synchronized使用起来非常简单,有三种使用模式:
1. 作为修饰符加在方法声明上,synchronized修饰非静态方法时表示锁住了调用该方法的堆对象,修饰静态方法时表示锁住了这个类在方法区中的类对象(记住JAVA中everything is object),例如下述代码:
1
2
3
4
5
6
|
publicclassIncableInt{
privateintvalue=0;
publicsynchronized intincAndGet(){
return++value;
}
}
|
上述代码实现了一个线程安全的自增函数,当不同线程进入incAndGet()方法体时,会竞争这个IncableInt对象上的锁,通过锁的互斥性保证了该方法不会被不同线程同时进入。
2. 可以用synchronized直接构建代码块,上述的自增整数可以也可以写成下面的形式:
1
2
3
4
5
6
7
8
|
publicclassIncableInt{
privateintvalue=0;
publicintincAndGet(){
synchronized(this){
return++value;
}
}
}
|
上述代码可以达到同样的互斥效果,sychronized代码块竞争的是后面括号中的对象锁,我们常常可以在一些源码中看到用一个普通的Object作为synchronized对象,相当于C系中mutex的效果。
3. 在使用Object.wait()使当前线程进入该Object的阻塞队列时,以及用Object.notify()或 Object.notifyAll()唤醒该Object的阻塞队列中一个或所有线程时,必须在外层使用synchronized (Object),这是JAVA中线程同步的最常见做法。之所以在这里要强制使用synchronized代码块,是因为在JAVA语义中,wait有出 让Object锁的语义,要想出让锁,前提是要先获得锁,所以要先用synchronized获得锁之后才能调用wait,notify原因类似,另外, 我们知道操作系统信号量的增减都是原子性的,而Object.wait()和notify()不具有原子性语义,所以必须用synchronized保证 线程安全。
另外,在使用synchronized时有三个原则:
a) sychronized的对象最好选择引用不会变化的对象(例如被标记为final,或初始化后永远不会变),原因显而易见的,虽然 synchronized是在对象上加锁,但是它首先要通过引用来定位对象,如果引用会变化,可能带来意想不到的后果,对于需要synchronized 不同对象的情况,建议的做法是为每个对象构建一个Object锁来synchronized(不建议对同一个引用反复赋值)。当然将 synchronized作为修饰符修饰方法就不会有引用变化的问题,但是这种做法在方法体较大时容易违反第二个原则。
b) 尽可能把synchronized范围缩小,线程互斥是以牺牲并发度为代价的,这点大家都懂。
c) 尽量不要在可变引用上wait()和notify(),例如:
1
2
3
4
|
synchronized(a){
(1)
a.wait()
}
|
若其他线程在线程1进入(1)时更改了a值,那么线程1会直接抛出一个IllegalMonitorException,表示在a.wait()前没有获得a的对象锁。推荐的做法还是声明一个专门用于线程同步的Object,这个Object永远不变。
死锁与活锁
synchronized相当于C++中的mutex,也就是可重入的01信号量,JAVA通过这个关键字保证互斥语义,在 synchronized过程中因为加锁失败而进入阻塞队列的线程,只能通过其他线程释放锁来唤醒,因此使用synchronized可能引发死锁,使用 时需要留意。另外,synchronized也可能引发活锁,因为synchronized是不公平竞争,后来的线程可能先得到锁,进而可能导致先到的线 程持续饥饿,非公平竞争在很大程度上提升了synchronized吞吐率(why?答案在下一篇中揭晓)。
虽然wait()和notify()也是阻塞和唤醒,看起来和synchronized有点类似,但实际上无论是wait()还是notify() 的调用都是以获得锁为前提,因此不会在wait()或notify()上发生死锁,进一步讲,wait()或notify()没有互斥语义,没有互斥就没 有竞争,没有竞争就不会有死锁。另外,wait()操作是可能被其他线程interrupt掉的(抛出中断异常)。
这里有个概念容易混淆,所谓死锁与互相等待还是有很大区别的,使用wait()和signal()是可能出现两个以上线程互相等待的情况,这种互相 等待是可以通过加入新线程signal()来解开,造成这种互相等待大部分原因是业务逻辑使然,属于正常情况。而使用synchronized一旦出现两 个线程互相等待,必然是死锁。
可以说wait()和notify()是专门用于线程同步的,对应C中的Semaphore,synchronized是专门用于线程互斥的,JAVA中将互斥和同步分成两种不同原语,使用起来更加友好。
相关推荐
在 Java 中,锁机制是数据同步的关键,存在两种锁机制:synchronized 和 Lock。了解这两种锁机制的实现原理对于理解 Java 并发编程非常重要。 synchronized 锁机制是在软件层面实现的,它依赖 JVM 实现。 HotSpot ...
文中详细介绍了synchronized在JVM中的实现,侧重于其内部对象Monitor(监视器锁)的实现原理。讨论了监视器锁的加锁过程、锁的状态(偏向锁、轻量级锁、重量级锁)以及锁的膨胀升级过程。此外,文章还覆盖了锁优化...
在Java虚拟机(JVM)层面,`synchronized`使用了`monitorenter`和`monitorexit`指令来实现这一机制。一旦线程进入同步代码块,它会持有锁直到代码块执行完毕,然后释放锁。如果线程在执行过程中被中断或抛出异常,...
【Java中的`java.net.BindException: Address already in use: JVM_Bind`异常】 在Java编程中,当你尝试启动一个服务器端应用,如Tomcat,或者任何需要监听特定端口的服务时,可能会遇到`java.net.BindException: ...
JVM 致命错误日志详解 JVM 致命错误日志是 Java 虚拟机(JVM)在遇到致命错误时生成的日志文件,用于记录错误信息和系统信息。该日志文件可以帮助开发者和维护者快速定位和解决问题。 文件头:文件头是错误日志的...
Java中的`synchronized`关键字是实现线程安全的关键机制,它基于Java对象头的Mark Word进行锁的状态管理。Mark Word是一个动态变化的数据结构,用于存储对象的HashCode、分代年龄、锁状态标志等信息。在32位JVM中,...
【狂神说JVM探究】是一份集合了多种格式的学习资料,主要涵盖了Java虚拟机(JVM)的基础知识。这份资料出自B站上的【狂神说Java】系列教程,为快速入门JVM提供了详实的笔记。以下是根据这些资源可能包含的一些关键...
一种显式定义同步锁对象来实现锁,提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock锁。 # synchronized锁与lock锁的对比 Lock是显式锁,需要手动的开启和...
1. 确保线程互斥:Synchronized保证同一时间只有一个线程可以执行同步代码,防止数据竞争和不一致。 2. 保证共享变量的可见性:当一个线程修改了同步代码块中的共享变量,其他线程在获取锁后能立即看到最新值,这是...
《揭秘Java虚拟机-JVM设计原理与实现》这本书深入探讨了Java虚拟机(JVM)的工作原理及其在Java编程中的核心地位。Java虚拟机是Java平台的核心组成部分,它负责执行字节码,为开发者提供了跨平台的运行环境。以下是...
- **实现**:synchronized基于Monitor机制,通过JVM实现,锁状态有无锁、偏向锁、轻量级锁和重量级锁四种。 - **轻量级锁**:在没有多线程竞争时,减少传统重量级锁的开销,适应线程交替执行同步块的场景。如果...
用Kotlin编写的JVM的libp2p实现 :fire: :warning: 这是繁重的工作! :warning: 路线图 构建jvm-libp2p的工作分为两个阶段: 最小阶段(v0.x):旨在提供最基本的最小堆栈,以允许基于JVM的以太坊2.0客户端与依赖...
jvm-npm, 适用于JVM的兼容CommonJS模块加载器 JVM上Javascript运行时中的NPM模块加载支持。 实现基于 http://nodejs.org/api/modules.html,应该完全兼容。 当然,不包括完整的node.js API,因此不要期望依赖于它的...
【JVM探究】 Java虚拟机(JVM)是Java编程语言的核心组成部分,它负责执行Java程序,提供了一个跨平台的运行环境。理解JVM的工作原理对于优化Java应用程序性能至关重要。以下是一些关于JVM的关键知识点: 1. **JVM...
总的来说,`synchronized`在JVM中的实现涉及到字节码指令、锁优化策略和对象头的设计,这些机制共同确保了Java多线程环境下的正确性和效率。理解这些细节有助于开发者更好地理解和使用`synchronized`,并优化多线程...
在探究JVM线程状态以及Thread.sleep的实现原理时,我们首先需要了解Java线程与操作系统线程之间的关系。在Java虚拟机(JVM)中,每个线程通常都是以一对一的关系映射到操作系统线程上的。然而,尽管两者在实现上是...
Dive into JVM garbage collection logging, monitoring, tuning, and tools Explore JIT compilation and Java language performance techniques Learn performance aspects of the Java Collections API and get ...
JVM面试资料。 JVM结构:类加载器,执行引擎,本地方法接口,本地内存结构; 四大垃圾回收算法:复制算法、标记-清除算法、标记-整理算法、分代收集算法 七大垃圾回收器:Serial、Serial Old、ParNew、CMS、Parallel...
在myeclipse中将html文件改成jsp文件时myeclipse卡住;将之前的任务关掉;再打开时多次部署项目的时候报错
《OpenJDK中的JVM Hotspot实现源码解析》 在Java世界中,JVM(Java Virtual Machine)是运行Java程序的关键组件,它负责将字节码解释执行或即时编译为机器码,使得Java具备跨平台的能力。Hotspot是Oracle JDK和...