逐渐深入Java多线程(四)----Java的ReentrantLock简介

目录

一,ReentrantLock简介

二,ReentrantLock可重入的原理

三,非公平锁的加锁

四,公平锁的加锁

五,解锁逻辑

六,ReentrantLock的Condition

七,ReentrantLock中的其他方法

八,关于Unsafe


一,ReentrantLock简介

ReentrantLock,字面意思是可重入锁,由JDK1.5加入,位于java.util.concurrent.locks包中。

ReentrantLock是一种可重入且互斥的锁,和synchronized关键字效果差不多,但是功能更多样,使用方式更丰富一些。

 

从ReentrantLock的构造方法中可以看到,ReentrantLock支持公平锁和非公平锁的模式:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

默认是非公平锁。

所谓公平锁,就是保证先申请锁的线程会先获得锁,用队列实现。

非公平锁就是先申请锁的线程不一定会先获得锁,有可能会出现饥饿的情况,效率比公平高一些。

 

在ReentrantLock类中定义了抽象类Sync,并且维护了一个同名参数sync,可重入锁对象很多操作都由sync参数来完成。

Sync类继承了抽象类AbstractQueuedSynchronizer,AbstractQueuedSynchronizer维护了一个双向链表队列,设有head和tail节点。

AbstractQueuedSynchronizer类继承了AbstractOwnableSynchronizer类,此类用来记录锁的归属线程。

类的继承关系如下图:

抽象类Sync有两个默认的实现类(子类),都是ReentrantLock类中定义的内部类:

FairSync,公平锁的实现类

NonfairSync,非公平锁的实现类

从ReentrantLock的构造方法可以看到,无参构造使用的是非公平锁NonfairSync。

 

二,ReentrantLock可重入的原理

加锁:lock(),lockInterruptibly(),tryLock()等方法可以用来加锁

  1. 在AbstractQueuedSynchronizer中维护了state参数,类似状态的功能,state为0时可以获得锁。
  2. 线程获得锁后,state加1,设置锁的所属线程为当前线程。
  3. 同一线程试图再次获得锁时,锁的所属线程就是自己,则可以获得锁(即可重入),state再加1。

解锁:unlock()方法可以用来解锁

  1. state减1。
  2. state减到0时,说明该线程已经完全放弃了该锁,设置锁的所属线程为null。

FairSync和NonfairSync对于加锁和解锁有不同的实现方法,下面分别看一下。

 

三,非公平锁的加锁

以lock()方法为例:

public void lock() {
    sync.lock();
}

调用NonfairSync的lock()方法:

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

首先调用的是compareAndSetState(0, 1)方法,这个方法在AbstractQueuedSynchronizer中定义:

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

实际上是用UNSAFE把state从0改为1,其实就是抢锁,改成功了就算是获得了锁,这是一个CAS操作。

此处是第一个非公平锁体现不公平性的地方:线程抢锁时没有考虑队列中有没有正在等待的线程,鉴于解锁时会先把state改成0,再唤醒队列中的线程,此时就很有可能会被某个还没加入队列的线程在这一步抢锁成功。

如果compareAndSetState(0, 1)调用成功,说明获取到了锁,下面调用setExclusiveOwnerThread()方法把归属线程设为自己:

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

此方法在AbstractOwnableSynchronizer类中,实际上这个类就维护了这么一个属性以及他的get/set方法:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** Use serial ID even though all fields transient. */
    private static final long serialVersionUID = 3737899427754241961L;

    /**
     * Empty constructor for use by subclasses.
     */
    protected AbstractOwnableSynchronizer() { }

    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * Sets the thread that currently owns exclusive access.
     * A {@code null} argument indicates that no thread owns access.
     * This method does not otherwise impose any synchronization or
     * {@code volatile} field accesses.
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    /**
     * Returns the thread last set by {@code setExclusiveOwnerThread},
     * or {@code null} if never set.  This method does not otherwise
     * impose any synchronization or {@code volatile} field accesses.
     * @return the owner thread
     */
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

如果前面的compareAndSetState(0, 1)调用失败,即没获取到锁,则调用acquire(1)方法:

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

此方法在AbstractQueuedSynchronizer中,首先调用tryAcquire()方法进行一次不公平竞争锁,方法定义在NonfairSync中:

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

下面是nonfairTryAcquire()方法:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        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 exceed
        setState(nextc);
        return true;
    }
    return false;
}

代码中可以看到,首先获取状态state,然后分几种情况:

