AQS 与 Sync 源码分析

ReentrantReadWriteLock 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

   虽然前面几篇文章,已经分析过很多次 AQS 但是有时候分析分析着就会陷入一种调用链分不清楚的情况。为了更好的理解 AQS 中的锁机制,专门用一篇文章分析 AQS 结构,理清调用关系!

Read More

ReentrantReadWriteLock 源码分析

ReentrantReadWriteLock 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. 概述

   这个类听名字好像是和 ReentrantLock 差不多,但是实际上他们两没有任何关系,他并没有直接或间接的继承 ReentrantLock。ReentrantLock 属于独占锁,也就是我们前面所说的在临界区之内只能有一个线程运行。比如说我们的 Hashtable 采用的就是这种方式,哪怕在 get 元素的时候都对表加了锁,其他线程希望读取都没办法,但事实上我们知道多个线程同时读不会引起安全问题。至于什么时候会出现安全问题,这里介绍一个操作系统中常提到的 Bernstein条件 ,概括的来说就是两个线程对同一个资源不能同时进行如下操作: 读写写读写写 。所以我们对数据进行并发访问是不会有问题的,于是诞生了 读锁 和 写锁的概念,在 Java 中提供的 ReentrantReadWriteLock 就是一个具体实现。

Read More

Semaphore 源码分析

Semaphore 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. Semephore 简单介绍

   一般来说,在 Java 中比较常用的同步工具就是 Lock 和 Synchronized 但是 Java 也加入了很多新的机制比如这里提到的信号量。他其实给人的感觉就是操作系统中的信号量,如果一个线程要运行需要获取一个资源进行一次 P 操作,在这里就是调用 acquire 方法,然后运行结束后调用 V 操作,释放这个资源以供其他线程使用,这里的 release 方法。那么如果资源都被其他线程抢光了,那么这个线程只能处于等待状态。也就是 P 操作返回的 -1 。

   上面我们所说的信号量是多值信号量,主要用于进程之间的同步。还有一种称之为二值信号量,也就是操作系统中常提到的 mutex 。对,此时他就是锁!因为做了一个 P 操作后,只有进行 V 操作其他线程才能进入临界区。此时和 Lock 作用一样了。

Read More

ReentrantLock 与 AQS 源码分析

ReentrantLock 与 AQS 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. 基本结构

   重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized更加灵活。ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,但是加锁多少次,就必须解锁多少次,这样才可以成功释放锁。

Read More

synchronized 原理分析

Synchronized 原理分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !
2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. synchronized 介绍

   在并发程序中,这个关键字可能是出现频率最高的一个字段,他可以避免多线程中的安全问题,对代码进行同步。同步的方式其实就是隐式的加锁,加锁过程是有 jvm 帮我们完成的,再生成的字节码中会有体现,如果反编译带有不可消除的 synchronized 关键字的代码块的 class 文件我们会发现有两个特殊的指令 monitorentermonitorexit ,这两个就是进入管程和退出管程。为什么说不可消除的 synchronized ,这是由于在编译时期会进行锁优化,比如说在 StringBuffer 中是加了锁的,也就是锁对象就是他自己,然而我们编译以后会发现根本没有上面的两条指令就是因为,锁消除技术。

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

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

2. 对象头和锁

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

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

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

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

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

1
2
3
4
5
6
7
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 代码块原理

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

1
2
3
4
5
6
7
8
9
10
11
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 的标志被修改过,在释放的同时唤醒等待的线程。

ConcurrentHashMap 源码分析

ConcurrentHashMap 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. 前言

   终于到这个类了,其实在前面很过很多次这个类,因为这个类代码量比较大,并且涉及到并发的问题,还有一点就是这个代码有些真的晦涩,不好懂。前前后后大概花了三天的时间看完的一些重要操作,接着今天来整理一下。

Read More

HashSet 源码分析

HashSet 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. 基本结构

1. 继承

这个类简直适合 HashMap 如出一辙,他们继承的类都极其相似。继承的是 AbstractSet

2. 实现

实现了 Set 接口,之后还有两个普遍的接口 Cloneable, java.io.Serializable 。

3. 主要字段

   这个字段非常的少,只有一个 HashMap 和一个常量。估计你已经猜到这个容器的底层是采用HashMap 实现的了,但是具体又是怎么实现的呢?

Read More

LinkedHashSet 源码分析

LinkedHashSet 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. 基本结构

   如果你看了 HashSet 现在基本一分钟就能弄明白 LinkedHashSet 的底层原理。里面没有任何字段,只有一个序列化 id,这个我们不说。然后他继承的是 HashSet ,有没有注意到简直就和 LinkedHashMap 一个套路,然后就调用一下父类的构造方法,但是调用的父类的构造方法都是同一个,很明显肯定是那个比较特殊的构造,也就是只能在包内访问,并且比其他的方法多一个 bool 参数的那个,因为它能指定底层的数据结构是 LinkedHashMap ,这也就解释了为什么 LinkedHashSet 中的四个方法都是 public 的,而最后一个是 default

Read More

LinkedHashMap 源码分析

LinkedHashMap 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1. 基本结构

1. 实现

实现的接口是 Map

2. 继承

   继承的是 HashMap 这个就比较熟悉了,事实上我们会看到 LinkedHashMap 代码量非常的少,主要就是因为他继承的 HashMap ,继承了大多数的操作。 仔细一点的都会发现 HashMap 里面有非常多的空白方法,这些方法其实是模板方法,为了让继承 HashMap 的类重写一些自己的特性。而不破坏代码结构。

Read More

HashMap 源码分析

HashMap 源码分析

1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !

2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!

1.结构

1. 继承

  该类继承自 AbstractMap 这个类似于 ArrayList

2. 实现

具体如下:

  1. 首先这个类是一个 Map 自然有 Map 接口
  2. 然后就是两个集合框架肯定会实现的两个接口 Cloneable, Serializable 。

    Read More