文档章节

java并发编程之:ReentrantLock实现原理与深入研究

BakerZhu
 BakerZhu
发布于 2017/07/08 10:52
字数 7489
阅读 471
收藏 0

前一篇博客简单介绍了ReentrantLock的定义和与synchronized的区别,下面跟随LZ的笔记来扒扒ReentrantLock的lock方法。我们知道ReentrantLock有公平锁、非公平锁之分,所以lock()我也已公平锁、非公平锁来进行阐述。首先我们来看ReentrantLock的结构

2015073100001

从上图我们可以看到,ReentrantLock实现Lock接口,Sync与ReentrantLock是组合关系,且FairSync(公平锁)、NonfairySync(非公平锁)是Sync的子类。Sync继承AQS(AbstractQueuedSynchronizer)。在具体分析lock时,我们需要了解几个概念:

AQS(AbstractQueuedSynchronizer):为java中管理锁的抽象类。该类为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。该类提供了一个非常重要的机制,在JDK API中是这样描述的:为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。 这么长的话用一句话概括就是:维护锁的当前状态和线程等待列表。

CLH:AQS中“等待锁”的线程队列。我们知道在多线程环境中我们为了保护资源的安全性常使用锁将其保护起来,同一时刻只能有一个线程能够访问,其余线程则需要等待,CLH就是管理这些等待锁的队列。

CAS(compare and swap):比较并交换函数,它是原子操作函数,也就是说所有通过CAS操作的数据都是以原子方式进行的。

公平锁(FairSync):lock

lock()定义如下:

final void lock() {
            acquire(1);
        }

lock()内部调用acquire(1),为何是”1”呢?首先我们知道ReentrantLock是独占锁,1表示的是锁的状态state。对于独占锁而言,如果所处于可获取状态,其状态为0,当锁初次被线程获取时状态变成1。

acquire()

acquire()是AbstractQueuedSynchronizer中的方法,其源码如下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

从该方法的实现中我们可以看出,它做了非常多的工作,具体工作我们先晾着,先看这些方法的实现:

tryAcquire

tryAcquire方法是在FairySync中实现的,其源代码如下:

protected final boolean tryAcquire(int acquires) {
        //当前线程
        final Thread current = Thread.currentThread();
        //获取锁状态state
        int c = getState();
        /*
         * 当c==0表示锁没有被任何线程占用,在该代码块中主要做如下几个动作:
         * 则判断“当前线程”是不是CLH队列中的第一个线程线程(hasQueuedPredecessors),
         * 若是的话,则获取该锁,设置锁的状态(compareAndSetState),
         * 并切设置锁的拥有者为“当前线程”(setExclusiveOwnerThread)。
         * 如果新添加的线程,当队列中没有元素 或者 无排在该线程之前的node元素的时候,直接让线程获取锁,无需排队。
         * 如果是唤醒线程,并且线程排在head节点 下个节点元素,则代表队列中无排在该线程前面的node元素。
         */
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        /*
         * 如果c != 0,表示该锁已经被线程占有,则判断该锁是否是当前线程占有,若是设置state,否则直接返回false
         */
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

在这里我们可以肯定tryAcquire主要是去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。

hasQueuedPredecessors:"当前线程"是不是在CLH队列的队首,来返回AQS中是不是有比“当前线程”等待更久的线程(公平锁)。

/*
         * 当c==0表示锁没有被任何线程占用,在该代码块中主要做如下几个动作:
         * 则判断“当前线程”是不是CLH队列中的第一个线程线程(hasQueuedPredecessors),当前线程之前还有没有需要执行的任务,有则返回true ,无则返回false
         * 若是的话,则获取该锁,设置锁的状态(compareAndSetState),
         * 并切设置锁的拥有者为“当前线程”(setExclusiveOwnerThread)。
         * 如果新添加的线程,当队列中没有元素 或者 无排在该线程之前的node元素的时候,直接让线程获取锁,无需排队。
         * 如果是唤醒线程,并且线程排在head节点 下个节点元素,则代表队列中无排在该线程前面的node元素。
         * (第一种情况)阻塞队列还未被使用:一直是 h == t == null 方法返回false
         * (第二种情况)当线程第一次访问中,第二个线程来访问,访问到节点:addWaiter->enq->
if(compareAndSetHead(new Node()))  tail = head; 判断通过设置完但是还未设置tail的值时, 第三个线程来访问,h != t && h.next == null  方法返回true
         * (第三种情况)当第一个线程执行完毕并唤醒队列中唯一一个线程的时候,如果不存在并发h!=t && h.next != null && s.thread == Thread.currentThread 成立 方法返回false
         * (第四种情况)当第一个线程执行完毕并唤醒队列中唯一一个线程的时候,如果不存在并发h!=t && h.next != null && s.thread == Thread.currentThread 成立,方法返回false。同时另一个线程访问,并且进入tryAcquire 的if (state == 0)中 h!=t && h.next != null && s.thread != Thread.currentThread 方法返回true
         */
public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;    
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

* (第一种情况)阻塞队列还未被使用:一直是 h == t == null 方法返回false
* (第二种情况)当线程第一次访问中,第二个线程来访问,访问到节点:addWaiter->enq->if(compareAndSetHead(new Node()))  tail = head; 判断通过设置完但是还未设置tail的值时, 第三个线程来访问,h != t && h.next == null  方法返回true
* (第三种情况)当第一个线程执行完毕并唤醒队列中唯一一个线程的时候,如果不存在并发h!=t && h.next != null && s.thread == Thread.currentThread 成立 方法返回false
* (第四种情况)当第一个线程执行完毕并唤醒队列中唯一一个线程的时候,如果不存在并发h!=t && h.next != null && s.thread == Thread.currentThread 成立,方法返回false。同时另一个线程访问,并且进入tryAcquire 的if (state == 0)中 h!=t && h.next != null && s.thread != Thread.currentThread 方法返回true

Node是AbstractQueuedSynchronizer的内部类,它代表着CLH列表的一个线程节点。对于Node以后LZ会详细阐述的。

compareAndSetState:设置锁状态

protected final boolean compareAndSetState(int expect, int update) {
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
        }

compareAndSwapInt() 是sun.misc.Unsafe类中的一个本地方法。对此,我们需要了解的是 compareAndSetState(expect, update) 是以原子的方式操作当前线程;若当前线程的状态为expect,则设置它的状态为update。

setExclusiveOwnerThread:设置当前线程为锁的拥有者

protected final void setExclusiveOwnerThread(Thread t) {
        exclusiveOwnerThread = t;
    }

addWaiter(Node.EXCLUSIVE)

private Node addWaiter(Node mode) {
        //new 一个Node节点
        Node node = new Node(Thread.currentThread(), mode);
        
        //CLH队列尾节点
        Node pred = tail;
        
        //CLH尾节点!= null,表示CLH队列 != null,则将线程加入到CLH队列队尾
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //若CLH队列为空,则调用enq()新建CLH队列,然后再将“当前线程”添加到CLH队列中。
        enq(node);
        return node;
    }

addWaiter()主要是将当前线程加入到CLH队列队尾。其中compareAndSetTail和enq的源代码如下:

/**
     * 判断CLH队列的队尾是不是为expect,是的话,就将队尾设为update
     * @param expect
     * @param update
     * @return
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
    
    /**
     * 如果CLH队列为空,则新建一个CLH表头;然后将node添加到CLH末尾。否则,直接将node添加到CLH末尾
     * @param node
     * @return
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) {     
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

addWaiter的实现比较简单且实现功能明了:当前线程加入到CLH队列队尾。

acquireQueued--------线程被唤醒之后只走这个方法内部for循环

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //线程中断标志位
            boolean interrupted = false;
            for (;;) {
                //上一个节点,因为node相当于当前线程,所以上一个节点表示“上一个等待锁的线程”
                final Node p = node.predecessor();
                /*
                 * 如果当前线程是head的直接后继则尝试获取锁
                 * 这里不会和等待队列中其它线程发生竞争,但会和尝试获取锁且尚未进入等待队列的线程发生竞争。这是非公平锁和公平锁的一个重要区别。
                 */
                if (p == head && tryAcquire(arg)) {//tryAcquire设置当前的锁拥有者为当前线程,status设置为1
                    setHead(node);     //将当前节点设置设置为头结点
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                /* 
                 * 如果不是head直接后继或获取锁失败,则检查是否要阻塞当前线程,是则阻塞当前线程
                 * shouldParkAfterFailedAcquire:判断“当前线程”是否需要阻塞,并且把tail上一个node的
                 * waitStatus由0设置为-1 标致为下个node需要被唤醒
                 * parkAndCheckInterrupt:阻塞当前线程
                 */
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);     
        }
    }

 Head为当前执行节点,把当前节点的线程设置为null , 前置节点设置为null

