解密Java中神奇的Synchronized关键字

简介: 解密Java中神奇的Synchronized关键字


在Java中,当多个线程同时访问同一块代码,会产生竞态条件,可能会导致数据不一致或其他问题。为了解决这个问题,Java提供了synchronized关键字,它能够保证同一时刻被synchronized修饰的代码最多只有1个线程执行。本文将从synchronized的定义、JDK6以前的实现方式、偏向锁和轻量级锁、锁优化、synchronized关键字的用法和注意事项等方面详细讲解。

🎉 定义

在Java中,synchronized关键字是一种同步锁,在多线程编程中,用于解决多个线程同时访问同一个资源的问题。当一个线程持有锁时,其他线程将会被阻塞,直到当前线程释放锁为止。synchronized可以加在方法上或对象上,作用的对象是非静态的,则取得的锁是对象锁;作用的对象是静态方法或类,则取到的锁是类锁,这个类所有的对象用的是同一把锁。

🎉 JDK6以前

在JDK6以前,synchronized加锁是通过对象内部的监视器锁来实现的,监视器锁本质上又是依赖于底层的操作系统的Mutex Lock来实现的,因此在高并发情况下,synchronized的性能就会变得非常低下。

当多个线程争夺同一个锁时,会发生线程阻塞和唤醒的操作,这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要比较长的时间,导致程序执行效率低下。

🎉 偏向锁和轻量级锁

为了提高synchronized的效率,JDK6引入了偏向锁和轻量级锁。

📝 偏向锁

在Java中,同步操作需要获取锁来保证多个线程访问共享资源的安全性。而在锁的获取过程中,JVM添加了一种优化方式——偏向锁。当一个线程访问同步块时,如果这个同步块没有被其他线程占用,那么JVM会把这个同步块的锁标记为偏向锁,并把当前线程ID记录在MarkWord中。这样,下次同一线程访问同步块时,就不需要再次获取锁,直接进入同步块即可。

但是,当有其他线程来竞争锁的时候,偏向锁需要进行撤销,转而使用轻量级锁或者重量级锁来保证同步操作的安全性。这里给出一个对象从无锁到偏向锁转化的过程:

第一步,检测MarkWord是否为可偏向状态,即锁标识位是01,如果是,则说明当前对象可被偏向,可以执行同步代码块。

第二步,如果需要访问同步块的线程ID与MarkWord中记录的线程ID相同,则不需要进行竞争,直接执行同步代码块即可。

第三步,如果需要访问同步块的线程ID与MarkWord中记录的线程ID不同,说明该对象已经被其他线程占用,需要进行竞争。此时,JVM会进行CAS操作,如果操作成功,则将线程ID替换为当前线程ID,并执行同步代码块。

第四步,如果CAS操作失败,则需要进行偏向锁的撤销。这个过程可以有两种情况触发:一是对象头的Epoch字段计数到一定次数,二是多个线程尝试竞争该对象的锁,都失败了。

第五步,完成偏向锁的撤销后,持有偏向锁的线程不会被挂起,继续执行同步代码块。如果获取锁失败,则视情况进行自旋或者进行阻塞等待,进一步升级为轻量级锁或重量级锁。

需要注意的是,在偏向锁撤销的过程中,需要清除那些曾经持有该偏向锁对象的线程的锁记录。这是因为在偏向锁状态下,持有锁的线程会在对象头中记录一个标记位和持有该锁的线程ID。而在撤销偏向锁的过程中,需要清除这些锁记录,因为它们已经不再持有该锁,以便其他线程可以重新争夺锁的所有权。并且,偏向锁撤销的过程不一定会挂起所有持有偏向锁的线程,只有在线程竞争锁时才会挂起线程。

在偏向锁撤销过程中,JVM会启动偏向锁撤销线程来遍历所有持有该偏向锁对象的线程栈,清除它们的锁记录。而在多线程编程中,当多个线程对一个内存位置进行读取和修改时,可能会出现一种情况——ABA问题。为了解决这个问题,JVM 在对象的内存布局中添加了一个Epoch字段,来判断一个线程是否因为ABA问题导致的线程变化。这个Epoch字段并不直接关系到偏向锁撤销的过程,但是有助于判断锁的状态是否发生了变化。

在实际的应用中,偏向锁的优化方式能够显著提高同步代码块的性能,但它并不适用于所有场景。在多线程应用程序中,如果存在大量的锁竞争,那么偏向锁的优化效果会下降,甚至被轻量级锁或重量级锁取代。因此,在使用偏向锁的时候,需要根据具体情况进行考虑和使用。同时,了解偏向锁的撤销过程,有助于我们更好地理解同步机制的底层实现,更好地进行多线程编程。

📝 轻量级锁

