锁机制学习笔记


目录:

CAS的意义

锁的一些基本原理

ReentrantLock的相关代码结构

两个重要的状态

  I.AQS的state(int类型,32位)

  II.Node的waitStatus

获取锁(AQS)的流程

  I.获取锁总操作

  II.tryAcquire(尝试获取锁)

  III.添加到等待队列

  IIII.自旋请求锁

  IIIII.释放锁


JUC的并发包功能强大,但也不容易理解,大神果然是用来膜拜的。经过一段时间的研究和理解,我把自己所了解的关于JUC中锁的相关知识整理下来,一方面给自己做个备忘,另一方面也给各位朋友做个参考。

文中源码的关键部分都做了注释,希望对大家有所帮助。另外这只是学习笔记,建议大家先去了解一些基础知识再来看其中的源码,大家有疑问的可以再参考其他文章,谢谢!

CAS的意义

CAS只是尝试性操作,可能一个线程在对比的时候,另一个线程已更改了状态,所以CAS操作可能失败。

for (;;){
     if (CAS(obj,expect,update)){
          do other business
     }
}
CAS(obj,expect,update) 必有一个期望对象expect,一个更新对象update,expect在多线程情况下同一时间只会有一个线程能匹配,且整个CAS方法中,other business都不是共享变量,因为他们对并发无影响。
 
CAS经常放在循环中,在多线程情况下,就是哪个线程先匹配到expect就执行,其他线程可在下次循环中再匹配到。

锁的一些基本原理

锁其实是个独占资源,其中的state代表的就是独占资源,获取锁就是线程对state数值的增加,释放锁就是state减少的过程
1.加锁的意义在于多线程获取同一个锁,这样每个线程就会按照获取锁的顺序执行。
2.在线程内创建的对象,是每个线程独立的,因为对它的操作无需加锁,而对共享变量的操作,就必须加锁或者CAS,如果CAS失败,则代表此次操作尝试失败,需考虑后续操作
3.尽量在线程外的其他类对共享变量进行锁定(即尽量实现线程安全的类),而不要把锁带到线程内去操作锁定,因为这样会增加代码复杂性

ReentrantLock的相关代码结构

两个重要的状态

I.AQS的state(int类型,32位)

用来描述有多少线程获持有锁。
独占锁的时代这个值通常是0或者1
对于可重入锁,一个线程可多次进入,每次进入state+1
共享锁的时代就是持有锁的数量。
tryAcquire()和tryRelease()其实就是尝试获取状态位state的修改权限并设置独占Thread
 

II.Node的waitStatus

对队列中节点的操作(锁定线程或释放线程)则是基于节点的waitStatus的
CANCELLED = 1:
节点操作因为超时或者对应的线程被interrupt。节点不应该不留在此状态,一旦达到此状态将从CHL队列中踢出。
SIGNAL =
-1:
节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。

CONDITION = -2:
表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
正常状态 =
0:
新生的非CONDITION节点都是此状态。

对于处在阻塞队列中的节点,当前节点之前的节点:
waitStatus >
0的是取消的节点,在处理中应该剔除
waitStatus = 0的,则需要将其改成-1

因此整个阻塞节点链的waitStatus应该为:-1,-1,-1,0

获取锁(AQS)的流程

锁的获取和释放都是基于上述2个状态来的,首先能不能获取锁是由AQS.state来控制,因此tryAcquire()和tryRelease()都是对state的控制,如果不能获取锁则需要加入到等待队列,此时线程的等待与释放则是由Node的waitStatus控制的。

下图演示了一个线程获取独占锁的过程:

I.获取锁总操作

 Java Code 
1
2
3
4
 
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
 
整个过程可分为以下四个步骤(只有tryAcquire是在Sync,其他3个都是在AQS中):
1.
tryAcquire(arg):
     如果tryAcquire(arg)成功,那就没有问题,已经拿到锁,整个lock()过程就结束了。如果失败进行操作2。
2.
addWaiter(Node.EXCLUSIVE):
     创建一个独占节点(Node)并且此节点加入CHL队列末尾。进行操作3。
3.
acquireQueued(addWaiter(Node.EXCLUSIVE), arg):
     自旋尝试获取锁,失败根据前一个节点来决定是否挂起(park()),直到成功获取到锁。进行操作4。
4.
selfInterrupt():
     如果当前线程已经中断过,那么就中断当前线程(清除中断位)。

II.tryAcquire(尝试获取锁)

 Java Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
非公平锁与公平锁在tryAcquire()方法上唯一区别就是比公平锁少了
isFirst(current),它的作用就是判断AQS是否为空或者当前线程是否在队列头

III.添加到等待队列

AQS的节点结构:
上图的head,tail,prev,next这几个属性构造了一条节点链
 
 Java Code 将节点加入到队列中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 这一段是为提高性能而设的,没有也不影响功能
    // 如果tail不为空,则设置新tail并返回
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node); // 如果tail为空,则执行enq(),创建新head,插入队列,否则逻辑和上面一样
    return node;
}
 
enq(Node)去队列操作实现了CHL队列的算法,如果为空就创建头结点,然后同时比较节点尾部是否是改变来决定CAS操作是否成功,当且仅当成功后才将尾部节点的下一个节点指向为新节点。可以看到这里仍然是CAS操作。
 Java
Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // tail == null,创建新的节点加入到尾部
            Node h = new Node(); // dummy
header,傀儡节点
            h.next = node;
            node.prev = h;
            if (compareAndSetHead(h)) { //
CAS设置头部
                tail = node;
                return h;
            }
        } else {
// tail !=
null,则和addWaiter()中那段一样
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
 

IIII.自旋请求锁

如果可能的话挂起线程,直到得到锁,返回当前线程是否中断过(如果park()过并且中断过的话有一个interrupted中断位)。
  

 Java
Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 
);
        pred.next = node;
    } else {
        // waitStatus=0,修改前一个节点状态位为SINGAL,表示后面有节点等待你处理,需要根据它的等待状态来决定是否该park()
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // ws<0才需要park(),ws>=0都返回false,表示线程不应该park()
    return false;
}
 

