【J.U.C-Locks】锁框架——Lock接口与ReadWriteLock接口

Lock接口是JUC的lock包中的一个接口,ReentrantLock可重入独占锁和ReadWriteLock读写锁都实现了此接口,
两者都支持公平与非公平锁,与可重入。
由CAS一文知这些锁在内部需要实现具体的资源获取释放的策略。

ReentrantLock资源获取时,如果资源没有被获取,那么CAS修改state值;如果资源已被当前线程获取过,那么setState值+1;
释放锁就是将state值-1。

ReadWriteLock接口为读写锁,针对于读多写少的情景。
原则就是允许多个线程读,只允许一个线程写;读写混合的话只允许同一个线程同时进行。
原理是state资源的按位拆分,然后在资源获取时,额外判断写锁读锁的数量+获取锁的是否就是当前线程,判断是否允许获取资源或者重入。

【Lock】接口

锁是用来控制多个线程访问共享资源的方式。

在Lock接口出现之前,Java是通过synchronized关键字来实现锁功能的。Lock接口提供了与synchronized关键字类似的同步功能,只是需要再使用时 显式地获取和释放锁

Lock提供的synchronized关键字所不具备的特性:

  • 尝试 非阻塞 地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁;
  • 被中断 地获取锁:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断,中断异常将被抛出,同时锁会被释放;
  • 超时 获取锁:在指定的截止日期之前获取锁,如果截止时间到了仍旧无法获取锁,则返回;

Lock接口:

public interface Lock {
	//获取锁
 	void lock();
 	//可中断地获取锁,即在锁的获取中可以中断当前线程
  	void lockInterruptibly() throws InterruptedException;
  	//尝试非阻塞地获取锁,调用该方法后立刻返回,如果能够获取返回true,否则返回false
  	boolean tryLock();
  	//超时的获取锁,当前线程再以下情况会返回:
  	//1.当前线程再超时时间内获得了锁
  	//2.当前线程再超时时间内被中断
  	//3.超时时间结束,返回false 
  	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
   	//释放锁
   	void unlock();
   	//获取等待通知组件,该组件和当前锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,调用后,当前线程将释放锁
    Condition newCondition();
}

【ReentrantLock】可重入锁(Lock接口实现类)

ReentrantLock类实现了Lock接口,是一种 可重入的独占锁 。它与synchronized的区别在于:

ReentrantLockSynchronized
锁实现基于AQS基于Monitor监视器
锁释放需显式unlock无需手动释放锁
锁类型公平、非公平非公平
可重入可重入可重入

使用方式:

ReentrantLock lock = new ReentrantLock(true);
        lock.lock();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

存储结构与构造函数

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;
}

ReentrantLock的构造函数有两种:指定公平锁、非公平锁(默认非公平)

	public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
锁类别
公平策略在多个线程争用锁的情况下,倾向于将锁授予等待时间最长的线程,也就是相当于有一个等待队列,先进入等待队列的线程会先获取锁;
非公平策略多个线程争用锁的情况下,获取锁的线程是随机的;

1.可重入性

  • ReentrantLock的可重入性:锁可以被单个线程多次获取

实现可重入的特性,需要解决两个问题:

  • 判断获取锁的线程是否就是当前持有锁的线程,如果是,再次获取成功;
  • 某个线程获取了n次锁,那么需要释放n次之后,才能被其他线程获取;

由此可知,ReentrantLock内部维护了一个计数器,也就是AQS提供的 state

2. 公平锁与非公平锁

2.1【公平锁获取锁】tryAcquire

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

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

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

由AQS原理分析一文知,tryAcquire线程尝试获取锁,是由ReentrantLock自己实现的。

  1. 首先根据AQS提供的getState方法获取资源状态
  2. 如果state=0,表示资源没有被任何线程获取:
    1. hasQueuedPredecessors,这个方法来自于父类AQS:如果当前线程是CLH队列的队头则返回false,否则返回true;
    2. 如果返回true,表示等待队列前面还有其他节点,直接返回获取锁失败
    3. 如果返回false,表示当前线程是第一个节点,尝试CAS修改state值
    4. CAS成功,setExclusiveOwnerThread设置独占线程为当前线程;
    5. CAS失败,返回获取锁失败
  3. 如果state!=0,表示资源已经被获取
    1. getExclusiveOwnerThread得到当前获取锁的线程
    2. 如果不是当前线程,直接返回获取锁失败
    3. 如果就是当前线程获取了锁,通过调研父类AQS提供的setState方法将state+1
  • hasQueuedPredecessors 判断当前节点在队列中是否有前驱节点 ,这也是公平锁的特性;
  • 这里的独占线程设置:
private transient Thread exclusiveOwnerThread;

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

2.2【非公平锁获取锁】tryAcquire

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

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

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

对比于上边的公平锁,非公平锁的区别在于:

  • 如果state=0,表示锁没有被任何线程获取,这时 不再判断当前线程是否是等待队列的第一个节点 ,而是直接去CAS尝试修改state获取锁。

获取锁流程:
在这里插入图片描述

2.3【释放锁】tryRelease

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;
        }
  1. 如果获取锁的线程不是当前线程,则抛出异常
  2. 将getState获取的state-1
  3. 如果此时state=0,表示重入的线程已全部释放:
    1. setExclusiveOwnerThread设置获取锁的线程为null
    2. setState设置state=0
    3. 返回true
  4. 此时state!=0,表示重入的此线程还没释放完:
    1. setState设置state
    2. 返回false

【ReadWriteLock】接口

Synchronized关键字和ReentrantLock都是基于排他锁的实现,也就是同一个时刻只允许一个进程对共享资源进行获取。

对于一种 读多写少 的场景,就需要使用读写锁提升性能。

  • 读写锁,即有两把锁:读锁和写锁
  • 同一个时刻允许多个线程对共享资源进行读操作;
  • 同一个时刻只允许一个线程对共享资源进行写操作;
  • 当进行读操作时,同一时刻的其他写操作会被阻塞;
  • 当进行写操作时,同一时刻的其他读操作会被阻塞;

ReadWriteLock接口方法:

public interface ReadWriteLock {
	Lock readLock();
	Lock writeLock();
}

【ReentrantReadWriteLock】读写锁(ReadWriteLock接口实现类)

ReentrantReadWriteLock使用方式:

ReadWriteLock lock = new ReentrantReadWriteLock();
        private int data = 0;

        // 读操作
        public void read() {
            lock.readLock().lock();
            try {
                System.out.println("read data begin");
                Thread.sleep((long) (Math.random() * 1000));
                System.out.println("read data end." + data);
            } catch (Exception e) {
                //ignore
            } finally {
                lock.readLock().unlock();
            }
        }

        // 写操作
        public void write(int data) {
            // 加写锁
            lock.writeLock().lock();
            try {
                System.out.println("write data begin");
                Thread.sleep((long) (Math.random() * 1000));
                this.data = data;
                System.out.println("write data end." + data);
            } catch (Exception e) {
                //ignore
            } finally {
                lock.writeLock().unlock();
            }
        }

存储结构与构造函数

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;
}

ReentrantReadWriteLock有两个内部类:ReadLock和WriteLock:

 public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
}

 public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;
}

同样的,ReentrantReadWriteLock的构造函数也区分公平锁与非公平锁(默认是非公平):

public ReentrantReadWriteLock() {
        this(false);
    }
    
public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

特点:

  • 支持 公平锁、非公平锁
  • 可重入 :读线程可重复获取读锁、写线程可再次获取读锁或写锁;
  • 锁降级 :允许从写锁降级为读锁,即写线程可以获取读锁,然后释放写锁;

读写锁实现原理【state按位拆分】

在AQS中是通过一个int类型的state来表示锁的,但是在ReentrantReadWriteLock中,需要使用一个state来表示两个锁。实现的方式即:按位拆分。

  • 将int类型的state拆分为高16位和低16位
  • 高16位表示读锁,低16位表示写锁
abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;

        /*
         * Read vs write count extraction constants and functions.
         * Lock state is logically divided into two unsigned shorts:
         * The lower one representing the exclusive (writer) lock hold count,
         * and the upper the shared (reader) hold count.
         */

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}

方法中的参数c即通过getState获取的state值:

  • sharedCount方法返回值即读锁数量
  • exclusiveCount方法返回值即写锁数量