private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

同样是for无限循环,判断当前节点之前是否没有线程节点了,如果是,就去争抢锁。用上面给出的子类tryAcquire函数,成功的话设置相关参数,这里也解释了释放指针,帮助垃圾回收(GC)。

shouldParkAfterFailedAcquire()

如果争抢失败,判断是否需要阻塞当前线程

// 返回“当前线程是否应该阻塞”
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 前继节点的状态
    int ws = pred.waitStatus;
    // 如果前继节点是SIGNAL状态,则意味这当前线程需要被unpark唤醒。此时,返回true。
    if (ws == Node.SIGNAL)
        return true;
    // 如果前继节点是“取消”状态,则设置 “当前节点”的 “当前前继节点”  为  “‘原前继节点’的前继节点”。
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

waitStatus是节点Node定义的,她是标识线程的等待状态,他主要有如下四个值:

CANCELLED = 1:线程已被取消;

SIGNAL = -1:当前线程的后继线程需要被unpark(唤醒);

CONDITION = -2 :线程(处在Condition休眠状态)在等待Condition唤醒;

PROPAGATE = –3:(共享锁)其它线程获取到“共享锁”.

waitStatus 为 0的时候代表该Node位于等待线程队列的末尾,后面没有线程需要被唤醒。

有了这四个状态,我们再来分析上面代码,当ws == SIGNAL时表明这个节点的后续节点需要被unpark(唤醒),直接返回true,当ws > 0 (CANCELLED),表明当前节点已经被取消了,则通过回溯的方法(do{}while())向前找到一个非CANCELLED的节点并返回false。其他情况则设置该节点为SIGNAL状态。我们再回到if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()),p是当前节点的前继节点,当该前继节点状态设置SIGNAL后返回true,并把当前线程阻塞,则调用parkAndCheckInterrupt()阻塞当前线程。

Node源码:

private transient volatile Node head;    // CLH队列的队首
private transient volatile Node tail;    // CLH队列的队尾

// CLH队列的节点
static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // 线程已被取消,对应的waitStatus的值
    static final int CANCELLED =  1;
    // “当前线程的后继线程需要被unpark(唤醒)”,对应的waitStatus的值。
    // 一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。
    static final int SIGNAL    = -1;
    // 线程(处在Condition休眠状态)在等待Condition唤醒,对应的waitStatus的值
    static final int CONDITION = -2;
    // (共享锁)其它线程获取到“共享锁”,对应的waitStatus的值
    static final int PROPAGATE = -3;

    // waitStatus为“CANCELLED, SIGNAL, CONDITION, PROPAGATE”时分别表示不同状态,
    // 若waitStatus=0,则意味着当前线程不属于上面的任何一种状态。
    volatile int waitStatus;

    // 前一节点
    volatile Node prev;

    // 后一节点
    volatile Node next;

    // 节点所对应的线程
    volatile Thread thread;

    // nextWaiter是“区别当前CLH队列是 ‘独占锁’队列 还是 ‘共享锁’队列 的标记”
    // 若nextWaiter=SHARED,则CLH队列是“独占锁”队列;
    // 若nextWaiter=EXCLUSIVE,(即nextWaiter=null),则CLH队列是“共享锁”队列。
    Node nextWaiter;

    // “共享锁”则返回true,“独占锁”则返回false。
    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
    }

    // 构造函数。thread是节点所对应的线程,mode是用来表示thread的锁是“独占锁”还是“共享锁”。
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // 构造函数。thread是节点所对应的线程,waitStatus是线程的等待状态。
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

说明
Node是CLH队列的节点,代表“等待锁的线程队列”。
(01) 每个Node都会一个线程对应。
(02) 每个Node会通过prev和next分别指向上一个节点和下一个节点,这分别代表上一个等待线程和下一个等待线程。
(03) Node通过waitStatus保存线程的等待状态。
(04) Node通过nextWaiter来区分线程是“独占锁”线程还是“共享锁”线程。如果是“独占锁”线程,则nextWaiter的值为EXCLUSIVE;如果是“共享锁”线程,则nextWaiter的值是SHARED。

parkAndCheckInterrupt()

parkAndCheckInterrupt()在AQS中实现,源码如下:

private final boolean parkAndCheckInterrupt() {
    // 通过LockSupport的park()阻塞“当前线程”。
    LockSupport.park(this);
    // 返回线程的中断状态。
    return Thread.interrupted();
}

