深入理解Java中的Synchronized关键字

简介: 深入理解Java中的Synchronized关键字


📝 定义

Synchronized能保证同一时刻被Synchronized修饰的代码最多只有1个线程执行。

synchronized如果加在方法上/对象上,那么,它作用的对象是非静态的,它取得的锁是对象锁;

synchronized如果作用的对象是一个静态方法或一个类,它取到的锁是类锁,这个类所有的对象用的是同一把锁。

📝 JDK6以前

Synchronized加锁是通过对象内部的监视器锁来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的,操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要比较长的时间。

Sun程序员发现大部分程序大多数时间都不会发生多个线程同时访问竞态资源的情况,大多数对象的加锁和解锁都是在特定的线程中完成,出现线程竞争锁的情况概率比较低,比例非常高,所以引入了偏向锁和轻量级锁。

🔥 对象从无锁到偏向锁转化的过程

第一步,检测MarkWord是否为可偏向状态,是偏向锁是1,锁标识位是01。

第二步,如果是可偏向状态,测试线程ID是不是当前线程ID。如果是,就直接执行同步代码块。

第三步,如果测试线程ID不是当前线程ID,且当前对象的MarkWord标识为可偏向状态,这说明该对象已经被其他线程获取过锁,不能偏向于当前线程,就需要通过CAS操作竞争锁,竞争成功,就把MarkWord的线程ID替换为当前线程ID,同时执行同步代码块。

第四步,假设线程B来CAS失败了,需要将当前对象的偏向锁撤销,然后再进行锁竞争,这个时候JVM后台线程会启动偏向锁撤销过程。

偏向锁撤销触发通常有二种情况:当在对象头的Epoch字段计数到一定次数时会触发偏向锁撤销或者有多个线程尝试竞争该对象的锁,都失败了,也会触发偏向锁的撤销。

  • 当有多个线程尝试获取同一个对象的锁时,通常只有一个线程能够获取成功,而其他线程则需要进入等待状态。当这个对象曾经被偏向锁优化过,且当前正在尝试获取锁的线程不是偏向线程时,如果该线程获取锁成功,JVM会撤销该对象的偏向锁状态,并重新获取锁。这个过程不需要等待其他线程都失败,只要有一个线程成功获取锁即可触发偏向锁的撤销。
  • 在Java对象的内存布局中,有一个叫做对象头的部分,其中的Epoch字段是为了解决ABA问题而设置的。与此同时,当一个对象从偏向锁升级为轻量级锁或重量级锁时,对象的标记字(MarkWord)中保存的线程ID可能会发生变化。为了能够判断一个线程是否是因为ABA问题导致的线程变化,会将Epoch字段的值加一来表示变化次数。因此,在线程之间竞争锁时,可以通过比较Epoch字段的值是否相同来判断锁是否发生了变化。如果发生了变化,说明对象的状态已经被其他线程修改,需要重新竞争锁。与此同时,这个Epoch字段并没有直接关系到偏向锁撤销的过程。
  • ABA问题指的是在多线程编程中,当多个线程对一个内存位置进行读取和修改时,可能会出现一种情况:最初的值被读取,然后被修改为新的值A,再次被修改为原来的值,也就是回到了最初的值B,但此时中间可能有其他线程对这个内存位置进行了修改,所以此时相对于最初的值B,这个内存位置已经发生了变化,无论是A还是其他值都与最初的B不同。在这种情况下,如果仅仅判断最终的值是否等于B,可能出现问题,需要使用其他机制来判断这个内存位置是否发生过变化。
  • 偏向锁撤销不一定会挂起所有持有偏向锁的线程,只有在线程竞争锁时才会挂起线程。如果没有其他线程竞争锁,偏向锁撤销的过程只会涉及到当前对象的MarkWord,不会影响到其他线程。

撤销偏向锁的过程会挂起所有持有偏向锁的线程(例如线程A),并清除它们的MarkWord中的偏向锁信息(标记位、偏向线程ID、Epoch次数)。然后遍历偏向锁的对象所在的线程的栈,查找锁对象的锁记录,将那些已经被访问过的记录清除,以表明这些线程都不再持有该锁。

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

第五步,完成偏向锁撤销后,持有偏向锁的线程不会被挂起,而是会继续执行同步代码块。线程A尝试获取该锁,如果获取成功,就可以继续执行同步代码块了,当线程A尝试获取该锁失败时,视情况继续进行自旋或者进行阻塞等待,导致进一步升级为轻量级锁或重量级锁。

安全点是jvm为了保证在垃圾回收的过程中引用关系不会发生变化,设置的安全状态,在这个状态上会暂停所有线程工作。一般有循环的末尾,方法临返回前,调用方法的call指令后,可能抛异常的位置,这些位置都可以算是安全点。

🔥 轻量级锁升级

轻量级锁升级过程是,在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的MarkWord的拷贝,拷贝无锁状态对象头中的MarkWord复制到锁记录中。

  • 这么做是因为在申请对象锁时,需要以该值作为CAS的比较条件。
  • 同时在升级到重量级锁的时候,能通过这个比较,判定是否在持有锁的过程中,这个锁被其他线程申请过,如果被其他线程申请了,在释放锁的时候要唤醒被挂起的线程。
  • 无锁的markword中可能存有hashCode,锁撤销之后必须恢复,这个markword要用于锁撤销后的还原。如果轻量级锁解锁为无锁状态,直接将拷贝的markword CAS修改到锁对象的markword里面就可以了。

