JAVA源码分析------锁(1)
http://870604904.iteye.com/blog/2258604
第一次写博客,也就是记录一些自己对于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源码分析------锁(1)的更多相关文章
- Java源码分析 | CharSequence
		
本文基于 OracleJDK 11, HotSpot 虚拟机. CharSequence 定义 CharSequence 是 java.lang 包下的一个接口,是 char 值的可读序列, 即其本身 ...
 - Java源码分析:关于 HashMap 1.8 的重大更新(转载)
		
http://blog.csdn.net/carson_ho/article/details/79373134 前言 HashMap 在 Java 和 Android 开发中非常常见 而HashMap ...
 - JAVA源码分析-HashMap源码分析(二)
		
本文继续分析HashMap的源码.本文的重点是resize()方法和HashMap中其他的一些方法,希望各位提出宝贵的意见. 话不多说,咱们上源码. final Node<K,V>[] r ...
 - Java源码分析之LinkedList
		
LinkedList与ArrayList正好相对,同样是List的实现类,都有增删改查等方法,但是实现方法跟后者有很大的区别. 先归纳一下LinkedList包含的API 1.构造函数: ①Linke ...
 - Java源码分析:Guava之不可变集合ImmutableMap的源码分析
		
一.案例场景 遇到过这样的场景,在定义一个static修饰的Map时,使用了大量的put()方法赋值,就类似这样-- public static final Map<String,String& ...
 - JAVA源码分析-HashMap源码分析(一)
		
一直以来,HashMap就是Java面试过程中的常客,不管是刚毕业的,还是工作了好多年的同学,在Java面试过程中,经常会被问到HashMap相关的一些问题,而且每次面试都被问到一些自己平时没有注意的 ...
 - 【转】【java源码分析】Map中的hash算法分析
		
全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.Conc ...
 - 【Java源码分析】LinkedList类
		
LinkedList<E> 源码解读 继承AbstractSequentialList<E> 实现List<E>, Deque<E>, Cloneabl ...
 - 一致性哈希Java源码分析
		
首次接触一致性哈希是在学习memcached的时候,为了解决分布式服务器的负载均衡或者说选路的问题,一致性哈希算法不仅能够使memcached服务器被选中的概率(数据分布)更加均匀,而且使得服务器的增 ...
 
随机推荐
- 更改VS2010 工程名的方法
			
哇~~~~~~~啦啦啦~~~~~~~~ 太开心了,通过不断的尝试,我终于知道怎么更改VS2010的工程名了!!! 下面分享给大家: 1.打开自己想要更改名字的工程,用ctrl+h在整个项目中把想更改的 ...
 - IP反查网站,ip反查接口,旁站查询接口大全,通过IP查域名汇总:
			
http://cn.bing.com/search?q=ip%3A220.181.111.85 http://dns.aizhan.com/?q=www.baidu.com http: ...
 - TP如何进行批量查询
			
public function getUserInfo($uid){ if(is_null($uid) || empty($uid)){return false;} if(is_arr ...
 - LeetCode HashTable 30 Substring with Concatenation of All Words
			
You are given a string, s, and a list of words, words, that are all of the same length. Find all sta ...
 - Android学习笔记(24):进度条组件ProgressBar及其子类
			
ProgressBar作为进度条组件使用,它还派生了SeekBar(拖动条)和RatingBar(星级评分条). ProgressBar支持的XML属性: Attribute Name Related ...
 - ALLEGRO修改位号回注ORCAD原理图
			
资料:ALLEGRO修改位号回注ORCAD原理图 https://wenku.baidu.com/view/54e221e114791711cd7917e2.html?re=view
 - 模拟IE各种版本的方法
			
下载360极速浏览器.开启“兼容模式” 默认会是IE7.可以通过控制台(Ctrl + shift + I)调整各个版本
 - PLSQL怎样导出oracle表结构
			
tools->export tables 是导出表结构还有数据 tools->export user objects是导出表结构 可以用tools->export tables ...
 - linux uart驱动——uart原理(一)
			
UART(Universal Asynchronous Receiver and Transmitter)通用异步收发器(异步串行通信口),是一种通用的数据通信协议,它包括了RS232.RS499.R ...
 - iOS打包(ipa包)
			
1.打开XCode打开project文件.选择Product,再点击Archive. 2.鼠标右键点击Shoe In Finder 3.鼠标右键选择"显示包内容" 4.鼠标左键双击 ...