JUC锁机制(Lock)学习笔记,附详细源码解析

JUC锁机制(Lock)学习笔记,附详细源码解析

2013-08-22 20:03 by CM4J, 56 阅读, 0 评论,收藏编辑

锁机制学习笔记


目录:

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
 
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {  // 0,代表当前锁无其他线程持有
        if (isFirst(current) && compareAndSetState(0, acquires)) { // isFirst是公平锁和非公平锁在tryAcquire的唯一区别
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 非0,代表有线程持有锁,判断持有者是否为当前线程
        // 这里修改为旧值+1呢?这是因为ReentrantLock是可重入锁,同一个线程每持有一次就+1
        int nextc = c + acquires;
        if (nextc < 0)
            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
 
final boolean acquireQueued(final Node node, int arg) {
    try {
        boolean interrupted = false;
        // 循环操作
        for (;;) {
            final Node p = node.predecessor();
            // 如果第一个节点是Dummy Header也就是傀儡节点,那么第二个节点实际上就是头结点了,此时则尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            // acquire失败,判断是否应该park,并检查线程是否interrupt
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } catch (RuntimeException ex) {
        cancelAcquire(node);
        throw ex;
    }
}

// CANCELLED = 1
// SIGNAL = -1
// CONDITION = -2
// NORMAL = 0
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 前一个节点的状态(注意:不是当前节点)
    int ws = pred.waitStatus;
    if (ws < 0)
        // waitStatus<0,也就是前面的节点还没有获得到锁,那么返回true,表示当前节点(线程)就应该park()了。
        return true;
    if (ws > 0) {
        // waitStatus>0,也就是前一个节点被CANCELLED了,那么就将前一个节点去掉,递归此操作直到所有前一个节点的waitStatus<=0,进行4
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        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
 
public final boolean release(int arg) {
    // tryRelease 成功
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // unpark节点链的head后续节点的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    // state-1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // state-1 == 0,则说明此时没有其他线程持有锁,release成功,unpark节点链的head节点的后续线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 设置新state
    setState(c);
    return free;
}
private void unparkSuccessor(Node node) {
    // 此时node是需要释放锁的头节点
    // 清空头结点的waitStatus,也就是不需要锁了,这里修改成功失败无所谓
    compareAndSetWaitStatus(node, ws, 0);

// 从头结点的下一个节点开始寻找继任节点,当且仅当继任节点的waitStatus<=0才是有效继任节点,否则将这些waitStatus>0(也就是CANCELLED的节点)从AQS队列中剔除
    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);
}

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

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

 
 
 
标签: 独占锁JUClock

JUC锁机制的更多相关文章

  1. JUC.Lock(锁机制)学习笔记[附详细源码解析]

    锁机制学习笔记 目录: CAS的意义 锁的一些基本原理 ReentrantLock的相关代码结构 两个重要的状态 I.AQS的state(int类型,32位) II.Node的waitStatus 获 ...

  2. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...

  3. Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock

    本章对ReentrantLock包进行基本介绍,这一章主要对ReentrantLock进行概括性的介绍,内容包括:ReentrantLock介绍ReentrantLock函数列表ReentrantLo ...

  4. Java多线程系列--“JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--“JUC锁”01之 框架02. Java多线程系列--“JUC锁”02之 互斥锁Reentrant ...

  5. java多线程系类:JUC锁:01之框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--"JUC锁"01之 框架02. Java多线程系列--"JUC锁&q ...

  6. Java多线程系列--“JUC锁”05之 非公平锁

    概要 前面两章分析了"公平锁的获取和释放机制",这一章开始对“非公平锁”的获取锁/释放锁的过程进行分析.内容包括:参考代码获取非公平锁(基于JDK1.7.0_40)释放非公平锁(基 ...

  7. Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

    概要 本章,我们对JUC包中的信号量Semaphore进行学习.内容包括:Semaphore简介Semaphore数据结构Semaphore源码分析(基于JDK1.7.0_40)Semaphore示例 ...

  8. 深入浅出Java并发包—锁机制(一)

    前面我们看到了Lock和synchronized都能正常的保证数据的一致性(上文例子中执行的结果都是20000000),也看到了Lock的优势,那究竟他们是什么原理来保障的呢?今天我们就来探讨下Jav ...

  9. 转 : 深入解析Java锁机制

    深入解析Java锁机制 https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw%3D%3D&mid=2247485524&idx=1&s ...

随机推荐

  1. Bag标签之中的一个行代码实行中文分词实例1

    例1: 分词(返回以逗号隔开的词组,gap=",") <bagid=pPage act=2words name=words gap=",">我喜欢黄 ...

  2. java压缩zip文件中文乱码问题(转——作者:riching)

    本人遇到了同样的问题,用了以下方案,奇迹般的解决了.我很纳闷为什么,经理说:好读书,不求甚解,不要问为什么... 用java来打包文件生成压缩文件,有两个地方会出现乱码 1.内容的中文乱码问题,这个问 ...

  3. Asp.net vNext 2

    Asp.net vNext 学习之路(二) View component(视图组件)应该是MVC6 新加的一个东西,类似于分部视图.本文将演示在mvc 6中 怎么添加视图组件以及怎么在视图中注入一个服 ...

  4. ASP.NET 5 Hello World

    ASP.NET 5系列教程 (二):Hello World   本篇文章内容比较基础,主要是向大家展示如何创建一个 ASP.NET 5 工程,主要包含内容如下: 创建ASP.NET 5 工程 添加 T ...

  5. CI框架 .htaccess 隐藏url在index.php解决方案

    CodeIgniter(下面简称"CI")是一款国外优秀的PHP轻量级MVC框架,它支持PHP4和PHP5.是开发中小型可拓展性需求高的Web应用程序的利器.眼下你所见到的这个博客 ...

  6. jquery-制作选项卡

    强大的jquery-制作选项卡   最近在学习jquery,特地把今天写的一个选项卡源码贴出来.只是做只是梳理,大神们请不要吐槽,如果有更好的方法,欢迎指点.谢谢. css <style> ...

  7. 实例学习SSIS(四)--使用日志记录和错误流重定向

    原文:实例学习SSIS(四)--使用日志记录和错误流重定向 导读: 实例学习SSIS(一)--制作一个简单的ETL包 实例学习SSIS(二)--使用迭代 实例学习SSIS(三)--使用包配置 实例学习 ...

  8. HubbleDotNet全文搜索数据库组件(一)

    HubbleDotNet 简介及安装详解 2012-11-05 12:59 来源:9SSSD.COM 作者:starts_2000 字号:T|T [摘要]HubbleDotNet 是一个基于.net ...

  9. SVG 学习(一)

    SVG 意为可缩放矢量图形(Scalable Vector Graphics). SVG 使用 XML 格式定义图像. 什么是SVG? SVG 指可伸缩矢量图形 (Scalable Vector Gr ...

  10. javascript 正则介绍

    1.正则直接量字符 \o NUL字符(\u000)\t 制表符\n 换行符(\u000A)\v 垂直制表符\f 换页符\xnn 由16进制nn指定的拉丁字符\uXXXX 由16进制XXXX指定的unc ...