1,state=0。则立即调用compareAndSetState()方法试图获得锁,成功则调用setExclusiveOwnerThread()方法设置归属线程,返回true,此段逻辑和lock()方法中基本相同。

2,归属线程就是自己。把state加1,返回true。

3,其他情况,表示获取锁失败,返回false。

 

于是我们看到了第二处非公平锁的不公平性:此代码逻辑发生在第一次试图抢锁失败之后,tryAcquire()方法中又一次试图抢锁。

 

此时acquire()方法中的tryAcquire()方法执行完成,显然tryAcquire()方法返回false后就执行后面的acquireQueued()方法,acquireQueued()方法的一个参数是通过调用addWaiter(Node.EXCLUSIVE)来获得的,先来看一下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)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

方法的功能是把一个排他模式的节点加入队列,创建一个新节点,如果tail为空时则把该节点放到队尾,tail.next指向该节点,否则调用enq()方法将节点入队。

获得了排他模式的节点后,下面看一下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;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在代码逻辑中,p是新节点的前一节点,如果p是head节点,说明此新增节点是队列中的第一个节点(head节点不在队列中,head的next指向的是队列的第一节点),于是直接调用tryAcquire()方法试图得到锁,如果成功则把head节点设为此新增节点。如果p不是第一个节点,或者是第一节点但是抢锁失败,则调用parkAndCheckInterrupt()方法阻塞此节点代表的线程。

此逻辑中的抢锁貌似不能算是不公平的,毕竟在这里只有第一节点才能抢。

代码会重复以上过程,直到成功获取锁为止。

至此非公平锁的加锁逻辑结束。

 

从非公平锁加锁的逻辑中可以看到,新的线程试图获得锁时,有两次不公平的抢锁机会,而且这种直接抢锁的方式比在队列中等待唤醒然后抢锁的成功率更高,使得非公平锁显得更加的不公平。

虽然很不公平,但是这种抢锁方式带来了很大的性能提升:通过这种方式获得锁,线程就不用加入阻塞队列了,生成队列节点和入队之类的操作就免了,入队的CAS重试也免了(入队竞争激烈的时候效果更佳),线程阻塞唤醒什么的上下文切换也免了(线程加锁时间短时效果更佳),于是总体性能会提高很多,不怕饿的可以用这种非公平的模式。

 

四,公平锁的加锁

以lock()方法为例:

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

调用AbstractQueuedSynchronizer的acquire()方法,和非公平锁一样:

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

tryAcquire()方法定义在FairSync中,和NonfairSync的tryAcquire()方法略有不同:

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

比非公平锁的逻辑多了一个hasQueuedPredecessors()方法,这个方法用于判断该节点前还有没有其他节点,也就是说,state为0时队列前面还有线程在排队的话,就不会试图获得锁了,所谓公平锁的公平性就是通过这个方法来实现的:

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

 

五,解锁逻辑

ReentrantLock的unlock()方法用来解锁:

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

公平锁和非公平锁的release()方法都在AbstractQueuedSynchronizer类中:

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类中:

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

方法比较简单,state减1,如果减到0就把归属线程设为null,然后返回true。

至此解锁逻辑结束。

 

六,ReentrantLock的Condition

ReentrantLock的Condition提供了一套新的Object监视器方法,这是一种线程通信的方式。

ReentrantLock的Condition通过以下代码获得:

final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
final Condition condition2 = lock.newCondition();

在Condition出现之前,我们使用的是Object的notify()、wait()、notifyAll()等方法,而Condition在粒度上更细化了,在一个Lock中可以创建多个Condition,每个Condition都可以有自己的等待队列(wait-sets),便于不同的线程进行不同的处理。另外,从性能上来说Condition比Object版要高一些,提供的方法也更丰富一些。

Condition提供了await()方法实现Object的wait()功能,signal()方法实现Object的notify(),signalAll()方法实现Object的notifyAll()。另外Condition还提供了打断,超时等方法。

使用Condition的signal(),await(),signalAll()方法前,需要先进行ReentrantLock的lock()操作。

Condition的这几个方法不要和Object版的方法混用。

Condition非常适合生产者消费者模式。

 

用final标识的Condition对象可以参与线程间的通信,这也许就是这个类起名叫Condition(情景)的原因,被某个Condition对象的await()方法阻塞的线程A,当另一个线程B调用这个Condition对象的signal()方法时,线程A就会被唤醒,此时Condition对象就成了各线程间协调阻塞和唤醒的通用场景标识。

