玩转高并发系列----锁(二)

这一章,我将结合JDK源码详细介绍如何通过AQS实现了读写锁以及如何通过Unsafe的park()unpark()方法并结合条件队列实现Condition。

读写锁(ReentrantReadWriteLock)

1 读写锁的基本用法:

首先我们看一看读写的基本用法:

  1. 顾名思义,读写锁,就是能够保证读写分离,在高并发情况下,做到读读不互斥,读写互斥,写写互斥,从而提高并发量。
  2. 读写锁也是基于AQS实现的,通过AQS内部的state变量,同时记录的读线程的个数和写线程的个数(因为是可重入的,对于写线程的,state的值也可以大于1)
  3. 读写锁看似两个锁,其实只有一个锁。内部争用的都是同一个state变量。并且是基于同一个锁生成的writeLockreadLock才具有读写锁的意义。
	private static class SharedTask {
        private int value;
        private final ReadWriteLock lock;
        private final Lock writeLock;
        private final Lock readLock;

        private SharedTask(int value) {
            this.value = value;
            // new 一个读写锁
            this.lock = new ReentrantReadWriteLock();
            // 获取写锁
            this.writeLock = lock.writeLock();
            // 获取读锁
            this.readLock = lock.readLock();
        }
        
        public void write(int num){
            try {
            	// 写方法,获取写锁
                writeLock.lock();
                value += num;
            } finally {
                writeLock.unlock();
            }
        }
        
        public int read(){
            try {
            	// 读方法 获取读锁
                readLock.lock();
                return value;
            }finally {
                readLock.unlock();
            }
        }
    }
  1. ReentrantReadWriteLock读写锁的类继承层次关系
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    /** 内部类:写锁的实现,继承自Lock接口*/
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** 内部类:读锁的实现,继承自Lock接口*/
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** 内部类:AQS的实现,继承自AbstractQueuedSynchronizer接口,是实现整个读写锁的具体对象:
    有两个内部类实现,公平锁(FairSync)和非公平锁(NonfairSync)*/
    final Sync sync;
    ......
}

在这里插入图片描述
3. 读写锁的实现原理

  1. 上文说道,ReadLock和WriteLock从表面上看,是两把不同的锁,但实际上它只是同一个锁的两个不同视图而已,即底层通过CAS争抢的同一个共享变量state。
  2. 读写锁的实现中,state变量被拆成两部分:
  • 高16位:用于“读”锁,例如:高16位的值等于5,表示有5个读线程都拿到了该读锁,或者同一个读线程获取了5次读锁(可重入锁)
  • 低16位:用于“写”锁,例如:低16位的值等于5,表示一个写线程获取了5次写锁(可重入锁),但是不可能存在5个写线程获取到写锁的情况(写写是互斥的
  1. 当state==0时,说明没有线程占用锁,即没有读锁也没有写锁。当state!=0时,要么有读线程,要么有写线程持有锁,两者不可能同时成立,因为读写互斥。

为什么要把int变量state拆成两部分,而不是用两个int类型分别表示读锁和写锁的状态呢?

因为底层通过CAS实现,而无法通过一次CAS同时操作两个int变量,所以用来一个int型的高16位和低16位分别表示读锁和写锁的状态。

3.1. 首先我们来看读写锁的实现。

写锁是基于 AQS中的 acquire/release模板方法来实现lock/unlock的
读锁是基于AQS中的 acquireShared/releaseShared模板方法来实现lock/unlock的

// 读锁的实现
public static class ReadLock implements Lock, java.io.Serializable {
   ......
   // 具体获取锁和释放锁的类:继承自AQS,由创建读写锁时传入:有公平和非公平两种不同实现
   private final Sync sync;
   protected ReadLock(ReentrantReadWriteLock lock) {
       sync = lock.sync;
   }
   public void lock() {
   // 通过AQS中的acquireShared方法获取锁
       sync.acquireShared(1);
   }
   public void unlock() {
   // 通过AQS中的releaseShared方法获取锁
       sync.releaseShared(1);
   }
   ......
}

//写锁的实现
public static class WriteLock implements Lock, java.io.Serializable {
    ......
    // 同上
    private final Sync sync;
	protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
    }
    public void lock() {
    // 通过AQS中的acquire方法获取锁
        sync.acquire(1);
    }
    public void unlock() {
    // 通过AQS中的release方法获取锁
       sync.release(1);
    }
	......
}

