Lock锁源码浅析--论如何凭枯燥吓退看客

Java中的锁

  • 重入锁(ReentrantLock)
  • 读写锁(ReentrantReadWriteLock)

Java锁如何实现

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;

    private final Sync sync;//锁

    abstract static class Sync extends AbstractQueuedSynchronizer {
        //some code
    }
    
    //非公平锁
    static final class NonfairSync extends Sync {
      	//some code
    }

    //公平锁
    static final class FairSync extends Sync {
       	//some code
    }
    ......
}

通过ReentrantLock的简化代码,我们可以发现它内部有一个内部类Sync对象来代表锁,而这个Sync对象继承了AQS。

队列同步器(AQS)

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691L;

    static final class Node {
	//some code
	}

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;

    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }

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

由这个简化的AQS代码可以看出,最主要的属性就是一个内部类Node,和一个被volatile修饰的int变量state。

AQS通过使用state来表示同步状态,用内置同步队列来完成线程获取资源的排队管理工作。

AQS的主要使用方式为继承,子类通过继承AQS来管理同步状态,这样不可避免就要对同步状态进行操作,所以AQS提供了三个方法来操作同步状态,即getState()、setState()和compareAndSetState()。

AQS的实现分析

1.同步队列

AQS依赖于内部的一个同步队列(双向队列)来完成线程的管理。当一个线程获取同步状态失败后,同步器会将该线程、等待状态、前驱和后继节点等信息构造成一个Node节点,调用CAS方法compareAndSetTail(Node expect,Node update)将其放在同步队列的尾部,同时阻塞该线程。

同步队列的首节点是获取到同步状态的线程,当它释放同步状态后,会唤醒它的后继节点(线程),当后继节点获得了同步状态后就将自己设为首节点。

2.抢占式同步状态的获取和释放

获取

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

抢占式方法acquire的主要逻辑是:先调用tryAcquire去线程安全地尝试获取同步状态,如果失败了,就构造一个抢占式Node节点:Node.EXCLUSIVE,通过addWaiter方法加到同步队列尾部,再用acquireQueued方法自旋获取同步状态。

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
	//判断队列为不为空	
        if (pred != null) {
            node.prev = pred;//将该节点的前驱节点设为队列尾结点
            if (compareAndSetTail(pred, node)) {//尝试使用CAS将节点放在队列尾部
                pred.next = node;
                return node;
            }
        }
	//失败了就执行enq,死循环尝试将节点放在队列尾部
        enq(node);
        return node;
    }
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;
                }
            }
        }
    }

将节点成功放在队列尾部后,就调用acquiredQueue去自旋获取同步状态:

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; //将原来的头节点设为null方便垃圾回收
                    failed = false;
                    return interrupted;//返回false
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;//如果线程被中断,则返回true
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

注意上面的方法,正常情况下acquireQueued返回的是false,所以acquire方法中的selfInterrupt方法不会执行;但是如果当前线程被中断,acquireQueued返回的就是true,那么selfInterrupt就会执行,去中断线程。

释放

通过release方法释放锁,以便后继节点可以获取锁。

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去释放锁,成功之后就用unparkSuccessor()去唤醒后继节点。

3.共享式同步状态获取和释放

获取

通过调用同步器的acquireShared可以共享式地获取同步状态。

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)//返回值>=0代表获取到了同步状态,返回值<0就需要执行doAcquireShared()方法自旋获取同步状态
            doAcquireShared(arg);
    }
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);//构建一个共享式Node节点
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//获取该节点的前驱节点
                if (p == head) {//如果前驱节点是头节点
                    int r = tryAcquireShared(arg);//尝试获取同步状态
                    if (r >= 0) {//获取到了同步状态
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

简而言之共享式获取的过程就是做一次或者自旋请求同步状态。

释放

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

共享式也是先释放同步状态,然后唤醒后继节点,唯一不同的是共享式需要通过CAS保证释放的线程安全。

重入锁ReentrantLock

重入锁即获得了锁的线程可以重复对资源加锁,还支持非公平锁和公平锁。

1.可重入的实现

ReentrantLock内部有个方法可以返回获得锁的线程,只要判断一下是否是当前线程就可以实现重入,重入一次就要增加一次同步状态的值,退出一次就要减少一次同步状态的值。

2.非公平锁的获取

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))//先尝试直接使用CAS获取锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);//失败了就调用AQS的acquire方法
        }
	//ReentrantLock的tryLock方法就是调用这个方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//如果还没有线程获得锁,那么直接让该线程获得锁,并将同步状态更新
                //只要CAS获取到了同步状态,就代表获取到了锁,那么刚释放完同步状态的线程再次获取同步状态的概率非常大,就会出现同一个线程重复获取锁的现象
                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;//没有获得同步状态则返回false
        }

3.公平锁的获取

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);//直接调用AQS的acquire方法
        }

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

4.公平锁和非公平锁的释放

两者的释放都是同一个unlock方法,底层直接调用AQS的独占式release方法。

总结

公平锁和非公平锁最大的区别是在于tryLock方法的实现上。公平锁的tryLock是先查看同步队列中有没有等待的节点,有就直接退出方法返回false;非公平锁是直接用CAS去获取同步状态(因为刚释放完同步状态马上去获取的话,成功率是很高,在这里体现了非公平),获取失败了才退出方法返回false。

为什么非公平锁是默认的?

每当一个不同的线程获取同步状态,就相当于进行了1次上下文切换,假如有5个线程,每个线程要获取两次同步状态才能完成任务,那么公平锁就要进行10次上下文切换,而非公平锁因为基本上每个线程都可以连续获得同步状态,这样就只要进行5次上下文切换,大大节省了系统开销。所以一般情况下非公平锁的效率比较高,假如能够确定需要使用公平锁,就用ReentrantLock(true)将重入锁设置为公平锁就可以了。