下面是一个Condition使用的例子,先让线程A负责输出1到3,然后唤醒线程B,线程B负责输出4到6,然后唤醒线程A,A最后输出over。代码中特意让线程B先于A开始执行。

下面是代码:

static class ThisIsANumber {
    public int number = 1;
}

public static void main(String[] args) {
    final Lock lock = new ReentrantLock();
    final Condition condition = lock.newCondition();
    final ThisIsANumber num = new ThisIsANumber();

    //线程A,负责输出1到3,输出完就唤醒其他线程,等其他线程把数字变为6以上时,输出over
    Thread threadA = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock();
                System.out.println("threadA get lock");
                while (num.number <= 3) {       //数字小于3则输出到3
                    System.out.println(num.number);
                    num.number++;
                }
                System.out.println("threadA call signal");
                condition.signal();     //唤醒其他线程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadA unlock");
                lock.unlock();
            }

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            try {
                lock.lock();
                System.out.println("threadA get lock again");
                if (num.number <= 6) {      //数字小于6则等待
                    System.out.println("threadA await() again");
                    condition.await();
                    System.out.println("threadA await() over");
                }
                System.out.println("threadA over");     //线程完成
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadA unlock again");
                lock.unlock();
            }
        }
    });

    //线程B,负责输出3到6,输出完则唤醒其他线程
    Thread threadB = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock();
                System.out.println("threadB get lock");
                if (num.number <= 3) {      //数字小于3则等待
                    System.out.println("threadB await()");
                    condition.await();
                    System.out.println("threadB await() over");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadB unlock");
                lock.unlock();
            }
            
            try {
                lock.lock();
                System.out.println("threadB get lock again");
                while (num.number <= 6) {       //数字小于6则输出到6
                    System.out.println(num.number);
                    num.number++;
                }
                System.out.println("threadB call signal");
                condition.signal();     //唤醒其他线程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadB unlock again");
                lock.unlock();
            }
        }

    });

    threadB.start();
    threadA.start();
}

输出的结果是这样的:

threadB get lock
threadB await()
threadA get lock
1
2
3
threadA call signal
threadA unlock
threadB await() over
threadB unlock
threadB get lock again
4
5
6
threadB call signal
threadB unlock again
threadA get lock again
threadA over
threadA unlock again

下面逐条解释一下这个输出的结果:

  • threadB get lock,由于先启动的线程B,所以B先获得锁。
  • threadB await(),number小于3,线程B开始WAIT。
  • threadA get lock,线程B开始WAIT后,线程A立即获得了锁,注意此时线程B还没有显式调用unlock()方法。
  • 1,线程A输出
  • 2,线程A输出
  • 3,线程A输出
  • threadA call signal,线程A输出完成后唤醒其他线程,也就是线程B。
  • threadA unlock,线程A唤醒线程B后等待了1秒,但是线程B一直没能开始执行,直到线程A调用unlock()方法。
  • threadB await() over,线程A调用unlock()方法释放锁后,线程B才获得锁,并从WAIT状态变为RUNNING状态。
  • threadB unlock,线程B释放锁。
  • threadB get lock again,线程B再次获得锁,因为此时线程A释放锁后Sleep了2秒,否则此时应该是线程A和线程B同时抢锁,而且线程A成功获得锁的概率要比线程B大的多。
  • 4,线程B输出
  • 5,线程B输出
  • 6,线程B输出
  • threadB call signal,线程B输出完毕,唤醒线程A。
  • threadB unlock again,线程B唤醒线程A后等待了1秒,但是线程A在此期间也没能开始执行,直到线程B释放锁。
  • threadA get lock again,线程A获得锁。
  • threadA over,线程A输出over。
  • threadA unlock again,线程A释放锁。

 

注意到中间有一次两线程抢锁,因为线程A当时在Sleep所以线程B抢到了锁,而且我们发现如果当时线程A没有在Sleep(也就是线程A释放锁后不Sleep2秒),那么线程A获得锁的可能性非常大,这种情况下的输出的结果就是这样了:

threadB get lock
threadB await()
threadA get lock
1
2
3
threadA call signal
threadA unlock
threadA get lock again
threadA await() again
threadB await() over
threadB unlock
threadB get lock again
4
5
6
threadB call signal
threadB unlock again
threadA await() over
threadA over
threadA unlock again

关于Condition我们有以下需要注意:

1,使用Condition的await()或signal()方法时,需要事先获得锁,否则将会抛异常。