拷贝成功后,虚拟机将使用CAS操作把对象中对象头MarkWord替换为指向锁记录的指针,然后把锁记录空间里的owner指针指向加锁的对象。

这个过程的目的是为了实现轻量级锁的互斥访问。CAS操作的作用是将对象头MarkWord指针指向锁记录空间,从而表示当前线程持有这个对象的锁。锁记录空间里的owner指针指向加锁的对象,是为了在释放锁的时候,能够知道哪个对象需要进行通知,从而唤醒被挂起等待锁的线程。同时,轻量级锁的升级过程也可以通过锁记录空间来判断是否在持有锁的过程中,这个锁被其他线程申请过,如果被其他线程申请了,在释放锁的时候要唤醒被挂起的线程,从而保证多个线程对同一个对象的访问是互斥的。

如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象MarkWord的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。

如果这个更新操作失败了,虚拟机首先会检查对象MarkWord中的Lock Word是否指向当前线程的栈帧。

Lock Word是一个对象头的一部分,用于实现Java对象的锁定。当一个线程获取了对象的锁时,Lock Word就会被设置为指向该线程的栈帧,表示这个对象被该线程持有了锁。其他线程如果要获取该对象的锁,会检查Lock Word中的锁标志是否为0,如果为0则表示该对象没有被锁定,可以获取锁。如果锁标志为1,则表示对象已经被锁定,其他线程必须等待锁的释放。

如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。如果不是说明多个线程竞争锁,进入自旋,若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,MarkWord中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。

当锁升级为轻量级锁之后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数(默认10次)依然没有拿到,锁就会升级成重量级锁。一般来说,同步代码块内的代码应该很快就执行结束,这时候线程B自旋一段时间是很容易拿到锁的,但是如果不巧,没拿到,自旋其实就是死循环,很耗CPU的,因此就直接转成重量级锁咯,这样就不用了线程一直自旋了。

🔥 自旋锁

自旋锁不是一种锁状态,而是一种策略。线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。

引入自旋锁,当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。

自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好。

自旋的次数必须要有一个限度,如果自旋超过了定义的限度仍然没有获取到锁,就应该被挂起。但是这个限度不能固定,程序锁的状况是不可预估的,所以JDK1.6引入自适应的自旋锁,线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少,甚至省略掉自旋过程,以免浪费处理器资源。

通过–XX:+UseSpinning参数来开启自旋(JDK1.6之前默认关闭自旋)。

通过–XX:PreBlockSpin修改自旋次数,默认值是10次。

🔥 重量级锁

当一个线程在等锁时会不停的自旋(底层就是一个while循环),当自旋的线程达到CPU核数的1/2时,就会升级为重量级锁。

将锁标志为置为10,将MarkWord中指针指向重量级的monitor,阻塞所有没有获取到锁的线程。

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

重量级锁的加锁-等待-撤销流程:

曾经获得过锁的线程,被唤醒后,优先得到锁。

举个例子,假设有A,B,C三个线程依次进入synchronized区,并且A已经膨胀成重量级锁。如果有一个线程 a 先进入 synchronized , 但是调用了 wait释放锁,这是线程 b 进入了 synchronized,b还在synchronized中执行,c线程又进来了。此时 a 在 wait_set ,b 不在任何队列,c 在 cxq_list ,假如 b 调用 notify唤醒线程,会把 a 插到 c 前面,也就是 b 退出synchronized的时候,会唤醒 a,a退出之后再唤醒 c。

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


🔔如果您需要转载或者搬运这篇文章的话,非常欢迎您私信我哦~

相关文章
|
2天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
2天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2天前
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
2032 3
|
3天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
14 3
|
1月前
|
JavaScript 前端开发 Java
java中的this关键字
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。自学前端2年半,正向全栈进发。若我的文章对你有帮助,欢迎关注,持续更新中!🎉🎉🎉
51 9
|
1月前
|
设计模式 JavaScript 前端开发
java中的static关键字
欢迎来到瑞雨溪的博客,博主是一名热爱JavaScript和Vue的大一学生,致力于全栈开发。如果你从我的文章中受益,欢迎关注我,将持续分享更多优质内容。你的支持是我前进的动力!🎉🎉🎉
51 8
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
47 4
|
2月前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
29 3
|
2月前
|
存储 安全 Java
了解final关键字在Java并发编程领域的作用吗?
在Java并发编程中,`final`关键字不仅用于修饰变量、方法和类,还在多线程环境中确保对象状态的可见性和不变性。本文深入探讨了`final`关键字的作用,特别是其在final域重排序规则中的应用,以及如何防止对象的“部分创建”问题,确保线程安全。通过具体示例,文章详细解析了final域的写入和读取操作的重排序规则,以及这些规则在不同处理器上的实现差异。
了解final关键字在Java并发编程领域的作用吗?
|
2月前
|
Java 程序员 编译器