第一次写博客,也就是记录一些自己对于JAVA的一些理解,不足之处,请大家指出,一起探讨。
这篇博文我打算说一下JAVA中锁,也就是Lock()的部分源码,这里我拿了一个Lock的具体实现类ReentrantLock来举例,但其实其他几个实现类大同小异。
附上一张流程图,来源我忘记,比较抱歉啊。
首先声明一下ReentrantLock类中的结构
其中有一个Sync静态内部类,该类继承自AbstractQuenedSynchrorizer
在AbstractQuenedSynchrorizer中,实现了大部分关于lock的操作,一般只留下tryAcquire()尝试获取锁,tryRelease()尝试释放锁,延迟到子类来完成,可以提高扩展性
而Sync也有两个子类,分别为NonfairSync与FairSync
那么从lock()入手。
lock()
先调了ReentrantLock中的lock()方法
public void lock() { sync.lock(); }
上面说过了,默认情况下调用lock()方法时调用的是非公平锁,也就是NonfairLock()类中的lock()
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
这段代码比较好理解,重点放在else部分内,
首先compareAndSetState(0, 1)其实是一个CAS自旋,锁若是未被持有,默认是状态是0,持有后改为1,该方法内部调用的是unsafe的一个自旋,原理就是compareAndSetState(old, new)
若是old值等于期望值,那么将其设置为new值,试想,第一个现成进入if部分,显然可以成功获得锁,并且设置锁的状态为1,那么后面的现成进入后,若是第一个现成不释放锁,之后的现成调用compareAndSetState(0, 1)时,因为old是0,而期望值是1,不相符,所以不会获得该锁。
接下来就是else
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
这是AbstractQuenedSynchrorizer中的一个函数,由于尝试获取锁
tryAcquire(arg)显然是调用了NonfairLock类中的tryAcquire()函数,之前也提到了AbstractQuenedSynchrorizer将tryAcquire()尝试获取锁,tryRelease()尝试释放锁,延迟到子类来完成
那么看一看NonfairLock类中tryAcquire()的代码
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(next); return true; } return false; }
getState()获取该锁的状态,初始值为0
也就是说若c==0,则表示该锁未被占用,那么使用compareAndSetState将其设置为1,同时将当前现成置标志位锁的拥有者。
这里其实很好的体现了什么叫非公平锁,试想,当一个现成尝试获取锁时失败看,进入else部分,else内部又让其尝试获取锁,假设之前占有锁的现成在此时释放了锁,那么也就会导致当前线程可以成功的获取到锁,注意,是在第一次获取锁失败之后的一次尝试获取,然后居然就获取成功了,也就是无视了等待队列中的现成,变成了后来者居上的局面。当然也不能说这种非公平方式的获取锁不好,恰恰是这样,大大提高了吞吐量。
那么接下来,若是c!=0呢,进入else部分,判断的条件是当前现成是否是锁的拥有者现成,如果是的话,只是简单的做了个状态+1而已。
若是以上两者情况都不属于,那么返回false,说明该现成当前来看确实无法获取到锁,准备将其插入到等待队列中。
在!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个条件的前半部分已经处理完了,返回若是true,则当前线程获得了锁,否则,没有获得锁
进入后半个判断acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这里可以看到其内部调了其他一个函数addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
这里需要说明一个问题,就是AbstractQuenedSynchrorizer在内部自己维护了一个双向链表,放的是未获得锁的等待线程
在看这段代码,将当前线程包装成一个Node节点。
获取到该链表的尾节点tail,若尾节点不为null,做一个尾节点与当前新节点的链接,同时compareAndSetTail(pred, node)将tail更新为新加入的节点
若是尾节点为null,调用enq()
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
其大意为若尾节点为null,认为当前链表为空,那么构造一个头结点之后将新节点加入该链表
addWaiter()的核心目的就是将线程包装成节点后加入链表尾部
好了,最后调用if内部执行的函数selfInterrupt();
static void selfInterrupt() { Thread.currentThread().interrupt(); }
中断当前线程,至此一个完整的lock()走完。
接下去就是unlock()
相比之下unlock()比较好理解
public void unlock() { sync.release(1); }
调用了AbstractQuenedSynchrorizer内的
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
类似的,看看tryRelease(arg)做了什么
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
这里继续说明一下,前面说过AbstractQuenedSynchrorizer将tryAcquire()尝试获取锁,tryRelease()尝试释放锁,延迟到子类来完成,这也是一个体现
这里比较好理解,更新状态,若c==0,setExclusiveOwnerThread(null);设置当前锁未被线程锁拥有,同时设置状态为,若是c不为0,依次释放,知道其为0,然后将该锁的拥有者置为null
返回去看release,获取等待队列的头节点,h != null && h.waitStatus != 0这个条件判断的是头结点是否是一个有效节点,若是调用unparkSuccessor(h);
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
这段代码的意思在于找出第一个可以unpark的线程,一般说来head.next == head,Head就是第一个线程,但Head.next可能被取消或被置为null,因此比较稳妥的办法是从后往前找第一个可用线程。
总结一下,一般来说,在等待队列中的头结点并不是持有锁的节点,而是理解成即将持有锁的节点,因为当锁被释放之后,若是没有被不公平锁的抢占方式抢走了锁,他们头结点是具有获取锁资格的第一人选,若是头结点成功获取到锁,那么他会从链表中脱离,链表更新头结点。
在这里阻塞线程使用的park,同样是unsafe调用了本地方法park()
反之,唤醒线程使用的是unpark(),调用过程同park()
AbstractQuenedSynchrorizer做为一个同步器,是Lock具体实现类的基本功能提供类,像ReentrantLock只是做了该类的一个代理,以及将tryAquire()与tryRelease()的延迟实现。
在每个具体实现部分比如获取锁,释放锁等操作,都调用的CAS自旋操作。
这个我小小的说一下我对于这里为何要使用自旋的原因,首先Lock我们知道是一种轻量级的锁的实现,那么基于这种方式,若是我们想Synchorized方式那样,直接阻塞其余线程,等到有资源的时候再将其唤醒。
一个线程的调度是比较耗费CPU资源的尤其是我们在JVM内部还会实现一些类似于等待队列,运行队列,就绪队列这样的数据结构是,一个线程的切换,不仅仅是将其信息置入到内存,还需要将其在各个队列之间相互转换,就绪队列->运行队列等等。这种情况下,若是我们知道同步操作可以在非常短的时间内完成,那还有比较这样做频繁的线程切换么。
我们大可以将A线程保持其占有处理机的专状态,也就是让其一致在循环运行,循环体可以是空,也可以是一些无意义的指令,等到有资源时直接进入他的工作状态。虽然看起来占着处理机不放不是很好,但是从某种程度上来说,这样会比频繁的切换线程所造成的内存消耗来的更能让人接受。
当然这之间必然有一种平衡,究竟让线程空转多少时间比较合适呢,时间长了明显不合适,短了,又会造成白转的现像。所以这个我个人认为还是主要看运用的场合,若是同步操作很快能完成,那可以用CAS,否则的话,就看如何取舍了。
恩,那这篇差不多写到这里,有不足的地方欢迎大家提出一起研究。
相关推荐
Java集合框架源码分析 Java集合框架是Java语言中一个非常重要的组件,提供了多种数据结构和算法来存储和操作数据。在Java集合框架中,LinkedList、ArrayList、HashMap、TreeMap等都是非常常用的数据结构。本文将对...
学习这些内容时,可以通过阅读源码、编写并发示例、分析并发问题来加深理解。`Java-concurrency-master`中的资料可能包括教程、代码示例、最佳实践等内容,对提升并发编程技能非常有益。如果你深入研究这些资源,将...
通过分析和学习这些源码,我们可以更深入地理解Java并发库的实际运用,并提升我们的并发编程技能。 总的来说,Java并发库是构建高性能、高并发应用的核心,熟练掌握其原理和实践,对于任何Java开发者来说都是不可或...
在分析Java源码时,我们通常关注的是Java标准库中的核心类与方法,因为这些组件是Java编程语言的基础。...在实际开发过程中,Java源码分析可以加深开发者对Java语言的理解,帮助解决编程中遇到的瓶颈。
集合源码分析 Java架构师--成神之路 修改记录 版本 编写时间 作者 描述 v1.0.0 2019-10-29 Rock.Sang 梳理大纲 v1.0.1 2019-11-15 Rock.Sang 完善所有目录结构 v1.0.2 2020-01-07 Rock.Sang 添加英语模块 v1.0.3 ...
Java集合框架是Java编程语言中非常重要的组成部分,它为Java开发者提供了大量用于存储数据的结构。Java集合框架主要包括两大类,单列集合和双列集合。单列集合中,Set接口的集合主要用于存储不重复的元素,而List...
在Java编程语言中,多线程是核心特性之一,它允许程序同时执行多个任务,从而提高了应用程序的效率和响应速度。这个"基于Java的源码-超简单...如果你已经下载了这个压缩包,可以通过分析源码进一步学习和实践这些概念。
通过分析这些源代码,我们可以深入理解Java编程语言的各个方面。 在Java编程中,有几个核心知识点是必不可少的: 1. **基础语法**:Java是一种面向对象的语言,它的基础包括变量、数据类型、运算符、控制结构(如...
Java源码集合是一个珍贵的学习资源,它包含了从大一开始积累的31个不同Java项目,适合于课程设计、毕业设计以及个人技能提升。这个压缩包是开发者或学习者的一个宝库,提供了各种类型的代码示例,有助于深入理解Java...
【Java源码工具-core-java】是一个专注于Java编程语言的开源项目,主要包含了Java相关的源代码示例和实用工具集合。这个项目旨在帮助开发者深入理解Java语言的内部机制,提升编程技巧,并提供了一些实用的工具类,...
04-Java并发线程池底层原理详解与源码分析-monkey 05-并发编程之深入理解Java线程-fox 06-并发编程之CAS&Atomic原子操作详解-fox 07-并发锁机制之深入理解synchronized(一)-fox 08-并发锁机制之深入理解...
Java readwritereentrantlock读写锁源码分析
Java源码分析是软件开发过程中一个重要的学习环节,它能帮助开发者深入理解代码背后的逻辑,提升编程技巧,以及优化程序性能。在这个过程中,我们通常会关注类的设计、算法的应用、数据结构的选择,以及如何利用Java...
深入理解Java API的源码对于提升编程技能、优化代码性能以及解决复杂问题至关重要。本篇文章将对Java API的部分关键组件进行源码解读,帮助读者深入理解其工作原理。 1. **对象创建与内存管理**: - `Object`类:...
它通过分析运行时的线程和锁状态,帮助开发者识别出可能导致死锁的潜在问题。下面我们将深入探讨JCarder的工作原理、使用方法以及如何利用它来优化多线程程序。 **1. JCarder的工作原理** JCarder利用了Java虚拟机...
集合源码分析 高并发与多线程 Stargazers over time 线程 线程的创建和启动 线程的sleep、yield、join 线程的状态 代码在 部分。 synchronized关键字(悲观锁) synchronized(Object) 不能用String常量、Integer、Long...
1. 客户端:负责用户界面展示,用户交互,通常由Java Swing或JavaFX实现,接收用户的输入并发送给服务器,同时显示服务器返回的消息。 2. 服务器端:处理客户端的连接请求,管理用户会话,存储和分发消息。Java的...
本项目“Java-Concurrent-Programming”是一个基于《Java并发编程艺术》这本书的学习总结,通过源码分析来深入理解并发编程的核心概念。 在Java中,线程是并发的基础,`Thread`类提供了创建和管理线程的基本功能。...
本文将深入探讨Java中的多线程编程核心技术,基于提供的标题和描述,我们将围绕Java线程源码进行详细分析。 首先,我们要理解Java中的线程是如何创建和运行的。Java提供两种方式来创建线程:继承Thread类和实现...
Java 读写锁是Java并发编程中的一种重要机制,它为多线程环境下的数据访问提供了更为精细的控制。在Java的`java.util....通过阅读和分析`readwritelock`这个示例,开发者可以更好地掌握如何在Java中使用读写锁。