JAVA多线程之——互斥锁ReentrantLock
来源:互联网 发布:软件测试干什么的 编辑:程序博客网 时间:2024/05/16 01:43
ReentrantLock简介
首先回顾一下synchronized关键字。
把代码声明为synchronized之后,那么就会保证,每次都只有一个线程获取对象的内部锁,进而产生互斥保证共享资源的安全。synchronized是获取对象的内部锁,所以是原生语法层面的互斥,需要JVM实现。
ReentrantLock是jdk1.5开始引入的JUC并发包中的一个类,ReentrantLock基于java代码实现,也就是API层面的互斥。ReentrantLock是锁的实现类,这就让它更具有灵活性,可以用多种算法来实现。而且在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)
要理解ReentranLock.我们先从它的整体结构和阅读源码开始学习。
在学习前可以先看看ReentrantLock的基本工作流程。然后带着流程去理解源码。这个流程图是在学习之后总结出来的。这里在前面也放一份。
ReentrantLock类中的方法列表:
// 创建一个 ReentrantLock ,默认是“非公平锁”。ReentrantLock()// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。ReentrantLock(boolean fair)// 查询当前线程保持此锁的次数。int getHoldCount()// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。protected Thread getOwner()// 返回一个 collection,它包含可能正等待获取此锁的线程。protected Collection<Thread> getQueuedThreads()// 返回正等待获取此锁的线程估计数。int getQueueLength()// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。protected Collection<Thread> getWaitingThreads(Condition condition)// 返回等待与此锁相关的给定条件的线程估计数。int getWaitQueueLength(Condition condition)// 查询给定线程是否正在等待获取此锁。boolean hasQueuedThread(Thread thread)// 查询是否有些线程正在等待获取此锁。boolean hasQueuedThreads()// 查询是否有些线程正在等待与此锁有关的给定条件。boolean hasWaiters(Condition condition)// 如果是“公平锁”返回true,否则返回false。boolean isFair()// 查询当前线程是否保持此锁。boolean isHeldByCurrentThread()// 查询此锁是否由任意线程保持。boolean isLocked()// 获取锁。void lock()// 如果当前线程未被中断,则获取锁。void lockInterruptibly()// 返回用来与此 Lock 实例一起使用的 Condition 实例。Condition newCondition()// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。boolean tryLock()// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。boolean tryLock(long timeout, TimeUnit unit)// 试图释放此锁。void unlock()
在来看一下它的源码,如果贴上全部源码可能会看的有点头晕。所以可以自行在Eclipse中打开源码。这里做一个简单的归类,以及后面一点一点的读。如图:
类图,ReentrantLock是Lock类的实现类,它有一个自己的内部类Sync.ReentrantLock将Lock类的大部分实现,全部委托给了Sync来实现。现,AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。Sync同时有两个子类,一个就是用于公平锁,一个用于非公平锁。下面就学习一下,是如何实现的。
锁的实现(加锁)
查看ReentrantLock API可以看到有一个方法lock()获取锁。源码如下:
public void lock() { sync.lock(); }
前面说了,大部分实现都是委托给了Sync这个类。而Sync有两个子类,先学习公平锁,理解了公平锁,对于非公平锁也容易多了。因此查看Sync子类 FairSync中的 lock()方法:
final void lock() { acquire(1); }
发现调用了一个acquire(1)方法,这个方法是干嘛的呢?从类图可以看到Sync继承了AbstractQueuedSynchronizer。而acquire(1)就是其AQS的一个方法。点击查看源码:
/** * Acquires in exclusive mode, ignoring interrupts. Implemented * by invoking at least once {@link #tryAcquire}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquire} until success. This method can be used * to implement method {@link Lock#lock}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
看其注释,这个方法是独占模式下的一个方法,并且忽略中断。如果获取到资源,就直接返回,否则就进入一个队列。直到获取到资源为止。我们获取资源第一个真方法可以说是这个才正式开始,那么这个方法也可以理解为独占模式下获取资源的顶层入口。然后分析方法:
方法中有一个判断,如果为真,就执行一个方法 selfInterrupt()。从方法名称来看,这个是自我中断。那么综合方法的注释——在获取资源时候,忽略中断。知道获取资源。那么方法就可以拆分2部分
1.获取资源,然后判断。
2.根据判断的真假决定是否执行自我中断(selfInterrupt())
先看第一步,if中包含两个方法。
1.tryAcquire(arg)
2.acquireQueued(addWaiter(Node.EXCLUSIVE), arg));
tryAcquire(arg)
tryAcquire是Sync的一个方法。源码:
/** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
该方法表示试图获取锁,如果获取成功直接返回true否则返回false。这里又分为2步骤
1.判断c是否等于0. 如果c等于0,那么就判断中执行hasQueuedPredecessors() 与compareAndSetState(0, acquires)。如果条件为真,就执行 setExclusiveOwnerThread(current) 并返回ture。
hasQueuedPredecessors 方法
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
这个方法就是判断当队列中是否有其它线程,如果没有,说明没有线程占有着锁。关于这个线程队列,下次会做个详细学习笔记。所以当hasQueuedPredecessors 返回false,说明当前队列没有线程,那么就执行compareAndSetState(0, acquires)方法。这个方法其实就是前面学习过类似的方法,CAS更新。把state更新为1.然后把独占线程设置为当前线程,就是说让当前线程获取到资源
那么为什么非得是c==0进来呢。那就看一下c不等于0是怎么执行的。
不等与0的时候执行current == getExclusiveOwnerThread()判断当前线程是否就是当前占锁的线程。那么这就可以理解,如果c==0就是说这个时候锁是空着的,没有任何线程占有,不等于0就是说明锁被占着。所以在上面,当更新完状态之后,我们就把锁给当前线程。那锁被占了为什么还要判断锁是不是当前线程占有,这是因为ReentrantLock是可重入锁,所以就要判断一次。如果是,就把c加上acquires然后更新状态值。返回。
这样我们就梳理一下:
在lock中调用的acquire(1),这个常量1就代表锁获取一次就需要更新的状态值,如果是第一次获取资源,则变为1,如果是重入那么就在原来的基础上加1.因此:ReentrantLock的锁的一个机制,是每当线程获取一次相同锁,就进行一次计数加1.同理,如果释放一次就减1.如果计数为0说明就是释放了锁
总结tryAcquire方法就是尝试的去获取一下资源,如果获取就返回true否则返回false.
因此在在尝试获取失败后再执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
addWaiter 方法
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)) { //进行CAS操作更新节点。如果失败代表有并发,进入enq方 法 pred.next = node; return node; } } enq(node); return node; }
嗯!在分析这个源码之前,先来看一下Node是什么。
static final class Node { /** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ static final Node EXCLUSIVE = null; /** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Thread thread; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
首先他是AbstractQueuedSynchronizer的一个静态的内部类。这个跟LinkedList源码中的Node类似。我们刚才说的队列就是由这个Node组成。组成的队列就是AQS中的CLH队列。关于CLH队列后续会写一遍学习笔记来仔细记录。这里我们需要理解的就是这个Node会把在一个个获取资源的线程串联成一个FIFO(先进先出)的队列。这样就保证了公平性。因此addWaiter 就是如果这个队列不为空就直接把线程加入队列,加入失败就代表有并发竞争,那就进入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; } } } }
OK。这个时候线程已经被封装成一个节点,并且加入了队列中。接下来要做一件什么事情呢?线程加入队列不可能还要一直运行着。所以需要将它挂起来。所以这个任务就交给acquireQueued来实现。
acquireQueued
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor();//获取节点的上一个节点 if (p == head && tryAcquire(arg)) {//如果上一个节点是头节点,并且常识获取锁成功。就把当前节点设置为头节点。 setHead(node); p.next = null; // help GC failed = false; return interrupted; //此处返回false.就是避免前面selfinterrupt方法执行。 } if (shouldParkAfterFailedAcquire(p, node) && //如果不当前节点不是头节点或者头节点获取资源失败。那么就意味着要等待头节点的下一次获取,那么判断当前线程是否需要挂起。 parkAndCheckInterrupt()) //如果当前线程需要挂起就调用LockSupport类中的park方法将线程挂起。然后进入下一次循环。 interrupted = true; } } finally { if (failed) //抛出异常就把节点从队列中移除 cancelAcquire(node); } }
shouldParkAfterFailedAcquire
*/ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
判断当前是否需要挂起是通过 waitStatus值来判断。
Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢?
原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点 代表了一个线程的状态,有的线程可能可能“等不及”获取锁了,需要放弃竞争,退出队列,有点线程在等待一些条件满足,满足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition同样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总需要一个变量买描述它,这个变量就叫waitStatus,它有四种状态:
/** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ **重点内容** static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3;
CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;
CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
PROPAGATE:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;
当且仅当前一个节点处于SIGNAL时候,才需要挂起。
释放锁
获取锁之后,是必须要在最后释放锁的。理解了获取锁,释放锁就容易多了。
释放操作需要做哪些事情:
1. 因为获取锁的线程的节点,此时在AQS的头节点位置,所以,可能需要将头节点移除。
2. 而应该是直接释放锁,然后找到AQS的头节点,通知它可以来竞争锁了。
到此ReentrantLock的公平锁基本分析完毕。那么还有一个非公平锁。非公平锁其实就是抢占式的。先看源码:
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
源码中就是非公平锁的实现,就是在执行公平锁之前,先不管三七二十一,先去获取一次锁,如果获取成功,直接返回,否则就老老实实的排队进入公平锁。让我想到买火车票排队。人太多,有些人就懒得排队,就会跑到最前面问售票员,可不可以帮我先买一张,我很急。售票员如果给一个白眼,那么他就公平了,如果售票员同情心起来,卖给他,那就非公平性了。
总结
源码的学习就在于能知其然。所以可能学习的过程会比较困难。但是希望自己坚持这种学习方式。到这里ReentrantLock基本学习分析完毕。其中涉及到的CLH队列。会做一个新的篇章学习。最后画一个流程图。来表示ReentrantLock的基本流程:
- JAVA多线程之——互斥锁ReentrantLock
- Java多线程之ReentrantLock
- java多线程学习之ReentrantLock
- java多线程之重入锁ReentrantLock
- Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock
- Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock (r)
- Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock (r)
- Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock
- Java多线程之concurrent包(一)——ReentrantLock与Condition
- Java多线程Lock对象之ReentrantLock(1)
- Java多线程Lock对象之ReentrantLock(2)
- 浅析java多线程之ReentrantLock的使用
- java多线程之LockSupport及ReentrantLock
- Java多线程之ReentrantLock使用-yellowcong
- java多线程编程——显示锁ReentrantLock(一)
- Java多线程系列(七)—ReentrantLock源码分析
- 多线程之重入锁ReentrantLock
- 【Java多线程】-ReentrantLock
- 学习AWT/Swing编程(一)解决Eclipse运行AWT/Swing项目和组件时中文乱码
- python学习笔记1---class
- 坚持#第156天~是那个事
- mybatis+maven实例
- Oracle 命令
- JAVA多线程之——互斥锁ReentrantLock
- Apache与Tomcat有什么关系和区别
- 排序算法之冒泡排序
- 屏幕适配之百分比方案详解
- keil4中debug信号函数的简单使用
- android中在fragment A里面点击button跳转到fragment B怎么实现?
- hello world在操作系统底层的执行过程
- 《春日飞翔》——为了纪念【小诗】
- java的classpath的作用