知识点:synchronized 原理分析_一只阿木木-编程思维

synchronized 原理分析

1. synchronized 介绍

在并发程序中,这个关键字可能是出现频率最高的一个字段,他可以避免多线程中的安全问题,对代码进行同步。同步的方式其实就是隐式的加锁,加锁过程是有 jvm 帮我们完成的,再生成的字节码中会有体现,如果反编译带有不可消除的 synchronized 关键字的代码块的 class 文件我们会发现有两个特殊的指令 monitorenter 和 monitorexit ,这两个就是进入管程和退出管程。

为什么说不可消除的 synchronized ,这是由于在编译时期会进行锁优化,比如说在 StringBuffer 中是加了锁的,也就是锁对象就是他自己,然而我们编译以后会发现根本没有上面的两条指令就是因为,锁消除技术。

Synchronized 使用的一般场景,在对象方法和类方法上使用,以及自定义同步代码块。但是在方法上使用 Synchronized 关键字和使用同步代码块是不一样的,方法上采用同步是采用的字节码中的标志位 ACC_SYNCHRONIZED 来进行同步的。而同步代码块则是采用了对象头中的锁指针指向一个监视器(锁),来完成同步。

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor ,获取成功之后才能执行方法体,方法执行完后再释放 monitor 。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

2. 对象头和锁

一个对象在内存中分为三部分:对象头、实例数据、对齐填充。

对象头中主要存放了 GC 分代年龄、偏向锁、偏向 id、锁类型、hash 值等。jvm 一般会用两个字来存放对象头,(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成。MarkWord里默认数据是存储对象的HashCode等信息,但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式

实例数据就包括对象字段的值,不仅有自己的值还有继承自父类的字段的值。一般字段的顺序是同类型的字段放在一起,空间比较大的字段放在前面。在满足上面的规则下父类的放在子类的前面。

对其填充并非必要的,整个对象需要是 8 字节的整数倍,当不足的时候会进行填充以达到 8 字节整数倍,主要还是为了方便存取。

这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(在 Synchronized 代码块中的监视器 )的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下。

ObjectMonitor() {
    _count        = 0; //记录个数
    _owner        = NULL; // 运行的线程
    //两个队列
    _WaitSet      = NULL; //调用 wait 方法会被加入到_WaitSet
   _EntryList    = NULL ; //锁竞争失败,会被加入到该列表
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

3. Synchronized 代码块原理

反编译下面的代码得到的字节码如下:

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("hello");
        }
    }

    public synchronized void test(){

    }
}

 

当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor ,重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。

值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。所以看到上面有两条 monitorexit !

4. Synchronized 方法原理

先看一个反编译的实例方法的结果,确实比普通的方法多了一个标志字段。方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor , 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。

如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

5. 偏向锁

偏向锁是 Java 为了提高程序的性能而设计的一个比较优雅的加锁方式。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做获取锁的过程。如果有其他线程竞争锁的时候就需要膨胀为轻量级锁。这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

偏向锁获取的过程如下,当锁对象第一次被线程获取的时候,虚拟机把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中的偏向线程ID,并将是否偏向锁的状态位置置为1。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,直接检查ThreadId是否和自身线程Id一致, 如果一致,则认为当前线程已经获取了锁,虚拟机就可以不再进行任何同步操作(例如Locking、Unlocking及对Mark Word的Update等)。

其实一般来说偏向锁很少又说去主动释放的,因为只有在其他线程需要获取锁的时候,也就是这个锁不仅仅被一个线程使用,可能有两个线程交替使用,根据对象是否被锁定来决定释放锁(恢复到未锁定状态)还是升级到轻量锁状态。

6.轻量级锁

轻量级锁,一般指的是在有两个线程在交替使用锁的时候由于没有同时抢锁属于一种比较和谐的状态,就可以使用轻量级锁。他的基本思想是,当线程要获取锁时把锁对象的 Mark Word 复制一份到当前线程的栈顶,然后执行一个 CAS 操作把锁对象的 Mark Word 更新为指向栈顶的副本的指针,如果成功则当前线程拥有了锁。可以进行同步代码块的执行,而失败则有两种可能,要么是当前线程已经拥有了锁对象的指针,这时可以继续执行。要么是被其他线程抢占了锁对象,这时候说明了在同一时间有两个线程同时需要竞争锁,那么就打破了这种和谐的局面需要膨胀到重量级锁,锁对象的标志修改,获取线程的锁等待。

