j**a 中的 synchronized 关键字用于实现线程同步,保证最多有一个线程同时执行由 synchronized 修饰的方法或块。 它基于进入和退出监视器对象进行同步。
在 j**a 中,synchronized 可用于装饰方法或 **块:
修饰方法时,将整个方法主体视为关键区域,并且只有一个线程可以执行该方法。
当一个块被修饰时,只有拥有该块对象的线程才能执行该块。
synchronized 关键字可用于解决多个线程并发访问共享资源时的线程安全问题。 当线程进入同步的方法或块时,它会自动获取对象上的锁,其他线程必须等待线程释放锁才能执行。 这确保了只有一个线程同时访问共享资源,避免了数据不一致和并发访问。
同步关键字支持的锁类型:
悲观锁:每次访问共享资源时都会添加锁。
不公平的锁:线程获取锁的顺序不一定与线程被阻塞的顺序相同。
重入锁:已获取锁的线程可以再次获取锁。
独占或独占锁:一次只能有一个线程可以持有锁,其他未获得锁的线程进入块。
1)同步修改方法,示例**:
同步修改实例方法 * public synchronized void method() ** synchronized 修改静态方法 * public static synchronized void method()。以同步修改示例方法为例,对源码实现进行反编译:
在同步修改方法中,底层是使用 acc synchronized 关键字进行隐式锁定和解锁。
2)同步修改器块,示例:
synchronized modifier block: lock 对象是类 * public void method() 的实例对象 synchronized modifier block: lock 对象是类 * public void method() 的类对象。将同步修改的 ** 块中的锁对象作为实例对象,对源代码进行反编译
其中,当 synchronized 修改 ** 块时,底层通过 monitorenter 和 monitorexit 两个关键字进行锁定和解锁
在执行同步之前,使用 monitorenter 进行锁定。
执行同步**后,使用 MonitorExit 释放锁定。
当引发异常时,也会使用 MonitorExit 释放锁。
不管是acc同步的关键字,还是monitorenter和monitorexit关键字,底层都是通过获取monitor锁来锁锁和解锁,monitor锁是通过objectmonitor实现的,虚拟机中的objectmonitor数据结构如下(用C++实现):
objectmonitor()同步锁定过程,如图所示:
处理流程: 1)当多个线程同时访问一个同步时,会先进入入口列表队列(阻塞队列)进行阻塞。
2)当线程获取到对象的对象锁时,进入临界区域,将对象锁中的owner变量设置为当前线程(即获取对象锁)。
3)如果持有对象锁的线程调用wait()方法,则当前持有的对象锁将被释放,所有者变量将恢复为null,线程将进入waitset集合并等待被唤醒。
4)等待集中的线程被唤醒并再次放入entrylist队列以竞争锁。
5)如果当前线程完成执行,则将释放对象锁,并重置变量的值,以便其他线程可以进入获取锁。
J**A 对象由 JVM 内存中的三个区域组成:
对象标头:它分为标记词(标记字段)、类指针(类型指针)和数组长度(如果 j**a 对象是数组)。
类指针:指向对象的类元数据的指针,虚拟机使用该元数据来确定对象是哪个类的实例。
数组长度:存储数组对象的长度。
实例数据:存储对象的真实有效信息(如字段信息),包括该类和父类的信息。
对齐填充:没有特殊的含义,因为虚拟机要求对象起始地址必须是 8 字节的整数倍,所以这个区域的目的只是字节对齐。
在 32 位虚拟机中,标记词的组成如下:
Mark Word 包含对象的哈希码、锁偏置的线程 ID、偏置锁标记、锁标识位和偏置锁时间戳(纪元:2 位)。
其中:无锁定状态:锁定ID位为01,偏置锁标记为0,计算对象的哈希码并写入对象头,当对象锁定时,将哈希码移动到监视器。
偏置锁:锁标识符为 01,偏置锁标记为 1,锁偏置的线程 ID 是持有偏置锁的线程的线程 ID。
轻量级锁:锁标识符设置为 00,标记工作中的指针通过 CAS 旋转指向锁记录。
重量级锁:锁定标识符设置为 10,标记工作中的指针指向管道(监视器)。
GC ID:锁 ID 位为 11,对象的代龄是指 J**A 对象发生 GC 的次数,当 GC 发生时,对象每次都会在幸存者区域复制,代龄为 +1。 当受试者达到设定的年龄阈值时,受试者从年轻一代晋升到老一代。 该参数占用 4 位(即 2 4-1=15),默认情况下,并行 GC 的年龄阈值为 15,并发 GC 的年龄阈值为 6。
从 jdk1自 6 日起,同步的实现机制进行了大幅度调整,除了使用 jdk15、除了引入的CAS自旋锁外,还增加了自适应自旋锁、锁消除、锁粗化、偏置锁、轻量级锁等优化策略,大大提升了同步的性能,同时语义清晰,操作简单,无需手动关闭,所以建议在允许的情况下尽量使用synchronized关键字。
同步锁主要有四种状态,性能从高到低依次为:无锁状态、偏置锁状态、轻量级锁状态、重量级锁状态。 锁可以从偏置锁升级为轻量级锁,再升级为重量级锁,但锁升级是单向的,即锁只能从低升级到高,不会出现锁降级的情况。
在 jdk1 中6、偏置锁和轻量级锁默认开启,偏置锁可以通过 -xx:-usebiasedlocking 禁用。
例:
object object = new object();进入临界区后同步(对象)在线程进入临界区之前,将锁 ID 设置为 01,将偏置锁标记为 0,并计算对象的哈希码并写入对象头。
偏置锁升级过程
当线程 A 进入临界区时,发现锁标志为 01,偏置锁标记为 0,立即将线程 A 的 threadID 记录到对象的标记字中,并将偏置锁更新为标记 1。
如果线程 A 再次进入临界区域,发现 Mark Word 中记录的 threadid 是线程 A,则直接执行。 因此,偏置锁的执行效率非常高。
偏置锁吊销过程
同步完成后不会释放偏置锁(即线程不会主动释放偏置锁),偏置锁的释放时机:只有当其他线程争夺偏置锁时,持有偏置锁的线程才会被撤销,偏置锁才会被释放。 吊销需要等待全局安全点(即,在该时间点没有执行字节码)。
当到达全局安全点时,根据当前持有偏置锁的线程是否已完成同步,有两种吊销方案**
1) 线程 A 正在执行同步**(即线程 A 尚未执行同步**) 如果线程 B 此时抢占了锁,则偏置锁被撤销,锁升级为轻量级锁(此时,轻量级锁由最初持有偏置锁并继续执行其同步的线程 A 持有**) 争用锁的线程 B 将进入旋转并等待获取轻量级锁。
2) 线程 A 已完成同步**(即线程 A 已退出同步**) 如果线程 B 此时抢占了锁,则偏置锁将被撤销,threadid 将留空,偏置锁将被标记为 0。根据线程 A 是否再次争用锁,分为以下两种情况:
如果线程 A 不再继续竞争,则锁将重新偏置到线程 B(即,线程 B 保持偏置锁)。
如果线程 A 继续竞争,则锁将升级为轻量级锁,通过 CAS 旋转抢占锁。
轻量级锁旨在减少使用操作系统的互斥锁导致的性能消耗,而无需多线程竞争。 因此,轻量级锁旨在提高几乎交替执行同步时的性能。
轻量级锁升级流程
当线程 A 和线程 B 同时抢占锁对象时,将撤销偏置锁,并将锁提升为轻量级锁,如下所示:
在线程 A 执行同步之前,会在 JVM 程序的堆栈帧中创建一个空间来存储锁定记录。 当线程 A 抢占锁定对象时,通过 CAS 旋转将锁定对象对象头中的标记字复制到线程 A 的锁定记录中,线程堆栈中指向锁记录的标记字中的指针指向线程 A 的锁定记录空间
如果更新成功,线程 A 将持有对象锁,并将对象锁的标记字的锁定标志更新为 00(即线程 A 持有轻量级锁并执行同步**),线程 B 通过等待 CAS 旋转来获取轻量级锁。
如果更新失败,则锁将被线程 B 抢占。
锁记录是特定于线程的数据结构,每个线程都有一个可用锁记录列表,以及一个可用锁的全局列表。 每个锁定对象的标记字将与一条锁记录相关联(即对象头标记字中的锁字指向锁定记录的起始地址),并且锁记录中有一个所有者字段,用于存储拥有锁的线程的唯一标识符, 表示锁被线程占用。
锁记录的内部结构,如图
如果线程无法通过 CAS 旋转获取锁,则存在锁争用,轻量级锁将膨胀为重量级锁。
轻量级锁吊销过程
轻量级锁吊销有两种方案:
当两个以上的线程同时争用一个锁时,轻量级锁被撤销并升级为重量级锁,而不是通过 CAS 自等待获取锁,而是直接阻塞线程。
当持有轻量级锁的线程完成同步**时,轻量级锁将被释放,并且通过 CAS 旋转将指向锁定对象标记字中锁定记录的指针替换回锁定对象的标记字。
当多个线程同时争用同一个锁时,JVM 会将轻量级锁升级为重量级锁。 重量级锁通过监视器在内部实现,而监视器又通过底层操作系统的互斥锁实现。
当锁升级为重量级锁时,将标记工作中的锁识别位更新为 10,并将标记工作中的指针指向显示器。
重量级锁工作流:当系统检查锁是否为重量级锁时,会阻塞等待获取锁的线程,阻塞的线程不消耗 CPU 资源当线程被阻塞或唤醒时,需要将 CPU 从用户模式转换为内核状态,状态转换需要消耗 CPU 资源,因此重量级锁的开销比较大。
偏置锁、轻质锁、重量级锁的优缺点:
斜锁、轻锁、重量级锁
偏置锁:对于单线程情况,当没有锁争用时,可以在进入同步时使用偏置锁**。
轻量级锁:适用于不太激烈的竞争和较短的同步**执行时间,升级为轻量级锁,当有竞争时,轻量级锁使用旋转锁,虽然使用轻量级锁会占用CPU资源,但比使用重量级锁效率更高。
重量级锁:适用于竞争激烈且同步**执行时间较长的情况,此时使用轻量级锁旋转的性能成本会比重量级锁更严重,需要将轻量级锁升级为重量级锁。
jdk1.6. 除了引入偏置锁、轻量级锁等同步锁优化外,还引入了自适应自旋锁、锁消除、锁粗化等锁优化策略。
自旋锁是一种基于繁忙等待的锁,当一个线程尝试获取锁时,如果该锁已被另一个线程占用,则该线程会在循环中等待,直到获得锁。 旋转锁适用于短时间保持锁的情况。
JDK1 中的旋转锁4.2,默认关闭,可以在 JDK1 中使用 -xx:+usespinning 打开默认开启 6,自旋锁的自旋次数默认为 10 次,可以通过参数 -xx:preblockspin 进行调整。
自适应旋转锁是一种改进的旋转锁,可根据锁的使用情况动态调整旋转次数:
如果锁在过去旋转中被另一个线程持有,则当前线程将增加旋转次数。
如果在过去的旋转中,锁很少能够成功旋转,那么当前的线程会减少旋转次数甚至忽略旋转过程,以免浪费 CPU 资源。
JVM在JIT编译时扫描运行上下文,经过转义分析后,对某个段不存在竞争或共享的可能,消除该段的锁定,提高程序运行效率。
例:
public void method()在上面的**中,锁在方法中是私有的、不可变的,根本不需要添加锁,JVM 会进行锁消除。
通常,同步块的作用域应尽可能小,并且只在共享数据的范围内进行同步,以尽量减少同步操作的次数,减少阻塞时间。 同时,增加解锁需要资源消耗,如果在**中出现一系列连续的解锁操作,可能会因为频繁的解锁操作而造成不必要的性能损失。
例:
public void method(object lock) synchronized (lock) }在上述情况下,两个锁定的块可以完全合并为一个,减少了频繁解锁带来的性能开销,提高了程序的运行效率。
wait-notification 机制是线程之间的一种协作方式,通过 wait()、notify() 和 notifyall() 方法实现,这些方法必须在 synchronized** 块或 synchronized 方法中,否则会抛出 illegalmonitorstateexception。 哪里:
wait() 方法:阻塞当前线程并释放锁,当前线程进入等待状态。
notify() 方法:执行 ** 块后释放锁,唤醒等待队列中的线程(随机唤醒等待队列中的线程)。
notifyAll() 方法:在 ** 块执行后释放锁,唤醒等待队列中的所有线程。
等待通知机制执行该过程,如图所示
处理过程: 1)线程执行同步**块,当不满足条件时,调用wait()方法释放当前线程持有的锁,将当前线程添加到等待队列中待挂起。
2)其他线程执行完同步块后,当满足条件时,调用notifyall()或notify()唤醒等待队列中的阻塞线程,并将线程放入entrylist(阻塞队列)中重新争抢锁。
例:
** 作者 Nanqiu * 同步等待通知机制示例 * @slf4jpublic类分配器 catch(exception e) }alsadd(from); als.add(to);返回资源 * param from **param to target * public synchronized void free(object from, object to)}注意:
notify() 随机通知等待队列中的一个线程,这可能会导致某些线程永远不会收到通知。
notifyAll() 通知等待队列中的所有线程。
因此,除非是故意的,否则请尝试使用 notifyAll()。
阅读推荐]更多精彩内容,如:
Redis 系列。
数据结构和算法。
NACOS系列。
MySQL系列。
JVM 系列。
卡夫卡系列。
并发编程系列。
请移至【南秋】个人主页参考。 内容不断更新。
关于作者]热爱科技、热爱生活的老宝贝,专注J**A领域,关注【南秋同学】带你一起学习、共同成长