1.【写锁获取锁】tryAcquire

public void lock() {
            sync.acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
  1. 首先根据AQS提供的getState方法获取资源状态
  2. 如果state!=0,表示资源已经被某个线程获取了:
    1. 首先 exclusiveCount, 计算写锁数量
    2. 如果写锁为0,(那么此时一定是读锁),当进行读操作时,写锁获取会失败,返回false;
    3. 如果写锁!=0,那么判断是否是当前线程持有写锁,如果不是,返回false;
    4. 如果是当前线程持有写锁,那么判断写锁数量是否超限(因为是可重入的)
    5. setState设置写锁状态
  3. 如果state = 0,表示当前资源没有锁:
    1. writerShouldBlock判断当前线程是否应该阻塞,公平锁与非公平锁的实现不同。
      • 公平锁:hasQueuedPredecessors判断等待队列中是否有其他线程排在前面
      • 非公平锁:直接返回false,表示不需要排队
        与ReentrantLock中的公平非公平锁逻辑相同
    2. compareAndSetState修改state同步变量,表示获取锁成功;
    3. setExclusiveOwnerThread设置获取锁的线程为当前线程;

写锁获取流程:
在这里插入图片描述

2.【写锁释放锁】tryRelease

public void unlock() {
            sync.release(1);
        }
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
  1. 判断当前线程是否持有锁,如果没有,抛出异常
  2. getState获取state值,并-release, 计算写锁数量
    1. 如果写锁数量=0,设置持有锁线程为null
  3. setState设置锁状态

3.【读锁获取锁】tryAcquireShared

public void lock() {
            sync.acquireShared(1);
        }
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                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 1;
            }
            return fullTryAcquireShared(current);
        }
  1. getState获取state值,然后exclusiveCount 获取写锁状态
    1. 如果写锁!=0,且持有锁线程不为当前线程,那么返回false;
    2. 如果写锁!=0,且就是当前线程持有写锁,向下执行(写线程可以获取读锁);
  2. readerShouldBlock判断读锁是否需要排队(与写锁的判断一样的)
    • 公平锁:hasQueuedPredecessors判断等待队列中是否有其他线程排在前面
    • 非公平锁:直接返回false,表示不需要排队
  3. sharedCount 获取读锁数量,判断是否超出最大值,且compareAndSetState尝试修改状态,CAS成功:
    1. 如果读锁数量=0,那么将当前线程设为firstReader;
    2. 如果读锁!=0,且 firstReader就是当前线程,那么将firstReaderHoldCount++;
    3. 如果读锁!=0,且firstReader不是当前线程,通过ThreadLocal获取当前线程的获取读锁次数,并保存到readHolds;
  4. 如果2、或3的条件不满足,则进入fullTryAcquireShared 循环重试 获取锁
final int fullTryAcquireShared(Thread current) {
            /*
             * This code is in part redundant with that in
             * tryAcquireShared but is simpler overall by not
             * complicating tryAcquireShared with interactions between
             * retries and lazily reading hold counts.
             */
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

读锁获取流程:
在这里插入图片描述

4.【读锁释放锁】tryReleaseShared

public void unlock() {
            sync.releaseShared(1);
        }
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }
  1. 将当前线程持有读锁数量-1,分为是否firstReader两种情况;
  2. 循环尝试compareAndSetState设置读锁状态,返回释放后的读锁数量是否为0;

总结Q&A

  • 为什么在获取锁的流程中,有时是通过compareAndSetState设置state值,有时是直接通过setState设置?
    如当获取写锁时,如果当前锁没有被任何线程获取,那么此时要通过CAS设置state状态,因为可能会有多个线程抢占;
    而如果锁已被当前线程获取,那么其他线程将不会有机会抢占,所以直接setState设置写锁值+1;
  • 为什么写锁加锁的CAS失败直接返回false,而读锁加锁会进入循环CAS尝试?
    一个线程通过CAS对写锁加锁失败,则另外的线程获取了锁,这个线程将不能再获取到写锁;
    但是一个线程获取到读锁,另一个线程可以继续获取读锁,所以可以通过循环尝试直到成功;
  • 写锁可以降级为读锁,而读锁不可以升级为写锁
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值