轻量级锁升级过程是为了实现对象的互斥访问,首先在当前线程的栈帧中创建一个锁记录用于存储锁对象的MarkWord的拷贝。该拷贝无锁状态对象头中的MarkWord,用于在申请对象锁时作为CAS的比较条件。同时也能通过这个比较判定是否在持有锁的过程中,这个锁被其他线程申请过,如果有,在释放锁的时候要唤醒被挂起的线程。轻量级锁的MarkWord如果存有hashCode,解锁后也需要恢复。拷贝成功后,虚拟机使用CAS操作把对象中对象头的MarkWord替换为指向锁记录的指针,再把锁记录空间里的owner指针指向加锁的对象。如果这个更新操作成功,那么当前线程就拥有了该对象的锁,并且对象MarkWord的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果更新操作失败,虚拟机会检查对象的MarkWord中的Lock Word是否指向当前线程的栈帧。如果是,则当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行。如果不是,则说明多个线程竞争锁,要进入自旋。若自旋结束时依然未获得锁,轻量级锁就要升级为重量级锁,锁标志的状态值变为“10”,对象MarkWord中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后续等待锁的线程也要进入阻塞状态。

当锁升级为轻量级锁后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数(默认10次)依然没有拿到,锁就会升级成重量级锁。一般来说,同步代码块内的代码应该很快就执行结束,这时线程B自旋一段时间是很容易拿到锁的。但如果不巧,没能拿到锁,自旋就会成为死循环,并且耗费CPU。因此,虚拟机会直接将锁升级为重量级锁,不再进行自旋。这样就不需要了线程一直自旋,性能会得到很大的提升。

📝 自旋锁

自旋锁并不是一种锁状态,而是一种智能的线程同步策略。它可以用于保护临界区的并发访问,避免多个线程同时进入临界区导致的数据不一致问题。自旋锁的核心思想是,在等待锁的过程中,不会主动阻塞线程,而是继续执行当前线程内的代码,同时不断自旋等待锁的释放,以减少线程的阻塞和唤醒,提高并发性能。

当一个线程尝试获取某个锁时,如果发现该锁已经被其他线程占用,则该线程不会被立即挂起或者睡眠,而是开始自旋等待锁的释放。自旋等待期间,该线程会一直占用CPU处理器资源,循环检测锁是否被释放。自旋等待不能替代阻塞,因为如果自旋时间过长,会占用过多CPU资源,反而降低性能。

自旋锁适用于维护临界区很小的情况。临界区很小表示锁占用的时间很短,如果持有锁的线程很快就能释放锁,那么自旋的效率就会非常高。但是自旋的次数必须要有一个限度,如果自旋超过了定义的次数仍然没有获取到锁,就应该被挂起。然而这个限度不能固定,因为程序锁的状况是不可预估的,所以JDK1.6引入了自适应的自旋锁,可以根据程序运行的情况动态调整自旋的次数。比如如果线程自旋成功了,那么下次自旋的次数会更多,反之则会更少,从而避免了自旋等待过程中浪费处理器资源的情况。

要开启自旋锁,可以使用JDK1.6之后提供的参数–XX:+UseSpinning,如果需要修改自旋次数,可以使用–XX:PreBlockSpin参数来指定,其中默认值为10次。使用自旋锁可以在一定程度上提高多线程程序的性能,但也需要注意合理设置自旋次数和使用范围,以免造成过多的CPU资源占用和线程的饥饿等问题。

📝 重量级锁

重量级锁是Java中的一种锁,是通过对象内部的监视器锁(Monitor)来实现的。监视器锁本质上又是依赖于底层的操作系统的MutexLock来实现的。由于操作系统实现线程之间的切换需要从用户态转换到核心态,状态之间的转换需要比较长的时间,因此依赖于操作系统MutexLock所实现的锁我们称之为“重量级锁”。

当一个线程在等待锁时,会不停的进行自旋,其中自旋的线程数达到CPU核数一半之后,就会升级为重量级锁。在升级为重量级锁之后,锁标志会被置为10,同时将MarkWord中的指针指向重量级的Monitor,这将阻塞所有没有获取到锁的线程。

重量级锁的加锁-等待-撤销流程分为三个步骤。

🔥 1. 加锁

当一个线程请求锁时,首先会查看锁标志是否为0,如果为0,则表示锁没有被占用,此时该线程会将锁标志置为1,并且获取到锁。如果锁标志不为0,则表示锁已经被占用,此时该线程会进入自旋状态。

🔥 2. 等待

在自旋状态下,如果锁一直没有被释放,那么自旋的线程数量会不断增加。当自旋的线程数量达到CPU核数的1/2时,就会升级为重量级锁。在升级为重量级锁之后,会将锁标志置为10,同时将MarkWord中的指针指向重量级的Monitor,这将阻塞所有没有获取到锁的线程。