说明:parkAndCheckInterrupt()的作用是阻塞当前线程,并且返回“线程被唤醒之后”的中断状态。
它会先通过LockSupport.park()阻塞“当前线程”,然后通过Thread.interrupted()返回线程的中断状态。

这里介绍一下线程被阻塞之后如何唤醒。一般有2种情况:
第1种情况:unpark()唤醒。“前继节点对应的线程”使用完锁之后,通过unpark()方式唤醒当前线程。
第2种情况:中断唤醒。其它线程通过interrupt()中断当前线程。

补充:LockSupport()中的park(),unpark()的作用 和 Object中的wait(),notify()作用类似,是阻塞/唤醒。
它们的用法不同,park(),unpark()是轻量级的,而wait(),notify()是必须先通过Synchronized获取同步锁。
关于LockSupport,我们会在之后的章节再专门进行介绍!

selfInterrupt()

private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

说明:selfInterrupt()的代码很简单,就是“当前线程”自己产生一个中断。但是,为什么需要这么做呢?
这必须结合acquireQueued()进行分析。如果在acquireQueued()中,当前线程被中断过,则执行selfInterrupt();否则不会执行。

在acquireQueued()中,即使是线程在阻塞状态被中断唤醒而获取到cpu执行权利;但是,如果该线程的前面还有其它等待锁的线程,根据公平性原则,该线程依然无法获取到锁。它会再次阻塞! 该线程再次阻塞,直到该线程被它的前面等待锁的线程锁唤醒;线程才会获取锁,然后“正式执行起来”!
也就是说,在该线程“成功获取锁并正式执行起来”之前,它的中断会被忽略并且中断标记会被清除! 因为在parkAndCheckInterrupt()中,我们线程的中断状态时调用了Thread.interrupted()。该函数不同于Thread的isInterrupted()函数,isInterrupted()仅仅返回中断状态,而interrupted()在返回当前中断状态之后,还会清除中断状态。 正因为之前的中断状态被清除了,所以这里需要调用selfInterrupt()重新产生一个中断!

小结:selfInterrupt()的作用就是当前线程自己产生一个中断

 

 

总结

再回过头看看acquire()函数,它最终的目的是获取锁!

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

(01) 先是通过tryAcquire()尝试获取锁。获取成功的话,直接返回;尝试失败的话,再通过acquireQueued()获取锁。
(02) 尝试失败的情况下,会先通过addWaiter()来将“当前线程”加入到"CLH队列"末尾;然后调用acquireQueued(),在CLH队列中排序等待获取锁,在此过程中,线程处于休眠状态。直到获取锁了才返回。 如果在休眠等待过程中被中断过,则调用selfInterrupt()来自己产生一个中断。

 

 

release

  public void unlock() {
        sync.release(1);
    }

当tryAcquire()成功之后,线程获取锁,执行任务,执行完毕之后,会调用AQS的release方法:

设置status=0  当前exclusiveOwnerThread 为null ,设置头Node的状态为0, 唤醒离 头节点 最近的有效节点对应的线程。

 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是子类Sync的实现: 修改status 值 , 设置当前锁的拥有者为null 以便下次赋值

    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;
    }

其他就不多说了,比较简单明晰。就看看unparkSuccessor怎么做的 传参node为 头节点 设置头节点状态为0 ,找到离头节点最近的有效节点对应的线程 并唤醒。

 private void unparkSuccessor(Node node) {
        /*
         * 参数传递的是头节点
         * 设置头节点的状态为0
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 
         * 循环遍历节点数据,获取离头节点最近的 有效Node 并唤醒节点
         */
        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);
    }

 

 

获取非公平锁(基于JDK1.7.0_40)

非公平锁和公平锁在获取锁的方法上,流程是一样的;它们的区别主要表现在“尝试获取锁的机制不同”。简单点说,“公平锁”在每次尝试获取锁时,都是采用公平策略(根据等待队列依次排序等待);而“非公平锁”在每次尝试获取锁时,都是采用的非公平策略(无视等待队列,直接尝试获取锁,如果锁是空闲的,即可获取状态,则获取锁)。

1. lock()

lock()在ReentrantLock.java的NonfairSync类中实现,它的源码如下:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

