深入浅出Java多线程(九):synchronized与锁
引言
大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第九篇内容:synchronized与锁。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!
在现代软件开发中,多线程技术是提升系统性能和并发能力的关键手段之一。Java作为主流的编程语言,其内置的多线程机制为开发者提供了丰富的并发控制工具,其中synchronized关键字及其背后的锁机制扮演了至关重要的角色。理解并掌握synchronized的使用原理与特性,有助于我们设计出高效且线程安全的应用程序。
Java中的每个对象都可以充当一把锁,这意味着任何实例方法或静态方法可以通过synchronized关键字来实现同步控制,从而确保同一时间只有一个线程能访问临界资源。例如,一个简单的实例方法同步:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
在这个例子中,increment方法被synchronized修饰,使得在同一时刻只能有一个线程对count变量进行递增操作,避免了数据竞争带来的不一致性问题。
同时,类锁的概念也是基于对象锁——类的Class对象同样可以作为锁,用于同步类的静态方法或某一特定对象实例上的代码块,如:
public class SharedResource {
public static synchronized void modifyStaticData() {
// 修改共享静态数据
}
}
这里,modifyStaticData方法通过类锁保护了所有实例共享的静态资源,保证了在多线程环境下的数据安全性。
深入探究Java多线程中的synchronized关键字及锁机制,我们会发现Java虚拟机为了优化锁的性能,引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,并且支持锁的自动升级和降级策略。这些机制能够根据实际的并发场景动态调整锁的表现形式,以最小化锁的获取和释放开销,进而提高系统的并发性能和响应速度。接下来,我们将逐一剖析这些概念和技术细节,以便更全面地理解和运用Java中的锁机制。
Java锁基础
在Java多线程编程中,锁机制是实现并发控制的核心手段之一。这里的“锁”基于对象的概念,任何Java对象都可以充当一把锁来保护共享资源的访问,确保同一时间只有一个线程可以执行临界区代码。synchronized关键字作为Java内置的关键同步工具,被广泛用于实现线程间的互斥操作。
synchronized关键字详解
synchronized关键字主要有三种使用形式:
实例方法锁定:当
synchronized关键字修饰实例方法时,它隐式地获取了当前对象实例作为锁:public class SynchronizedExample {
private int counter;
public synchronized void increment() {
counter++;
}
}在上述代码中,
increment方法被synchronized修饰,意味着每次仅有一个线程能执行该方法内部逻辑,即修改counter变量。静态方法锁定:如果
synchronized修饰的是静态方法,则锁对象为类的Class对象,所有实例共享这把锁:public class SynchronizedExample {
private static int sharedCounter;
public static synchronized void incrementStatic() {
sharedCounter++;
}
}在这个例子中,对
incrementStatic方法的访问将受到类锁的保护,确保在多线程环境下,对sharedCounter的更新是原子性的。代码块锁定:通过
synchronized关键字包裹一个代码块,显式指定锁对象:public class SynchronizedExample {
private final Object lock = new Object();
public void blockLockingMethod() {
synchronized (lock) {
// 临界区代码
}
}在这里,我们创建了一个独立的对象
lock用作锁,只有获得了这把锁的线程才能执行代码块内的内容。
synchronized关键字保证了其修饰的方法或代码块在同一时间只能由单个线程访问,从而避免了因多个线程同时修改数据导致的数据不一致问题,有效地实现了多线程环境下的同步控制。随着JVM对锁性能优化的不断深入,还引入了偏向锁、轻量级锁和重量级锁等不同级别的锁状态,使得Java多线程同步更加灵活高效。
synchronized原理
在Java多线程编程中,synchronized关键字所实现的同步机制深入底层,与JVM内部对象头结构密切相关。每个Java对象都拥有一个对象头(Object Header),它是内存中存放对象元数据的地方,包含了对象的Mark Word区域,这个区域用于存储对象的hashCode、GC分代年龄以及锁状态等信息。
Java对象头与锁状态
对象头结构:非数组类型的Java对象,其对象头占用2个机器字宽,对于32位系统是32位,64位系统则是64位。Mark Word中的一部分空间被用来记录锁的状态,包括无锁、偏向锁、轻量级锁和重量级锁四种状态。
| 长度 | 内容 | 作用 |
|---|---|---|
| 32/64bit | Mark Word | 存储对象的hashCode或锁信息等 |
| 32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
| 32/64bit | Array length | 数组的长度(如果是数组) |
这里着重关注一些Mark Word 的内容:
| 锁状态 | 29bit或者61bit | 第1bit是否偏向锁 | 第2bit锁标志位 |
|---|---|---|---|
| 无锁 | 0 | 01 | |
| 偏向锁 | 线程ID | 1 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 此时第1bit不用于标识偏向锁 | 00 |
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 此时第1bit不用于标识偏向锁 | 10 |
锁状态转换:
无锁状态:没有任何线程持有该对象锁,所有线程都可以尝试修改资源。 偏向锁:当一个线程首次获得锁时,会将当前线程ID写入对象头的Mark Word中,后续进入同步代码块时只需检查是否为当前线程持有即可快速获取锁。例如,若只有一个线程长期访问某一对象,则可以避免不必要的CAS操作和自旋消耗。
class BiasedLockExample {
private int count;
public void increment() {
synchronized (this) {
count++;
}
}
}
在上述例子中,如果increment方法仅由一个线程执行,那么JVM可能会将对象标记为偏向锁,从而提高效率。
轻量级锁:当存在多个线程竞争同一锁,但实际发生锁竞争的概率较小的情况下,JVM使用轻量级锁来避免频繁的线程阻塞和唤醒开销。轻量级锁通过CAS操作试图将当前线程栈中的锁记录地址替换到对象头的Mark Word中,如失败则表明存在锁竞争,转而升级为自旋或重量级锁。 重量级锁:当锁竞争激烈时,轻量级锁无法满足需求,就会升级为依赖于操作系统的互斥量(mutex)实现的重量级锁。此时线程将被挂起,直到锁释放后重新调度,降低了CPU的利用率但确保了线程间互斥性。
Java虚拟机通过对象头的Mark Word动态调整锁状态以适应不同场景下的并发控制需求,实现了从偏向锁、轻量级锁到重量级锁的平滑过渡,有效提升了多线程环境下程序的性能表现。通过灵活运用和理解这些锁状态及其背后的原理,开发者能够更好地优化多线程应用中的同步逻辑。
Java锁升级机制
在Java多线程同步中,synchronized关键字实现的锁具有动态升级的能力,从偏向锁到轻量级锁再到重量级锁,根据竞争情况自动调整以优化性能。
偏向锁
偏向锁是为了解决大多数情况下只有一个线程频繁获得锁的情况。当一个线程首次获取对象锁时,JVM会将其设置为偏向锁,并将该线程ID记录在对象头的Mark Word中。后续该线程再次进入同步代码块时,只需简单地验证Mark Word中的线程ID是否与当前线程一致即可快速获取锁。例如:
public class BiasedLockExample {
private int sharedResource;
public void access() {
synchronized (this) {
// 仅有一个线程长期访问此方法时,偏向锁生效
sharedResource++;
}
}
}
如果其他线程尝试获取已被偏向的锁,系统会检查偏向锁是否有效并进行撤销操作,通过CAS尝试替换Mark Word的内容。若失败,则表明存在锁竞争,此时偏向锁升级至轻量级锁。
其操作流程如下图:

下图总结了偏向锁的获得和撤销流程:

轻量级锁
轻量级锁主要应用于多个线程间交替访问同一对象但不存在大量持续竞争的场景。当线程试图获取锁时,它首先会在自己的栈帧中创建一个用于存储锁记录的空间(Displaced Mark Word),然后通过CAS操作尝试将对象头的Mark Word替换为指向锁记录的指针。成功则表示获得锁;否则,线程开始自旋(循环尝试获取锁)。
public class LightweightLockExample {
private int sharedResource;
public void access() {
Object lock = new Object();
synchronized (lock) {
// 若多个线程短暂交替访问此方法,轻量级锁生效
sharedResource++;
}
}
}
自旋次数并非固定不变,而是采用了适应性自旋策略,即根据历史成功率动态调整自旋次数。如果经过若干次自旋后仍未能获得锁,则轻量级锁升级为重量级锁。
轻量锁操作流程如下:

重量级锁
重量级锁依赖于操作系统的互斥量(mutex)来实现线程间的互斥控制。当锁竞争激烈,轻量级锁无法满足需求时,锁状态会转换为重量级锁。这时,请求锁的线程会被挂起并放入等待队列中,直至持有锁的线程释放锁资源。
public class HeavyweightLockExample {
private static final Object lock = new Object();
public void concurrentAccess() {
synchronized (lock) {
// 若大量并发线程同时访问此方法,可能导致锁升级为重量级锁
// 线程将被操作系统调度器挂起和唤醒
performHeavyOperation();
}
}
private void performHeavyOperation() {
// 执行耗时较长的操作...
}
}
重量级锁虽然会导致线程阻塞及上下文切换,但它确保了在高度竞争环境下的公平性和线程安全。当调用wait()或notify()方法时,即使原本是轻量级或偏向锁,也会先膨胀成重量级锁,以便正确管理线程的阻塞和唤醒状态。
总结来说,Java锁的升级机制是一种根据实际运行状况动态调整同步成本的技术手段,使得在多种并发场景下都能尽可能保持高效率和线程安全性。
锁对比与选择
在Java多线程同步中,有三种主要的锁类型:偏向锁、轻量级锁和重量级锁。每种锁都有其特定的适用场景及性能特性。
偏向锁
优点:当只有一个线程长期独占对象锁时,偏向锁几乎无额外开销,获取和释放锁的速度接近非同步方法调用。 缺点:当存在锁竞争或者程序执行过程中锁的所有者发生变化时,需要撤销偏向锁并升级为更高级别的锁,这个过程会产生额外的系统开销。 适用场景:适用于大部分时间只由一个线程访问同步块的场合。
案例:
public class BiasedLockExample {
private int sharedResource;
public void exclusiveAccess() {
synchronized (this) {
// 若只有主线程频繁访问此方法,则偏向锁效率高
sharedResource++;
}
}
}
轻量级锁
优点:相比于重量级锁,轻量级锁通过自旋避免了线程上下文切换带来的开销,在没有其他线程竞争的情况下能快速获得锁,提高了程序响应速度。 缺点:如果多个线程同时争夺锁,轻量级锁会导致较多的CAS操作以及可能的长时间自旋等待,反而浪费CPU资源。 适用场景:适用于线程间对锁的竞争不激烈且锁持有时间较短的情况。
案例:
public class LightweightLockExample {
private final Object lock = new Object();
public void concurrentAccess() {
synchronized (lock) {
// 若并发线程交替短暂持有锁,轻量级锁效果好
processData();
}
}
private void processData() {
// 执行一些快速计算或短期持有的共享资源访问...
}
}
重量级锁
优点:确保了线程间的互斥性和公平性,不会因自旋消耗过多CPU资源,阻塞未获得锁的线程,保证了系统的稳定性。 缺点:获取和释放锁涉及操作系统层面的信号量操作,导致较大的上下文切换开销,因此在高并发、锁竞争激烈的场景下性能较低。 适用场景:适用于高度竞争性的环境,即大量并发线程同时请求同一锁资源的情况。
案例:
public class HeavyweightLockExample {
private static final Object LOCK = new Object();
public void criticalSection() {
synchronized (LOCK) {
// 在大量并发线程竞争同一锁时,重量级锁能确保公平性和稳定性
accessSharedResource();
}
}
private void accessSharedResource() {
// 访问公共资源,如数据库连接、文件写入等耗时较长的操作...
}
}
综上所述,根据应用中的具体并发模式和锁争用情况,合理选择合适的锁类型至关重要。在实际编程中,JVM会根据实际情况自动进行锁状态的调整和升级,但开发人员也应具备理解这些锁机制的能力,并适时调整JVM参数以优化程序性能。例如,若确定应用程序不存在偏向锁的优势场景,可考虑禁用偏向锁功能。
总结与建议
Java多线程中,synchronized关键字及锁机制的运用涉及到从偏向锁到轻量级锁再到重量级锁的动态升级过程。在设计并发程序时,理解并合理选择锁策略对于提高系统性能至关重要。
偏向锁旨在优化单一线程访问临界区的场景,通过记录当前持有锁的线程ID来避免无竞争时的额外开销。但当其他线程尝试获取锁时,需撤销偏向锁,并可能升级为轻量级锁。
public class BiasedLockDemo {
private int count;
public void increment() {
synchronized (this) {
// 偏向锁适用于只有一个线程长期执行此方法的情况
count++;
}
}
}
轻量级锁利用CAS操作和自旋机制,减少线程阻塞带来的上下文切换成本,在低竞争环境下提升响应速度。然而,若存在持续锁竞争,过多的自旋可能导致CPU空耗,此时会转为重量级锁。
public class LightweightLockDemo {
private final Object lock = new Object();
public void process() {
synchronized (lock) {
// 在短暂且交替访问同步块的情况下,轻量级锁能提供较好的性能
doWork();
}
}
private void doWork() {
// 执行快速计算或读取共享资源的操作...
}
}
重量级锁虽然开销较大,但确保了互斥性和公平性,尤其适合于高度竞争的同步场景。它通过操作系统互斥量实现,能够防止长时间占用CPU资源的自旋等待。
public class HeavyweightLockDemo {
private static final Object LOCK = new Object();
public void criticalSection() {
synchronized (LOCK) {
// 当多个线程频繁争夺同一锁资源时,重量级锁能提供稳定的保护
accessSharedResource();
}
}
private void accessSharedResource() {
// 访问需要严格同步控制的公共资源...
}
}
在实际开发中,JVM默认启用偏向锁和轻量级锁功能,但根据具体应用场景,可以通过调整JVM参数如-XX:UseBiasedLocking、-XX:+/-UseLightweightLocking等来控制锁行为。同时,关注代码结构,尽可能减少不必要的锁竞争,优化数据结构,是提高多线程程序效率的关键所在。通过深入理解锁升级机制和每种锁的特点,开发者可以更好地权衡并发处理中的性能和安全性问题。
本文使用 markdown.com.cn 排版
深入浅出Java多线程(九):synchronized与锁的更多相关文章
- 深入浅出Java多线程(2)-Swing中的EDT(事件分发线程) [转载]
本系列文章导航 深入浅出Java多线程(1)-方法 join 深入浅出Java多线程(2)-Swing中的EDT(事件分发线程) 深入浅出多线程(3)-Future异步模式以及在JDK1.5Concu ...
- Java多线程-同步:synchronized 和线程通信:生产者消费者模式
大家伙周末愉快,小乐又来给大家献上技术大餐.上次是说到了Java多线程的创建和状态|乐字节,接下来,我们再来接着说Java多线程-同步:synchronized 和线程通信:生产者消费者模式. 一.同 ...
- Java多线程专题5: JUC, 锁
合集目录 Java多线程专题5: JUC, 锁 什么是可重入锁.公平锁.非公平锁.独占锁.共享锁 可重入锁 ReentrantLock A ReentrantLock is owned by the ...
- java多线程--6 死锁问题 锁Lock
java多线程--6 死锁问题 锁Lock 死锁问题 多个线程互相抱着对方需要的资源,然后形成僵持 死锁状态 package com.ssl.demo05; public class DeadLock ...
- java 多线程8 : synchronized锁机制 之 方法锁
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数 ...
- Java多线程学习——synchronized锁机制
Java在多线程中使用同步锁机制时,一定要注意锁对对象,下面的例子就是没锁对对象(每个线程使用一个被锁住的对象时,得先看该对象的被锁住部分是否有人在使用) 例子:两个人操作同一个银行账户,丈夫在ATM ...
- 深入浅出Java多线程
Java给多线程编程提供了内置的支持.一个多线程程序包含两个或多个能并发运行的部分.程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径. 多线程是多任务的一种特别的形式,但多线程使用了 ...
- Java多线程同步方法Synchronized和volatile
11 同步方法 synchronized – 同时解决了有序性.可见性问题 volatile – 结果可见性问题 12 同步- synchronized synchronized可以在任意对象上加 ...
- Java多线程:synchronized的可重入性
从Java多线程:线程间通信之volatile与sychronized这篇文章中我们了解了synchronized的基本特性,知道了一旦有一个线程访问某个对象的synchronized修饰的方法或代码 ...
- java 多线程12 : 无锁 实现CAS原子性操作----原子类
由于java 多线程11:volatile关键字该文讲道可以使用不带锁的情况也就是无锁使变量变成可见,这里就理解下如何在无锁的情况对线程变量进行CAS原子性及可见性操作 我们知道,在并发的环境下,要实 ...
随机推荐
- freeswitch xml_rpc模块
概述 freeswitch有非常多的周边模块,给我们提供各种各样的功能,有些功能在适当的场景下可以极大的方便我们的开发和应用. 今天我们介绍一个不常用的模块mod_xml_rpc. freeswitc ...
- C# 防XSS攻击 示例
思路: 对程序代码进行过滤非法的关键字 新建控制台程序,编写代码测试过滤效果 class Program { static void Main(string[] args) { //GetStrReg ...
- APB Slave Design
APB Slave Design module apb_slave #( REG1_ADDR = 8'h00, REG2_ADDR = 8'h04, REG3_ADDR = 8'h08 ) ( // ...
- 23- 数码管动态显示02-转换BCD码
1.BCD码 数码管动态显示的data[19:0]使用二进制数表示的多位十进制数,不能直接生成段选和片选信号,需要使用BCD码表示的十进制数 BCD码(Binary-Coded Decimal),又称 ...
- Oracle官网下载软件需要登录Oracle账户问题
问题描述 当我们在Oracle官网上下载JDK时,(JDK下载地址)系统会提示需要登录Oracle账户.对于没有Oracle账户的人来说,注册账户太繁琐. 没有账户怎么办??? 此处推荐一个靠谱的网站 ...
- [转帖]Difference between localhost and 127.0.0.1?
https://www.tutorialspoint.com/difference-between-localhost-and-127-0-0-1#:~:text=The%20most%20signi ...
- 【转帖】mysql一个索引块有多少指针_深刻理解MySQL系列之索引
索引 查找一条数据的过程 先看下InnoDB的逻辑存储结构:node 表空间:能够看作是InnoDB存储引擎逻辑结构的最高层,全部的数据都存放在表空间中.默认有个共享表空间ibdata1.若是启用in ...
- Rsync的一个高级应用
Rsync的一个高级应用 背景 2019年刚开始接触linux时. 有一个很恶心的场景. 很多人为了简单起见, 提交数据库的修改(数据结果和预制数据) 都不是增量处理, 都是全量提交过来. 所以会造成 ...
- [转帖]010 Linux 文本统计与去重 (wc 和 uniq)
https://my.oschina.net/u/3113381/blog/5427461 wc 命令一般是作为组合命令的一员与其他命令一同起到统计的作用.而一般情况下使用 wc -l 命令较多. u ...
- JDK发布版本的总结
https://www.oracle.com/java/technologies/javase/8all-relnotes.html 从官网总结一下每个版本的发布日期 Java SE 8u141 Ad ...