在轻量级锁释放的过程就采用 CAS 把栈上的赋值的 Mark Word 替换到锁对象上,如果失败说明有其他线程执抢占过锁,锁对象的 Mark Word 的标志被修改过,在释放的同时唤醒等待的线程。

 

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/yizhiamumu/p/9107053.html

源码:java集合源码之:数组与链表(一)_一只阿木木-编程思维

  数组和链表是数据结构中最基本的部分。 数组 在java中,数组定义为一种基本类型,其可以通过下标获取到对应位置的数据。那么这种结构的数据,在内存中是怎么存放的呢? 数组在内存中是一段连续的存储单元,每个数据依次放在每个单元中。 分析这种结构,我们可以得出以下几个结论: 创建一个数组,必须声明其长度,以在内存

源码:java集合源码之:哈希表(二)_一只阿木木-编程思维

要想知道一个元素是否在数组或链表中,只能从前向后挨个对比,无论是数组还是链表,其对数据的查询表现都比较无力。在的二叉排序树中,还会将数据排序以进行二分查找,将时间复杂度从O(n)降低到O(lg n)。 出现这个问题的根源在于,我们没有办法直接根据一个元素找到它存储的位置。 那有没有办法消除这个对比的过程呢?哈希表就是解

知识点:java 内存模型完全解密_一只阿木木-编程思维

Java虚拟机(JVM) 规范中定义了一种Java的内存模型,即Java Memoory Model(简称JMM),用来实现让Java程序在各个平台下都能达到一致的内存访问效果。 JVM是整个虚拟机,JMM模型属于JVM的一部分。JDK1.5后Java内存模型才逐渐的成熟和完善起来。 主内存与工作内存 Java内存模型

多线程初探:三种方式创建多线程详细示例_一只阿木木-编程思维

多线程即在同一时间,可以做多件事情。 创建多线程有3种方式,分别是继承线程类,实现Runnable接口,匿名类 一:线程概念 首先要理解进程(Processor)和线程(Thread)的区别 进程:启动一个LOL.exe就叫一个进程。 接着又启动一个DOTA.exe,这叫两个进程。 线程:线程是在进程内部同时做的事情

总结:java 集合进阶精讲2-arraylist_一只阿木木-编程思维

知识点:Java 集合框架图 总结:Java 集合进阶精讲1 总结:Java 集合进阶精讲2-ArrayList   初探: ArrayList底层结构是数组,是List接口的 可变数组的实现,所以会占用一块连续的内存空间。 每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小,可以动态

分布式系列:什么是分布式系统1_一只阿木木-编程思维

  什么是分布式系统: 原来有一个20多万行代码的系统,现在拆分成20个小系统,每个小系统1万多行代码。原本代码之间都是直接基于spring调用,现在拆开来了,20个小系统部署在不同的机器上,得基于分布式服务框架(比如dubbo)搞一个rpc调用,接口与接口之间通过网络通信来请求和响应。 分布式系统可以大概分成两类。

结合源码分析 bubble 使用注意事项_一只阿木木-编程思维

使用dubbo时候要尽量了解源码,不然会很容易入坑。   一、服务消费端ReferenceConfig需要自行缓存 ReferenceConfig实例是个很重的实例,每个ReferenceConfig实例里面都维护了与服务注册中心的一个长链,并且维护了与所有服务提供者的的长链。假设有一个服务注册中心和N个服务提供者,那

分布式事务实战方案汇总_一只阿木木-编程思维

分布式事务: 1 CAP 定理 1.1 概念 CAP 理论在分布式系统中 一致性:分布式环境下多个节点的数据是否强一致 可用性:分布式服务能一直保证可用状态。当用户发出一个请求后,服务能在有限时间内返回结果 分区容忍性:特指对网络分区的容忍性 对于共享数据系统,最多只能同时拥有CAP其中的两个,没法三者兼顾。