IIIII.释放锁

release()设置state=state-1,如果state=0,则无其他线程持有锁,可unpark节点链的head节点的后续线程(因为head节点是在节点链的傀儡节点),否则不做操作。
 
同时unparkSuccessor()会先把前置节点的waitStatus设为0,然后再unpark线程
因为state=0,则acquireQueued()的tryAcquire()能成功,即此线程能获取到锁退出
 
注意:
unpark是按照节点链的顺序一次unpark一个线程
 

 Java
Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 
)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

原创文章,请注明引用来源:CM4J

参考文章:http://www.blogjava.net/xylz/archive/2010/07/05/325274.html

JUC.Lock(锁机制)学习笔记[附详细源码解析]的更多相关文章

  1. JUC.Condition学习笔记[附详细源码解析]

    目录 Condition的概念 大体实现流程 I.初始化状态 II.await()操作 III.signal()操作 3个主要方法 Condition的数据结构 线程何时阻塞和释放 await()方法 ...

  2. Laravel学习笔记之Session源码解析(上)

    说明:本文主要通过学习Laravel的session源码学习Laravel是如何设计session的,将自己的学习心得分享出来,希望对别人有所帮助.Laravel在web middleware中定义了 ...

  3. Laravel学习笔记之Session源码解析(下)

    说明:在中篇中学习了session的CRUD增删改查操作,本篇主要学习关闭session的相关源码.实际上,在Laravel5.3中关闭session主要包括两个过程:保存当前URL到session介 ...

  4. Laravel学习笔记之Session源码解析(中)

    说明:在上篇中学习了session的启动过程,主要分为两步,一是session的实例化,即\Illuminate\Session\Store的实例化:二是从session存储介质redis中读取id ...

  5. memcached学习笔记——存储命令源码分析上篇

    原创文章,转载请标明,谢谢. 上一篇分析过memcached的连接模型,了解memcached是如何高效处理客户端连接,这一篇分析memcached源码中的process_update_command ...

  6. memcached学习笔记——存储命令源码分析下篇

    上一篇回顾:<memcached学习笔记——存储命令源码分析上篇>通过分析memcached的存储命令源码的过程,了解了memcached如何解析文本命令和mencached的内存管理机制 ...

  7. Hadoop学习笔记(10) ——搭建源码学习环境

    Hadoop学习笔记(10) ——搭建源码学习环境 上一章中,我们对整个hadoop的目录及源码目录有了一个初步的了解,接下来计划深入学习一下这头神象作品了.但是看代码用什么,难不成gedit?,单步 ...

  8. [Spark內核] 第42课:Spark Broadcast内幕解密:Broadcast运行机制彻底解密、Broadcast源码解析、Broadcast最佳实践

    本课主题 Broadcast 运行原理图 Broadcast 源码解析 Broadcast 运行原理图 Broadcast 就是将数据从一个节点发送到其他的节点上; 例如 Driver 上有一张表,而 ...

  9. Sping学习笔记(一)----Spring源码阅读环境的搭建

    idea搭建spring源码阅读环境 安装gradle Github下载Spring源码 新建学习spring源码的项目 idea搭建spring源码阅读环境 安装gradle 在官网中下载gradl ...

随机推荐

  1. IE下被遮住的iframe能接收事件

    今天,2012-2-22,是个很二的日子,遇到了一个诡异的问题~~ 下午收到一封信. from:someone@ctrip.comto:小灰灰title:被挡住的iframe也能接收到点击事件小灰灰, ...

  2. Python之路,day6-Python基础

    1.config 模块 import configparser conf = configparser.ConfigParser() conf[', 'Compression': 'yes', '} ...

  3. 简单配置和使用Maven

    1,下载Maven 从:https://maven.apache.org/download.cgi 其实两个都一样, 2,安装过程 解压你下载的包,随意放哪里都可以 ,假设 我放在了 D:\JavaT ...

  4. 在Mac上使用vim的几个命令

    Mac上面编辑文件的命令总是记不住,留一个参考!!! 如果是vim,则:Esc 退出编辑模式,输入以下命令: :wq  保存后退出vi,若为 :wq! 则为强制储存后退出(常用) :w    保存但不 ...

  5. 崽崽帮www.zaizaibang.com精选1

    南京郊外免费旅游景点推荐!不花钱又好玩~ 南艺帅哥手绘的南京,想说不爱你都难! [快乐 你懂的]—太原市育华幼儿园小一班 昆明周边游:那些近在咫尺的梨园 弘雅小学开展一年级新生入学准备期活动 大班的主 ...

  6. 关于在DataGrid.RowDetailsTemplate中的控件查找不到的问题

    DataGrid.RowDetailsTemplate中的控件需要显示出来才能查找,可以尝试在MouseLeftButtonUp等事件中处理.

  7. Unable to create Azure Mobile Service: Error 500

    I had to go into my existing azure sql database server and under the configuration tab select " ...

  8. spring @value 注入static

    import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Com ...

  9. dubbo服务自动化测试搭建

    java实现dubbo的消费者服务编写:ruby实现消费者服务的接口测试:通过消费者间接测试dubbo服务接口的逻辑 内容包括:dubbo服务本地调用环境搭建,dubbo服务启动,消费者部署,脚本编写 ...

  10. My tool chain in CentOS 7

    - Git Client SmartGit http://www.linuxlinks.com/article/20120129035558195/GitClients.html candidates ...