Java中加入了可重入锁的具体实现:ReentrantLock,还有读写锁的实现:ReentrantReadWriteLock。
ReentrantLock的底层实现 ReentrantLock是维护了一个内部类Sync来实现的,这个内部类Sync继承了AQS,实现了不公平的加锁和解锁。并且实现了NonfairSync和FairSync来实现可重入锁的公平性和非公平性。
ReentrantLock实现可重入获取锁和解锁 在ReentrantLock中的Sync中的加锁的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @ReservedStackAccess 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; }
在方法nonfairTryAcquire中,先判断当前的共享资源有没有线程已经加锁,如果没有,使用CAS加锁,返回true,如果已经获得了锁,判断获得锁的线程是不是当前线程,如果是,同步状态+1,否则返回错误。如果在加锁过程中采用了超时加锁,那么具体实现是AQS内的实现方法。
ReentrantLock实现解锁的方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @ReservedStackAccess 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; }
如果该锁被获取了n次,那么前n-1次的释放锁会返回false,只有同步状态完全释放了,才能够返回true。该方法把0作为释放条件,同步状态为0的时候才将占有线程设置为null,返回true,表示释放成功。
ReentrantLock实现公平性加锁和非公平性加锁 在ReentrantLock的实现中,默认的加锁方法就是非公平性的加锁,也就是说,虽然实现了内部类NonfairSync继承了Sync,但是加锁的方法只是调用了Sync中的加锁方法:
1 2 3 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
在公平锁的加锁方法里,重新进行了方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @ReservedStackAccess 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; }
公平锁是按照申请顺序来进行加锁的,也就是说是FIFO顺序,所以在方法里面会先判断有没有前驱节点,如果没有前驱节点,那么这个节点就是头节点,可以进行加锁操作,这也就是通过实现AQS来搭建的好处,可以非常清楚的判断当前队列信息。 公平锁的解锁方式跟非公平锁并无区别。设定一个ReentrantLock是公平锁的实现还是非公平锁的实现,可以通过在构造函数中传入boolean变量来决定。
ReentrantLock中的公平锁和非公平锁对比 在公平锁中,每一个线程都会得到锁,也就是都能得到运行机会,而在非公平锁中,如果一个线程在使用CAS加锁前恰好有一个其它线程得到了锁,那么这个线程将会加锁失败,而如果足够凑巧,那么非公平锁将会存在饥饿现象。 但是非公平锁与公平锁相比,非公平锁在执行过程中线程切换较少,所以系统的开销更小,保证了更大的吞吐量。
ReentrantReadWriteLock的底层实现 ReentrantLock或者synchronized方法都属于独占锁,也就是在同一时刻只能有一个线程持有该锁,在ReentrantReadWriteLock中实现了读写锁,也就是在读状态下,允许多个线程同时获取一个锁。 读写锁改进了在大量数据并发访问少量数据写入情况下的并发性能。 同ReentrantLock一样,在读写锁中通过维护一个私有的Sync的类来实现底层方法的构建。通过实现一个ReadLock和一个WriteLock来实现方法的调用。
ReentrantReadWriteLock实现读写状态 在底层的内部类Sync的实现里,有这么几个变量和方法:
1 2 3 4 5 6 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; static int sharedCount(int c) { return c >>> SHARED_SHIFT; } static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
也就是在ReentrantReadWriteLock中,同步状态是在一个整型变量上通过使用“按位切割使用”来进行读写锁的区分的。在一个32位的变量上,高16位表示的是读状态,低16位表示的是写状态。 读写锁获取读或者写的状态是通过位运算来进行的。假设同步状态为c,c>>>SHARED_SHIFT表示的是读状态(无符号位右移16位),c&EXCLUSIVE_MASK表示的是写状态(抹去高16位)。 所以,在c不等于0时,当写状态等于0 ,读状态大于0,也就是读锁已经被获取。
ReentrantReadWriteLock中读锁和写锁对于获取释放的实现 写锁的获取等同于互斥锁的实现,也就是存在任何获取到锁的线程,这次获取就失败了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @ReservedStackAccess protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
从代码中能够看出,在写锁的实现中,是包括可重入锁的实现的,也就是存在一个写的线程多次获取该所,但是因为按位切割使用的方法,在这里存在一个写锁的最大数量限制,当然,如果发生这种情况,大概系统已经宕机了。 读锁是一个共享锁,当且仅当持有锁的线程持有的是写锁时候,读锁的获取才会失败:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @ReservedStackAccess protected final int tryAcquireShared(int unused) { 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 != LockSupport.getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }
在Java6以后,获取读锁的方法中添加了当前线程获取锁的次数,所以实现复杂了许多。
锁降级 锁降级是一种线程安全的实现方式,是写锁降级成为读锁。降级锁是在拥有写锁的时候,持有写锁,先获取读锁,然后释放写锁的过程。 这个过程中保证了数据的可见性,如果线程A不获取读锁而是直接释放写锁,如果此时有另外一个线程B获取写锁更改数据,那么线程A无法获取线程B中的数据更新,所以需要先持有读锁,然后阻塞另外线程B,知道A释放读锁之后,B才能够获取写锁进行数据更新。
写在文章后:前两次次接触位运算的概念也是在JDK的实现中,一次是在LinkedList扩容的时候,通过位运算确定扩容的大小;另一次是JDK中对于折半查找的实现的时候,通过位运算确定中间量的位置大小。然后这里通过位运算确定读和写的状态,忽然发现JDK实现里面可真是喜欢使用位运算。Java怕不是基于位运算编程...