说明
lock()会先通过compareAndSet(0, 1)来判断“锁”是不是空闲状态。是的话,“当前线程”直接获取“锁”;否则的话,调用acquire(1)获取锁。
(01) compareAndSetState()是CAS函数,它的作用是比较并设置当前锁的状态。若锁的状态值为0,则设置锁的状态值为1。
(02) setExclusiveOwnerThread(Thread.currentThread())的作用是,设置“当前线程”为“锁”的持有者。

“公平锁”和“非公平锁”关于lock()的对比

公平锁   -- 公平锁的lock()函数,会直接调用acquire(1)。
非公平锁 -- 非公平锁会先判断当前锁的状态是不是空闲,是的话,就不排队,而是直接获取锁。

2. acquire()

acquire()在AQS中实现的,它的源码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

(01) “当前线程”首先通过tryAcquire()尝试获取锁。获取成功的话,直接返回;尝试失败的话,进入到等待队列依次排序,然后获取锁。
(02) “当前线程”尝试失败的情况下,会先通过addWaiter(Node.EXCLUSIVE)来将“当前线程”加入到"CLH队列(非阻塞的FIFO队列)"末尾。
(03) 然后,调用acquireQueued()获取锁。在acquireQueued()中,当前线程会等待它在“CLH队列”中前面的所有线程执行并释放锁之后,才能获取锁并返回。如果“当前线程”在休眠等待过程中被中断过,则调用selfInterrupt()来自己产生一个中断。

“公平锁”和“非公平锁”关于acquire()的对比

公平锁和非公平锁,只有tryAcquire()函数的实现不同;即它们尝试获取锁的机制不同。这就是我们所说的“它们获取锁策略的不同所在之处”!

非公平锁的tryAcquire()在ReentrantLock.java的NonfairSync类中实现,源码如下:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

nonfairTryAcquire()在ReentrantLock.java的Sync类中实现,源码如下:

final boolean nonfairTryAcquire(int acquires) {
    // 获取“当前线程”
    final Thread current = Thread.currentThread();
    // 获取“锁”的状态
    int c = getState();
    // c=0意味着“锁没有被任何线程锁拥有”
    if (c == 0) {
        // 若“锁没有被任何线程锁拥有”,则通过CAS函数设置“锁”的状态为acquires。
        // 同时,设置“当前线程”为锁的持有者。
        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(nextc);
        return true;
    }
    return false;
}

说明
根据代码,我们可以分析出,tryAcquire()的作用就是尝试去获取锁。
(01) 如果“锁”没有被任何线程拥有,则通过CAS函数设置“锁”的状态为acquires,同时,设置“当前线程”为锁的持有者,然后返回true。
(02) 如果“锁”的持有者已经是当前线程,则将更新锁的状态即可。
(03) 如果不术语上面的两种情况,则认为尝试失败。

“公平锁”和“非公平锁”关于tryAcquire()的对比

公平锁和非公平锁,它们尝试获取锁的方式不同。
公平锁在尝试获取锁时,即使“锁”没有被任何线程锁持有,它也会判断自己是不是CLH等待队列的表头;是的话,才获取锁。
而非公平锁在尝试获取锁时,如果“锁”没有被任何线程持有,则不管它在CLH队列的何处,它都直接获取锁。
非公平锁实际上是线程抢占资源的时候的公平与不公平十分,如果线程在等待队列中之后,也是依照FIFO原则进行唤醒/获取的。

总结
公平锁和非公平锁的区别,是在获取锁的机制上的区别。表现在,在尝试获取锁时 —— 公平锁,只有在当前线程是CLH等待队列的表头时,才获取锁;而非公平锁,只要当前锁处于空闲状态,则直接获取锁,而不管CLH等待队列中的顺序。
只有当非公平锁尝试获取锁失败的时候,它才会像公平锁一样,进入CLH等待队列排序等待。

 

 

ReentrantLock 锁 lock ,tryLock , lockInterruptibly :

lock:

lock public void lock() 获取锁。 如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。

如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。

如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一 直处于休眠状态,此时锁保持计数被设置为 1。

lockInterruptibly

public void lockInterruptibly() throws InterruptedException
1)如果当前线程未被中断,则获取锁。
2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
4)如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以
前,该线程将一直处于休眠状态:
     1)锁由当前线程获得;或者
     2)其他某个线程中断当前线程。
5)如果当前线程获得该锁,则将锁保持计数设置为 1。
   如果当前线程:
       1)在进入此方法时已经设置了该线程的中断状态;或者
       2)在等待获取锁的同时被中断。
   则抛出 InterruptedException,并且清除当前线程的已中断状态。
