转自: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 ...
在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中,...
一种显式定义同步锁对象来实现锁,提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock锁。 # synchronized锁与lock锁的对比 Lock是显式锁,需要手动的开启和...
1. 确保线程互斥:Synchronized保证同一时间只有一个线程可以执行同步代码,防止数据竞争和不一致。 2. 保证共享变量的可见性:当一个线程修改了同步代码块中的共享变量,其他线程在获取锁后能立即看到最新值,这是...
- **实现**:synchronized基于Monitor机制,通过JVM实现,锁状态有无锁、偏向锁、轻量级锁和重量级锁四种。 - **轻量级锁**:在没有多线程竞争时,减少传统重量级锁的开销,适应线程交替执行同步块的场景。如果...
jvm-npm, 适用于JVM的兼容CommonJS模块加载器 JVM上Javascript运行时中的NPM模块加载支持。 实现基于 http://nodejs.org/api/modules.html,应该完全兼容。 当然,不包括完整的node.js API,因此不要期望依赖于它的...
《揭秘Java虚拟机-JVM设计原理与实现》这本书深入探讨了Java虚拟机(JVM)的工作原理及其在Java编程中的核心地位。Java虚拟机是Java平台的核心组成部分,它负责执行字节码,为开发者提供了跨平台的运行环境。以下是...
用Kotlin编写的JVM的libp2p实现 :fire: :warning: 这是繁重的工作! :warning: 路线图 构建jvm-libp2p的工作分为两个阶段: 最小阶段(v0.x):旨在提供最基本的最小堆栈,以允许基于JVM的以太坊2.0客户端与依赖...
【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探究】是一份集合了多种格式的学习资料,主要涵盖了Java虚拟机(JVM)的基础知识。这份资料出自B站上的【狂神说Java】系列教程,为快速入门JVM提供了详实的笔记。以下是根据这些资源可能包含的一些关键...
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和...
5-4JVM内置锁synchronized关键字详解.mp4