读写锁(ReentantReadWriteLock)

读写锁也是可重入的,也支持公平锁和非公平锁。

1.读写状态和可重入的设计

在AQS中用state来表示同步状态,重入锁也遵循了AQS的设计,但是读写锁它进行了优化,用一个state变量表示读状态和写状态。具体实现如下:

一个int型的state有32位,读写锁用高16位来表示写状态,记录写锁的次数;用低16位表示读状态,记录读锁的次数。

2.读写锁的设计

写锁的获取和释放

写锁的lock方法是直接调用AQS的acquire方法,在此不再介绍。

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

写锁的tryLock是一个支持重入的排他锁方法,只要写状态为0或者获得写锁的就是当前线程,且读状态也为0,该线程就能获取写锁,增加写状态。假如写状态不为0且当前线程不是获得写锁的线程,或者读状态不为0,则线程进入阻塞状态。

public boolean tryLock( ) {
            return sync.tryWriteLock();
        }
//Sync类的方法
final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {//说明写状态或者读状态不为0
                int w = exclusiveCount(c);
                if (w == 0 || current != getExclusiveOwnerThread())//如果是读状态不为0或者获得写锁的不是当前线程,就返回false
                    return false;
                if (w == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
            }
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

为什么读状态必须为0呢?(即为什么在读锁被占用的时候不能获得写锁)

这是因为读写锁必须保证写锁对数据的操作对读锁是可见的。假如有线程获得了读锁,然后给另一个线程获得写锁,那么获得读锁的线程就无法得知获得写锁的线程对数据的操作。只有当所有的线程都释放了读锁,才允许线程获得写锁去改变数据。

写锁的释放是直接调用AQS的release方法来实现的。

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

读锁的获取与释放

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

可以看出读锁的lock方法是调用AQS的共享式同步状态获取方法acquireShared来实现的。

读锁的tryLock方法是调用Sync的tryReadLock方法来实现的。假如在获取读锁的过程中,有其他线程获取了写锁,就退出方法并返回false,否则就死循环尝试用CAS增加读状态。

public boolean tryLock() {
            return sync.tryReadLock();
        }

final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {//死循环尝试增加读状态
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)//如果写状态不为0且获得写锁的线程不是当前线程,返回false
                    return false;
                int r = sharedCount(c);
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {//尝试增加读状态
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }

读锁的释放采用的是AQS的共享式同步状态释放方法。

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

LockSupport

AQS同步队列中线程的阻塞和唤醒是如何实现的呢?

LockSupport提供了多个阻塞和唤醒线程的方法:

  • park():阻塞当前线程,被唤醒或者中断才能从park方法返回
  • parkNanos(long nanos):阻塞当前线程一段时间,超时、唤醒或中断才能返回
  • parkUntil(long deadline):阻塞当前线程,直到deadline这一时刻、唤醒或者中断才能返回
  • unpark():唤醒阻塞的线程

Condition接口

Object拥有一组监视器方法包括wait、sleep、notify等,可以与synchronized配合使用。Condition接口同样定义了一组监视器方法用于和Lock配合使用。方法列表如下:

  • await():跟wait方法差不多,都需要得到通知才能恢复运行,但是还能被其他线程调用interrupt()方法中断,如果等待线程从await方法返回,说明该线程获取到了Condition对象所对应的锁
  • awaitUninterruptibly():同await,但是不会被中断
  • awaitNanos():同await,可以被通知、中断和超时
  • awaitUntil():同awaitNanos,如果没到时就被通知或中断,则返回true,否则如果到时自动结束则返回false
  • signal():唤醒一个等待在Condition上的线程,该线程从等待方法返回必须获得与Condition相关的锁
  • signalAll():唤醒所有等待线程,能够从等待方法返回的线程必须获得与Condition相关的锁
public class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                conditionAwait();
            }
        }).start();

        Thread.sleep(1000);

        lock.lock();//因为await释放了锁,这里才能获取到锁
        condition.signal();//唤醒等待线程
        lock.unlock();//释放锁

    }

    public static void conditionAwait() {
        lock.lock();
        try {
            condition.await();//必须获得锁才能调用await,调用完await后锁就被释放了
            System.out.println("被唤醒了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
Condition的实现

AQS中的内部类ConditionObject类实现了Condition接口。

1.等待队列

ConditionObject用于构建等待队列的节点就是AQS的Node。所有调用await方法的线程都会被构建成Node节点放入等待队列。因为只有获得锁的线程才能调用await,所以这里是线程安全的。

2.等待await

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();//这里将当前线程加入等待队列
            int savedState = fullyRelease(node);//释放锁
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

3.唤醒signal

public final void signal() {
            if (!isHeldExclusively())//判断当前线程是否获得了锁,必须获得锁才能调用signal方法
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);//唤醒首节点
        }

private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        Node p = enq(node);//将节点放入同步队列尾部
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);//唤醒节点
        return true;
    }

唤醒线程其实是用enq方法将首节点线程从等待队列中移到同步队列尾部去,然后调用LockSuppor.unpark唤醒线程。

注意等待队列和同步队列之间的差别:

等待队列存放的是所有调用await的线程,进入等待状态的线程是挂起的,无法获得CPU时间片。而同步队列中的线程处于RUNNABLE状态,是不断自旋的。所以在将等待队列的节点移到同步队列后,还要用LockSupport.unpark去唤醒节点,让它进入RUNNABLE状态,它才能去获取CPU时间片。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值