6)在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或
重入获取。

tryLock    public boolean tryLock()

仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
1)如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。
即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),
而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公
平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS)
,它几乎是等效的(也检测中断)。
2)如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。
3)如果锁被另一个线程保持,则此方法将立即返回 false 值。

ReentrantLock可中断特性

对ReentrantLock可中断锁的介绍。可中断锁是通过ReentrantLock提供的lockInterruptibly()方法实现的。

响应中断是什么意思?

比如A、B两线程去竞争锁,A得到了锁,B等待,但是A有很多事情要处理,所以一直不返回。B可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。在这种情况下,synchronized的做法是,B线程中断自己(或者别的线程中断它),我不去响应,继续让B线程等待,你再怎么中断,我全当耳边风。而lockInterruptibly()的做法是,B线程中断自己(或者别的线程中断它),ReentrantLock响应这个中断,不再让B等待这个锁的到来。有了这个机制,使用ReentrantLock时死锁了线程可以中断自己来解除死锁。

什么叫做中断自己?

比如A、B两线程去竞争锁,它们肯定是被父线程创建并启动的,那父线程一定有它们的引用。线程都有interrupt()方法,假设父线程创建的线程B的引用是b,那b.interrupt()就是中断自己。

怎么中断自己

lock.lockInterruptibly(),这个方法会抛出异常InterruptedException。
什么时候抛出异常呢?当调用interrupt()方法自我中断的时候。
这时线程就进入了中断处理的过程,不会再等待锁了。
至于异常处理是怎样的,有很多种选择呀。比如可以退出线程的run()方法使线程完结,也可以使线程处理另外的事情。

一个中断锁的例子

package com.current.reentrantlock.interruptibly;

import java.util.concurrent.locks.ReentrantLock;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: zhubo
 * Date: 2017-09-12
 * Time: 17:35
 */