🔥 3. 撤销

当一个线程释放锁时,会将锁标志置为0。此时正在等待的线程会被唤醒并争夺锁,曾经获得过锁的线程,在被唤醒之后会优先得到锁。如果一个线程在等待锁的过程中调用了wait()方法,则该线程会被加入到等待队列中,并通过wait_set等待被唤醒。如果一个线程在等待锁的过程中调用了notify()方法,则该线程会将等待队列中的第一个线程唤醒,等待队列中被唤醒的线程会被加入到同步队列中,并通过park()方法等待获取锁。

当重量级锁撤销之后,系统会将其转变为无锁状态。撤销锁之后会清除创建的Monitor对象,并修改markword,这个过程需要一段时间。Monitor对象是通过GC来清除的。当GC清除掉Monitor对象之后,锁就被撤销为无锁状态。

重量级锁适用于多线程互斥访问同一资源的情况。由于重量级锁性能较低,因此只在必要时才应该使用。在Java中,Synchronized关键字就是一种重量级锁。在使用Synchronized关键字时,应尽量减少锁的持有时间,这样可以提高程序的并发性能。

🎉 锁优化

针对于synchronized的性能问题和在某些情况下可能导致死锁的情况,Java提供了以下的锁优化:

📝 锁消除

在编译器层面消除不必要的加锁操作,将锁的范围缩小到最小,这样可以减少锁竞争的概率,提高程序的执行效率。

📝 锁粗化

在进行一系列操作时,将多个连续的加锁操作放在一个代码块中,这样可以减少加锁和解锁的开销,提高程序的执行效率。

📝 自适应自旋

当线程尝试获取同步锁失败后,它并不会立即进入阻塞状态,而是再次尝试获取同步锁,如果一段时间内失败的次数越多,就会进入阻塞状态。这个时间段就是自旋时间,是由操作系统动态调整的。

🎉 synchronized关键字的用法和注意事项

synchronized关键字的用法有以下几种:

📝 修饰方法

这种方式是修饰整个方法,即使方法中没有同步代码块,也会锁定这个方法,这种方式适用于整个方法需要同步的情况。

public synchronized void method() {
    // 同步代码块
}
📝 修饰代码块

这种方式是将同步代码块包在synchronized括号内,只有在执行到synchronized代码块时才会锁定,这种方式适用于只需要同步执行部分代码的情况。

public void method() {
    synchronized (this) {
        // 同步代码块
    }
}
📝 修饰静态方法

和修饰方法类似,这种方式是锁定整个静态方法,适用于整个静态方法需要同步的情况。

public synchronized static void method() {
    // 同步代码块
}
📝 修饰类

这种方式是锁定整个类,即使不同实例中的线程也会被锁定,适用于整个类需要同步的情况。

public void method() {
    synchronized (ClassName.class) {
        // 同步代码块
    }
}

需要注意的是,只有在多个线程访问同一块资源时,才需要使用synchronized关键字。如果同步块内的代码很少,那么锁的代价就会超过同步块内的代码的执行代价,从而导致程序执行效率变低。同时,在使用synchronized关键字时,需要考虑死锁问题,即多个线程无限制地等待对方释放锁的情况。因此,在编写代码时,需要特别注意同步块的范围和锁的粒度。


相关文章
|
3月前
|
Java 开发者 C++
Java多线程同步大揭秘:synchronized与Lock的终极对决!
Java多线程同步大揭秘:synchronized与Lock的终极对决!
80 5
|
3月前
|
存储 Oracle 安全
揭秘Java并发核心:深入Hotspot源码腹地,彻底剖析Synchronized关键字的锁机制与实现奥秘!
【8月更文挑战第4天】在Java并发世界里,`Synchronized`如同导航明灯,确保多线程环境下的代码安全执行。它通过修饰方法或代码块实现独占访问。在Hotspot JVM中,`Synchronized`依靠对象监视器(Object Monitor)机制实现,利用对象头的Mark Word管理锁状态。
53 1
|
3月前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
Java并发编程实战:使用synchronized关键字实现线程安全
59 0
|
16天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
1月前
|
算法 Java 程序员
Java中的Synchronized,你了解多少?
Java中的Synchronized,你了解多少?
|
1月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
19 1
|
1月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
24 2
|
1月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
19 0
|
2月前
|
存储 安全 Java
Java并发编程之深入理解Synchronized关键字
在Java的并发编程领域,synchronized关键字扮演着守护者的角色。它确保了多个线程访问共享资源时的同步性和安全性。本文将通过浅显易懂的语言和实例,带你一步步了解synchronized的神秘面纱,从基本使用到底层原理,再到它的优化技巧,让你在编写高效安全的多线程代码时更加得心应手。
|
2月前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。