自顶向下彻底理解 Java 中的 Synchronized

阅读本文至少要知道 synchronized 用来是干什么的... 需要的前置知识还有 Java 对象头和 Java 字节码的部分知识。

synchronized 的使用

synchronized 有三种使用方式,三种方式锁住的对象是不相同的。

锁分为实例对象锁class 对象锁类对象锁,注意这三种锁是不一样的。

  • 修饰实例方法,此时锁住的是对象,锁分为实例对象锁
  • 修饰静态方法,此时锁住的是类对象锁
  • 修饰代码段,此时锁住的是括号中的对象(synchronized(this)),可以是实例对象锁或者 class 对象锁(synchronized(Object.class)

此时出现了锁住类和锁住对象,要注意这两个锁是不同的,在一个线程拿到类的锁时,另外一个线程是可以拿到对象的锁的。

synchronized 底层语义实现

代码同步块和方法级别的synchronized使用在JVM 层实现是不一样的。当然如果从 JVM 到 CPU 层面是采用 Lock 指令实现的。

每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

当多个线程同时请求某个对象监视器时,新请求锁的线程将首先被加入到ConetentionList中。对象监视器会设置几种状态用来区分请求的线程:

Contention List:所有请求锁的线程将被首先放置到该竞争队列

Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List

Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set

OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck

Owner:获得锁的线程称为Owner

!Owner:释放锁的线程

synchrionized 字节码层在同步块的入口插入 monitorenter,在同步块出口插入monitorexit

方法级别的同步是通过在方法的 flag 表示上设置 ACC_SYNCHRONIZED 来实现的。具体可以查看《深入理解 JVM 虚拟机》 中字节码一章。

对 synchronized 的优化

JDK 1.6 实现了对锁的大量优化。可以分为两种,一种是减少对 synchronized 的使用,一种是在特殊条件下使用更轻量级的锁来代替 synchronized。

减少对锁的使用

锁消除

当编译器检测到一些被加上 synchronized 的代码不存在竞争的时候(通过逃逸分析,感兴趣可以去看一下《深入理解 Java 虚拟机》),就会被视为线程私有的,锁会被安全的消除掉。

锁粗化

当编译器发现 synchronized 被加入在循环当中,不断的加锁解锁会有极大的效率问题。不要认为你不会写出这么傻的代码,JDK 中有许多方法是同步的,比如 HashTable 中的一些方法。

for (int i = 0; i < 100; i++) {
    synchronized (this) {
        //do something
    }
}

编译器会自动把它优化成

synchronized (this) {
    for (int i = 0; i < 100; i++) {
        //do something
    }
}

来减少锁的获取和释放。

自旋锁与自适应锁

有相当多一段代码在代码同步块中只运行一小会儿,如果为了等待这一会儿去挂起和恢复线程,切换线程带来的开销不是很值得,在引入了自旋锁后,当遇到锁被别的线程占用的时候,这个线程就进入一段忙循环,这就是自旋。

但是如果多次忙循环后仍然获取不到锁,那么只能挂起线程将锁升级为重量级锁了。

自适应锁会记录之前在代码同步快的运行时间来决定是否要执行自旋以及自旋的时间,如果之前自旋成功过,那么这次也很有可能会自旋成功。如果之前自旋失败,那么就省略掉自旋过程直接挂起线程避免浪费 CPU 资源。

通过轻量级锁来代替 synchronized

轻量级锁设计出来是想要在竞争较少的情况下减少 synchronized 的性能消耗,而不是用来代替 synchronized 的。想要看懂轻量级锁的使用需要对 Java 对象头有一定的了解。关于 Java 对象头可以参考。好,接下来我就默认认为你懂 Mark Word 是什么了。

锁的膨胀过程是 偏向锁→轻量级锁→重量级锁,膨胀过程的单方向的。不能缩小回来。

下面是 Mark Word 的内容和锁的关系。

存储内容 标志位 状态
对象哈希码,对象分代年龄 01 未锁定
指向记录锁指针 00 轻量级锁定
指向重量级锁指针 10 膨胀(重量级锁定)
11 GC 标记
偏向线程 id,时间戳,分代年龄 01 可偏向

偏向锁

偏向锁的思想就是:锁经常被同一个线程重复获取,那么可以通过设置偏向锁来避免使用重量级锁。因为如果这段时间只有这一个线程在重复获取这个对象的锁,那么对这部分代码的同步就是无意义的。

当线程获取锁的时候发现 Mark Word 是未锁定的状态,那么就采用 CAS 把这个 Mark Word 设置成偏向状态,把这个线程的 id 设置进去,然后如果这个线程再次获取这个锁的时候发现这个偏向锁的 id 和当前线程的 id 一样则不需要同步直接运行。

当有另外一个线程尝试获取这个偏向锁的时候,锁会恢复到未锁定或者轻量级锁的状态。

  • 如果对象未被锁定,则会变成未锁定的,不可偏向的对象
  • 如果对象被锁定了,则会变成轻量级锁状态

如果大多数锁总是被多个不同的线程访问,那么偏向模式就是多余的,可以 采用 --XX:UseBiaseLocking 来禁止偏向锁来提高性能。

轻量级锁

当线程进入一个代码同步块的时候,虚拟机将使用 CAS 将 Mark Word 更新为指向 Lock Record 的指针。如果成功则线程拥有这个对象锁,mark word 将被设为 00。

如果更新失败,则检查该线程是否持有这个对象锁,如果已经持有则直接向下执行

如果没有持有这个对象锁则轻量级锁膨胀为重量级锁,锁标志状态变为 10。

参考文献

原文地址:https://www.cnblogs.com/stonymoon/p/9818739.html

时间: 2025-01-07 02:52:37

自顶向下彻底理解 Java 中的 Synchronized的相关文章

理解java中的volatile关键字

Java语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量.这两种机制的提出都是为了 实现代码线程的安全性.Java 语言中的 volatile 变量可以被看作是一种 "程度较轻的 synchronized":与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分. volatile 写和读的内存语义: 线程 A 写一个 volatile 变量,实质上是线程 A

巨人大哥谈Java中的Synchronized关键字用法

巨人大哥谈Java中的Synchronized关键字用法 认识synchronized 对于写多线程程序的人来说,经常碰到的就是并发问题,对于容易出现并发问题的地方价格synchronized基本上就搞定 了,如果说不考虑性能问题的话,这一操绝对能应对百分之九十以上的情况,若对于性能方面有要求的话就需要额外的知识比如读写锁等等.本文目的先了解透彻synchronized的基本原理. Synchronized的基本使用 Synchronized的作用主要有三个: (1)确保线程互斥的访问同步代码 

读深入理解Java中的String(包括JVM)一文总结和提升

读深入理解Java中的String(包括JVM)一文总结和提升 摘要:String作为Java语言中的字符串模拟类,无论是实际的编程工作还是笔试面试过程,都需要我们都String类非常熟悉,对于String类的大部分字符串操作方法,都必须达到熟练运用的程度才行.但是,笔试和面试过程中,面试官往往喜欢问一些String特性相关的题目,来考察面试者对于String基础知识的掌握是否牢固.(本人尚未研读深入理解JVM这本书,分析JVM都是查看网上资料来分析的,若在接下来的内容有分析不到位的地方请见谅和

深刻理解Java中形参与实参,引用与对象的关系

声明:本博客为原创博客,未经允许,不得转载!原文链接为http://blog.csdn.net/bettarwang/article/details/30989755 我们都知道,在Java中,除了基本数据类型之外,其他的都是引用类型,当它们作为函数参数时,传递的也是引用,通过引用可以改变对象的值,很多人便因此而忽略形参与实参,引用与对象的关系问题.废话不多说,先看下面一个例子: import java.util.*; public class Student { private String

深入理解Java中的IO

深入理解Java中的IO 引言:     对程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项艰难的任务 < Thinking in Java >   本文的目录视图如下: Java IO概要 a.Java IO中常用的类 b.Java流类的类结构图 1.流的概念和作用 2.Java IO所采用的模型  : 3.IO流的分类 4.Java IO流对象 1.输入字节流InputStream 2.输出字节流OutputStream 3.字符输入流Reader 4.字符输出流Write

深刻理解Java中final的作用(一):从final的作用剖析String被设计成不可变类的深层原因

声明:本博客为原创博客,未经同意,不得转载!小伙伴们假设是在别的地方看到的话,建议还是来csdn上看吧(原文链接为http://blog.csdn.net/bettarwang/article/details/26744661),看代码和提问.讨论都更方便. Java中final的作用主要表如今三方面:修饰变量.修饰方法和修饰类.以下就从这两个方面来解说final的作用.在文末从final及类的设计安全性出发,论述了Java中String为何要被设计成不可变类. 1.final修饰变量 fina

线程的生命周期 - 理解Java中线程的状态

刚刚开始学cocos2-x,仅仅是按照教程把已经安了一般Android的开发环境的eclipse重新升级到安装好cdt和ndk就花了我几十小时,差点都要放弃了. 参考博客 D:\cocos2d-x\cocos2d-x-2.2.3\cocos2dx\platform\third_party\android\prebuilt 说说大概的过程: 下载ndk插件,ndk包,cdt插件.最开始我按照书上的下载了cocos2d-x 2.0.1,希望跟书上统一起来,这样以后学习的时候可以参考书上的也不会遇到太

理解Java中的弱引用(Weak Reference)

理解Java中的弱引用(Weak Reference) 本篇文章尝试从What.Why.How这三个角度来探索Java中的弱引用,理解Java中弱引用的定义.基本使用场景和使用方法.由于个人水平有限,叙述中难免存在不准确或是不清晰的地方,希望大家可以指出,谢谢大家:) 1. What——什么是弱引用? Java中的弱引用具体指的是java.lang.ref.WeakReference<T>类,我们首先来看一下官方文档对它做的说明: 弱引用对象的存在不会阻止它所指向的对象变被垃圾回收器回收.弱引

深入理解 Java中的 流 (Stream)

首先,流是什么? 流是个抽象的概念,是对输入输出设备的抽象,Java程序中,对于数据的输入/输出操作都是以"流"的方式进行.设备可以是文件,网络,内存等. 流具有方向性,至于是输入流还是输出流则是一个相对的概念,一般以程序为参考,如果数据的流向是程序至设备,我们成为输出流,反之我们称为输入流. 可以将流想象成一个"水流管道",水流就在这管道中形成了,自然就出现了方向的概念. 当程序需要从某个数据源读入数据的时候,就会开启一个输入流,数据源可以是文件.内存或网络等等.