public class ReentrantLockSample {
    public static void main(String[] args) {
        testSynchronized();
        //testReentrantLock();
    }
    public static void testReentrantLock() {
        final SampleSupport1 support = new SampleSupport1();
        Thread first = new Thread(new Runnable() {
            public void run() {
                try {
                    support.doSomething();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"first");
        Thread second = new Thread(new Runnable() {
            public void run() {
                try {
                    support.doSomething();
                }
                catch (InterruptedException e) {
                    System.out.println("Second Thread Interrupted without executing counter++,beacuse it waits a long time.");
                }
            }
        },"second");
        executeTest(first, second);
    }
    public static void testSynchronized() {
        final SampleSupport2 support2 = new SampleSupport2();
        Runnable runnable = new Runnable() {
            public void run() {
                support2.doSomething();
            }
        };
        Thread third = new Thread(runnable,"third");
        Thread fourth = new Thread(runnable,"fourth");
        executeTest(third, fourth);
    }
    /**
     * Make thread a run faster than thread b,
     * then thread b will be interruted after about 1s.
     * @param a
     * @param b
     */
    public static void executeTest(Thread a, Thread b) {
        a.start();
        try {
            Thread.sleep(100);
            b.start();
            Thread.sleep(1000);
            b.interrupt();
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
abstract class SampleSupport {
    protected int counter;
    /**
     * A simple countdown,it will stop after about 5s.
     */
    public void startTheCountdown() {
        long currentTime = System.currentTimeMillis();
        for (;;) {
            long diff = System.currentTimeMillis() - currentTime;
            if (diff > 5000) {
                break;
            }
        }
    }
}
class SampleSupport1 extends SampleSupport {
    private final ReentrantLock lock = new ReentrantLock();
    public void doSomething() throws InterruptedException {
        lock.lockInterruptibly();
//        try {
//            lock.lockInterruptibly();
//        } catch (InterruptedException e) {
//            //做一些其它的事,不结束线程
//        }
        System.out.println(Thread.currentThread().getName() + " will execute counter++.");
        startTheCountdown();
        try {
            counter++;
        }
        finally {
            lock.unlock();
        }
    }
}
class SampleSupport2 extends SampleSupport {
    public synchronized void doSomething() {
        System.out.println(Thread.currentThread().getName() + " will execute counter++.");
        startTheCountdown();
        counter++;
    }
}

中断介绍

中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序。虽然初次看来它可能显得简单,但是,你必须进行一些预警以实现期望的结果。

从上面的介绍知道,interrupt()并不会使线程停止运行,那如何停止线程呢?

中断线程最好的,最受推荐的方式是,使用共享变量(shared
variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量(尤其在冗余操作期间),然后有秩序地中止任务。

    class Example2 extends Thread {
        volatile boolean stop = false;
        public static void main(String args[]) throws Exception {
            Example2 thread = new Example2();
            System.out.println("Starting thread...");
            thread.start();
            Thread.sleep(3000);
            System.out.println("Asking thread to stop...");
            thread.stop = true;
            Thread.sleep(3000);
            System.out.println("Stopping application...");
            //System.exit( 0 );
        }
        public void run() {
            while (!stop) {
                System.out.println("Thread is running...");
                long time = System.currentTimeMillis();
                while ((System.currentTimeMillis() - time < 1000) && (!stop)) {
                }
            }
            System.out.println("Thread exiting under request...");
        }
    }

 

 

 

 

 

 

 

© 著作权归作者所有

BakerZhu
粉丝 109
博文 517
码字总数 423077
作品 0
通州
程序员
私信 提问
加载中

评论(0)

BAT等公司必问的8道Java经典面试题,你都会了吗?

工作多年以及在面试中,我经常能体会到,有些面试者确实是认真努力工作,但坦白说表现出的能力水平却不足以通过面试,通常是两方面原因: 1、“知其然不知其所以然”。做了多年技术,开发了很...

java填坑路
2019/01/06
0
0
跳槽时,这些Java面试题99%会被问到

我在 Oracle 已经工作了近 7 年,面试过从初级到非常资深的Java工程师,且由于 Java 组工作任务的特点,我非常注重面试者的计算机科学基础和编程语言的理解深度,可以不要求面试者非要精通 ...

Java小铺
2018/08/15
536
0
Java 并发编程——CyclicBarrier

一、简介 是一个同步工具类,它允许一组线程在到达某个栅栏点(common barrier point)互相等待,发生阻塞,直到最后一个线程到达栅栏点,栅栏才会打开,处于阻塞状态的线程恢复继续执行.它非常...

Jitwxs
2019/11/25
0
0
Java多线程系列——从菜鸟到入门

持续更新系列。 参考自Java多线程系列目录(共43篇)、《Java并发编程实战》、《实战Java高并发程序设计》、《Java并发编程的艺术》。 基础 Java多线程系列——过期的suspend()挂起、resume()继...

郑斌blog
2017/02/23
0
0
4种常用Java线程锁的特点,性能比较及使用场景

多个线程同时对同一个对象进行读写操作,很容易会出现一些难以预料的问题。所以很多时候我们需要给代码块加锁,同一时刻只允许一个线程对某个对象进行操作。多线程之所以会容易引发一些难以发现...

mikechen优知
2019/03/10
273
0

没有更多内容

加载失败,请刷新页面

加载更多

四个步骤,学会单片机

有很多想学习单片机的朋友,但是不知道怎么入门,今天来讲讲我学习单片机的一些感受以及方法。由于单片机是一门要求动手的技术,所以,建议先确定所要学习单片机的型号,然后选用一块开发板,...

demyar
18分钟前
29
0
Nginx 配置获取用户真实IP

server { listen 80; server_name baidu.com; location / { if ($http_user_agent !~* (mobile|nokia|iphone|ipad|android|samsung......

牛A和牛C之间的我
20分钟前
19
0
Tower for Mac(强大的Git客户端) v4.3

Tower mac版是Macos上一款强大的Git客户端,具有强大的Git资源库管理、版本控制、分支管理等等,已被很多知名公司使用,并且能够和Xcode、GitHub、Beanstalk、BBEdit等软件无缝结合使用 . 功...

麦克W
21分钟前
19
0
Android知识体系总结2020之Android部分网路编程篇

1.计算机网络基础 计算机网络基础 2.网络数据解析 网路数据解析篇 3.Android网络编程 HttpClient和HttpURLConnection【比较过时,了解即可】 OkHttp Retrofit...

ClAndEllen
22分钟前
15
0
leaflet常用插件

1、常用地图切换加载(osm、google、baidu、gaode、tianditu.etc) https://github.com/htoooth/Leaflet.ChineseTmsProviders 2、切片地图加载(wmts)(支持矢量切片) https://github.com...

东东笔记
22分钟前
22
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部