2,线程使用await()方法,自己进入WAIT状态,会直接让出锁,不需要调用unlock()方法,其他线程就可以开始抢锁了。

3,线程调用signal()方法,唤醒其他线程时,被唤醒的线程无法立即被唤醒。被唤醒的线程还需要拿到锁才能从等待状态变成RUNNING状态。持锁线程调用unlock()或await()方法都会释放锁。

4,sleep()方法不会使线程让出锁,这一点和await()方法不一样。

 

关于生产者消费者模式需要注意:

使用生产者消费者模式时,特别是多个生产者和多个消费者的模式下,最好让生产者和消费者分别使用不同的Condition对象,因为Condition的signal()方法唤醒的不一定是哪个线程,所以可能会出现生产者处理完成后又唤醒一个生产者,或者消费者处理完成后又唤醒一个消费者,流程会卡住,所以最好使用两个Condition对象,让生产者唤醒的一定是消费者,消费者唤醒的也一定是生产者。

 

七,ReentrantLock中的其他方法

1, void lockInterruptibly()

这个方法也是用于获取锁,不过在等锁的过程中如果被interrupt,线程会立即抛出InterruptedException异常,不再继续等锁。相比之下,lock()方法在等锁期间被interrupt不会立即响应,而是会等到抢到锁,然后再interrupt。

2,boolean tryLock()

尝试抢锁,抢锁成功则返回true,失败则返回false,立即返回结果,不等待。

3,boolean isHeldByCurrentThread()

判断锁是否是当前线程持有。

4,boolean isLocked()

判断锁是否被任何线程持有,官方建议此方法用于锁状态监控,不要用于同步控制。

5,boolean isFair()

判断锁是否是公平锁。公平锁则返回true,非公平锁返回false。

6, Thread getOwner()

返回持锁的线程。没有线程持锁则返回null。

另外,官方注释中提到,如果调用这个方法的线程不是锁的持有者,返回的线程能体现锁的近似状态,比如有很多线程正在抢锁(暂时还没有线程抢到锁),这个方法的返回值就可能是null。没明白这段注释是想说什么。

7,boolean hasQueuedThreads()

判断是否有线程正在等待抢锁。

官方注释中提到,不要用这个方法来判断是否有线程会去抢锁,因为取消操作随时都会发生,这个方法是用来监控锁状态的。

8,boolean hasQueuedThread(Thread thread)

判断指定线程是否正在等待抢锁。

和hasQueuedThreads()方法一样,官方建议不要用这个方法来判断指定线程会不会去抢锁。

9,int getQueueLength()

返回在队列中等待抢锁的线程数量近似值。所谓近似值指的是,队列中的线程数量是动态变化的,该方法获取数量需要遍历等待队列,在遍历期间线程数量都有可能变化,所以只能是近似值。所以,不要用这个方法进行同步校验,只能用来监控锁状态。

10,Collection<Thread> getQueuedThreads()

返回在队列中等待抢锁的线程列表。返回的列表元素没有特定顺序。此返回值是不精确的,因为队列中的线程随时会变。这个方法主要是用来给子类重写用的。

11,boolean hasWaiters(Condition condition)

判断某个Condition下有没有等待状态的线程。这个方法的返回值不代表将来一定会唤醒那么多线程,因为这些线程随时可能会被interrupt或超时。此方法只用来监控系统状态。

12,int getWaitQueueLength(Condition condition)

返回在指定Condition下正在等待的线程数。不能用来做同步控制,只用来监控系统状态。

13,Collection<Thread> getWaitingThreads(Condition condition)

返回指定Condition下正在等待的线程列表。返回的列表元素没有特定顺序。返回结果不精确,因为这些线程随时可能会被interrupt或超时。这个方法主要是用来给子类重写用的。

 

八,关于Unsafe

Unsafe是sun.misc包下的类,提供了很多native标识的方法,是一些很底层的方法,甚至会绕过JVM。

在ReentrantLock中很多lock操作或者CAS操作都是由Unsafe类完成的。

官方不建议开发者使用Unsafe类,一个原因是sun.misc下的类本来是给Java自己用的,Java完全有可能在以后的版本中移除里面的类,包括Unsafe。二是因为Unsafe权限太高,提供的硬件级别操作会绕开JVM管理,自己操作内存地址,修改变量,操作线程,要是玩脱了JVM可能会崩,或者出现内存泄露什么的。正如他的名字所表达的意思一样,这个类确实不太安全。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章