3.2. AQS中的四种模板方法:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    // 获取写锁和互斥锁
    public final void acquire(int arg) {
    // tryAcquire()方法由子类Sync提供实现
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    // 释放写锁或互斥锁
    public final boolean release(int arg) {
    // tryRelease()方法由子类Sync提供实现
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // 获取读锁,即可分享的锁
    public final void acquireShared(int arg) {
    // tryAcquireShared()方法由子类Sync提供实现
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    // 释放读锁,即可分享的锁
    public final boolean releaseShared(int arg) {
    // tryReleaseShared()方法由子类Sync提供实现
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    ......
}

Sync有两个实现类:公平锁和非公平锁,因此将读/写,公平/非公平进行排列组合,便可以得到四种组合实现:

  1. 读锁的公平实现
  2. 读锁的非公平实现
  3. 写锁的公平实现
  4. 写锁的非公平实现

以上四种排列组合,通过Sync的两个子类FairSync和NonFairSync分别实现

// 非公平锁实现
static final class NonfairSync extends Sync {
	// 写线程在抢锁时,是否应该阻塞:非公平,所以直接返回false
    final boolean writerShouldBlock() {
       return false; // writers can always barge
    }
    // 读线程在抢锁时,是否应该阻塞:非公平,所以判断当前阻塞队列的头结点是否为写线程,若为写线程,则不抢锁,进入阻塞状态。
    // 为解决写线程一直处于“饥饿”状态的现象:
    // 即当前持有锁的是读线程,如果读线程也能一直抢锁,则可能导致写线程永远拿不到锁。在读并发远远大于写并发时,很有可能出现。
    final boolean readerShouldBlock() {
       return apparentlyFirstQueuedIsExclusive();
    }
}
// 公平锁的实现
static final class FairSync extends Sync {
// 写线程在抢锁时,是否应该阻塞:公平,所以需要判断当前阻塞队列是否有处于等待的节点
   final boolean writerShouldBlock() {
      return hasQueuedPredecessors();
   }
   // 读线程在抢锁时,是否应该阻塞:公平,所以需要判断当前阻塞队列是否有处于等待的节点
   final boolean readerShouldBlock() {
      return hasQueuedPredecessors();
   }
}

3.3 Sync抽象类对AQS中的tryAcquire/tryRelease方法的实现,即写锁(互斥锁)的实现。一行一行在代码中解读

// 写锁的获取流程
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    
    // 获取当前状态:即是否有读线程或者写线程获得锁
    // c==0; 表示没有线程获取锁
    // c!=0; 表示有读线程或者写线程获取锁(前面讲过,state变量被拆成高16位和低16位分别表示读锁和写锁的状态)
    int c = getState();
    
    // 通过位运算,获取写锁的状态
    // w==0; 表示没有写线程
    // w!=0; 表示有写线程,由于锁可重入,因此w可以大于1
    int w = exclusiveCount(c);
	
	// 有读线程或者写线程获取锁 进入if语句
    if (c != 0) {
       // w==0; 表示没有写线程,而c!=0;表示当前所持有线程,所以当前占有锁的一定是读线程,
       // 而执行tryAcquire方法的是写线程,所以根据读写互斥原则,该线程需进入阻塞队列。返回false
       // w!=0则说明持有锁的是写线程,而current != getExclusiveOwnerThread();即当前线程不是持有锁的线程:
       // 根据写写互斥原则,该线程需进入阻塞队列。返回false
       if (w == 0 || current != getExclusiveOwnerThread())
           return false;
       // 执行到这里,说明持有锁的线程,就是当前执行tryAcquire的写线程。即w!=0&&current===getExclusiveOwnerThread()
       // 由于锁可重入,因此该线程可以重复获取锁,但需要校验重入次数不能超过最大值(即低16位所能表示的最大正数:65535)
       if (w + exclusiveCount(acquires) > MAX_COUNT)
           throw new Error("Maximum lock count exceeded");
           
       // 当前写线程获取到锁,直接更新state变量
       // 执行到这里,说明当前线程一定是持有锁的线程,所以更新state变量是安全的,可以直接更新
       setState(c + acquires);
       // 成功获取到锁,返回true
       // 注意:此处获取到锁,则说明一定是重入锁,所以无需设置持有锁的线程为当前线程,
       // 即执行setExclusiveOwnerThread(current);
       return true;
    }
    // writerShouldBlock(); 判断当前写线程是否应该阻塞,是则返回false,进入阻塞队列,否则通过CAS争抢锁(state变量)
    // compareAndSetState(c, c + acquires);争抢锁,抢锁成功,则设置持有锁的线程为当前线程,否则返回false,进入阻塞队列
    if (writerShouldBlock() ||
           !compareAndSetState(c, c + acquires))
       return false;
    // 设置持有锁的线程为当前线程
    setExclusiveOwnerThread(current);
    return true;
}
....
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 写锁的释放流程:tryRelease
protected final boolean tryRelease(int releases) {
	// 判断持有锁的线程是否为当前线程,不是,则直接抛出异常,只有自己才能释放自己占用的锁
   if (!isHeldExclusively())
      throw new IllegalMonitorStateException();
   
   // 获取释放当前锁之后,state变量的值  
   int nextc = getState() - releases;
   // 校验是否当前锁只有,state变量的值。
   // 如果低16的值等于0,表示没有线程持有该锁,将持有锁的线程设置为null: setExclusiveOwnerThread(null);
   // 如果低16位的值不等于0,则表示当前线程仍然有重入的锁,因此不更改持有锁的线程为null
   boolean free = exclusiveCount(nextc) == 0;
   if (free)
      setExclusiveOwnerThread(null);
   // 更新state变量的值。
   setState(nextc);
   return free;
}
// 判断持有锁的线程是否为当前线程
protected final boolean isHeldExclusively() {
    return getExclusiveOwnerThread() == Thread.currentThread();
}

3.4 Sync抽象类对AQS中的tryAcquireShared/tryReleaseShared方法的实现,即读锁(共享锁)的实现。一行一行在代码中解读

// 读锁的获取流程:tryAcquireShared
protected final int tryAcquireShared(int unused) {
   Thread current = Thread.currentThread();
   
   // 获取当前state变量
   int c = getState();
   // exclusiveCount(c) != 0;表示当前持有锁的是写线程。
   // 根据读写互斥原则,持有锁的线程不是当前线程时,是获取不到读锁的。所以直接返回-1.
   // 此处注意:一个写线程获取到WriteLock后,可以再次获取ReadLock
   if (exclusiveCount(c) != 0 &&
       getExclusiveOwnerThread() != current)
       return -1;
       
   // 获取读锁的状态:即state高16位的值
   int r = sharedCount(c);
   
   // readerShouldBlock();读线程是否应该阻塞,公平锁与非公平锁的不同体现之处。
   // r < MAX_COUNT; 获取读锁的线程个数(包括重入次数),要小于最大获取锁的个数(即16位正数,65535)
   // compareAndSetState(c, c + SHARED_UNIT); 直接CAS抢读锁,高16位加1
   if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 进入if语句,说明该线程已获取到读锁。
        // r==0; 说明当前线程是第一个获取到读锁的线程。
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
        // r!=0&&firstReader == current,说明第一个获取锁的线程再次获取到读锁(即重入),记录重入次数
            firstReaderHoldCount++;
        } else {
        // r!=0&&firstReader != current; 说明其他读线程获取到读锁
        // 以下代码用于统计各个获取到读锁的读线程的重入次数
            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;
   }
   // 执行到这里,说明当前读线程没有获取到锁。
   // 通过一个for(;;)循环,自旋,不断的获取读锁,获取锁的逻辑和上面的逻辑差不多,不再过多赘述。读者可自行阅读源码。
   return fullTryAcquireShared(current);
}

static final int SHARED_SHIFT   = 16;
// 无符号右移16位,即表示获取到高16位的值
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

由于读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接减1,而是需要通过一个for循环+CAS操作不断重试。

// 读锁的释放流程:tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
   Thread current = Thread.currentThread();
   // 判断当前线程是否是第一个获取读锁的线程
   if (firstReader == current) {
       // 第一个获取读锁的线程的重入次数等于1,则将firstReader设置为null
       if (firstReaderHoldCount == 1)
           firstReader = null;
       else
           firstReaderHoldCount--;
   } else {
   // 以下操作为更新当前线程的重入次数,如果重入次数小于等于1,则移除该线程
       HoldCounter rh = cachedHoldCounter;
       if (rh == null || rh.tid != getThreadId(current))
           rh = readHolds.get();
       int count = rh.count;
       if (count <= 1) {
       // 如果重入次数小于等于1,则移除该线程
           readHolds.remove();
           if (count <= 0)
               throw unmatchedUnlockException();
        }
        --rh.count;
   }
   
   // 由于会有多个读线程同时持有锁,因此释放时,也会有多个读线程同时释放锁,更新state变量。
   // 因此state变量的更新不是安全,所以通过一个for循环+CAS重试的方式,更新state变量
   for (;;) {
       int c = getState();
       int nextc = c - SHARED_UNIT;
       if (compareAndSetState(c, nextc))
            return nextc == 0;
   }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值