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. 浅谈 js 字符串之神奇的转义

    原文:浅谈 js 字符串之神奇的转义 字符串在js里是非常常用的,但是你真的了解它么?翻阅<MDN String>就可以了解它的常见用法了,开门见山的就让你了解了字符串是怎么回事. 'st ...

  2. [kmp+dp] hdu 4628 Pieces

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=4622 Reincarnation Time Limit: 6000/3000 MS (Java/Ot ...

  3. BZOJ 3282 Tree Link-Cut-Tree(LCT)

    题目大意: 给定N个点以及每一个点的权值,要你处理接下来的M个操作.操作有4种.操作从0到3编号.点从1到N编号. 0:后接两个整数(x,y),代表询问从x到y的路径上的点的权值的xor和.保证x到y ...

  4. vi使用高级

    下面的命令在vi命令模式被激活 h 光标左移一个字符 l 光标向右移动一个字符 j 光标下移一行 k 移动光标线 一.对整行操作 1.复制光标所在行 yy 2.删除光标所在行 dd 3.选中光标所在行 ...

  5. Coreseek:索引和检测的第二步骤施工

    1,非常索引easy,代码行 g:/service/coreseek/bin/indexer -c g:/service/coreseek/etc/csft_mysql.conf   person 在 ...

  6. Windows环境搭建与第一个C# Sample

    Redis入门 - Windows环境搭建与第一个C# Sample   什么是Redis? Redis是一个开源.支持网络.基于内存.键值对存储数据库,使用ANSI C编写.从2013年5月开始,R ...

  7. Appium Android Bootstrap源码分析之控件AndroidElement

    通过上一篇文章<Appium Android Bootstrap源码分析之简介>我们对bootstrap的定义以及其在appium和uiautomator处于一个什么样的位置有了一个初步的 ...

  8. ASPX的Timer位置没放正确,导致整页刷新,而不是UpdatePanel里的内容刷新。

    提示:Timer应该放在UpdatePanel的ContentTemplate标签里,才行.放在外面的话,会导致整页刷新.

  9. DOM2级事件对象、添加事件、阻止默认事件、阻止冒泡事件、获取事件对象目标的兼容处理

    事件对象——兼容处理 /* * 功能: 事件对象兼容 * 参数: 表示常规浏览器的事件对象e */ function getEvent(e) { // 如果存在e存在,直接返回,否则返回window. ...

  10. 快速构建Windows 8风格应用23-App Bar概述及使用规范

    原文:快速构建Windows 8风格应用23-App Bar概述及使用规范 本篇博文主要介绍App Bar概述.App Bar命令组织步骤.App Bar最佳实践.   App Bar概述 Windo ...