1.读写锁ReadWriteLock
读写锁有以下特点:1.读锁之间不互斥,2.写锁之间互斥,只能同时有一个写锁进行写操作,3.写锁优先,唤醒线程时优先唤醒写锁。jdk中的ReadWriteLock就是读写锁,ReentrantReadWriteLock是ReadWriteLock接口的一个实现类。
ReentrantReadWriteLock除了读写锁的特性以外还有以下的特点:1.支持公平锁和非公平锁,2.可重入,读锁可以再次获取读锁,写锁可以再次获得写锁,3.支持锁降级,写锁可以获取读锁(不支持读锁升级为写锁,4.支持响应中断的方式加锁,5.支持Condition
写锁和读锁之间互斥:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyReadWriteLock {
public static void main(String[] args) throws InterruptedException {
final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//默认为非公平锁 构造方法中可以增加布尔型参数true为公平锁,false为非公平锁
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.writeLock().lock();
System.out.println("线程1获取写锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1释放写锁");
readWriteLock.writeLock().unlock();
}
}).start();
Thread.sleep(1000);
readWriteLock.readLock().lock();
System.out.println("线程2获取读锁");
System.out.println("线程2释放读锁");
readWriteLock.readLock().unlock();
}
}
剩余三种读锁和读锁不互斥、写锁和写锁互斥、读锁和写锁互斥不再举例。
锁降级
在获取写锁的时候可以获取读锁,然后将写锁释放,这样就降级成了读锁
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyReadWriteLock2 {
public static void main(String[] args) throws InterruptedException {
final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
System.out.println("获取写锁");
readWriteLock.writeLock().lock();
System.out.println("获取读锁");
readWriteLock.readLock().lock();
System.out.println("释放写锁");
readWriteLock.writeLock().unlock();
}
}
获取读锁后不能再获取写锁,所以不支持读锁升级为写锁。
ReadWriteLock继承了Lock,Condition和可响应中断的加锁与Lock一致。
2.AQS(AbstractQueuedSynchronizer)
AQS中维护了一个先进先出(FIFO)队列,线程要获取锁会被加入到队列中,直到获取锁之后被移除。
AQS中的state是一个很重要的变量,我个人理解它代表的是独占方式和共享方式分别占用的资源数。AQS有两种资源获取方式:Exclusive(独占方式)和Shared(共享方式)。独占方式获取资源的方法时acquire(int arg),共享方式获取资源的方法是acquireShared(int args),先看独占模式的源码:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法中先尝试获取锁tryAcquire(arg),如果不成功将当前线程加入尝试获取锁的队列。AQS中没有实现tryAcquire方法,而是将它交给子类来实现。这里以ReentrantReadWriteLock中的内部类Sync为例,看tryAcquire是怎么实现的。Sync继承了AQS。源码如下:
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;
}
首先拿到AQS的state,执行exclusiveCount(c)并赋值给w,exclusiveCount是计算state中的资源被独占的数量,在Sync中是这样实现获取state中代表的独占和共享资源的数量的:
/*
* 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; }
在Sync中,state的高16位代表着共享资源被占用的数量,低16位代表独占资源被占用的数量。SHARED_SHIFT是常量16,EXCLUSIVE_MASK是1带符号左移16位减一,1带符号左移16位就是第十七位为1其余为0的正数,再减一就是后16位为1的正数。exclusiveCount方法是将state与EXCLUSIVE_MASK做逻辑与运算,EXCLUSIVE_MASK高16位为0,那么结果的高16位就是0,EXCLUSIVE_MASK低16位为1,那么结果的低16位就是state的低16位,也就是这个方法将state的高16位置0,取的是低16位代表的独占资源被占用的数量。sharedCount是将state无符号右移16位,取的就是state的高16位代表的共享资源被占用的数量。
接着看Sync中的tryAcquire方法,如果state不为0,并且w为0,那么就返回false,因为w是state的后16位,w为0,state不为0,那么state的前16位就不为0,也就是读写锁的读锁被占用,那么获取写锁就无法获取的实现原理。如果state不为0,w不为0,并且获取独占资源的线程不是当前线程,也会返回false,这里判断是否当前线程就是为了可重入,如果tryAcquire的数量和w加起来大于MAX_COUNT(MAX_COUNT在上面的代码中表示2的16次方减一,资源的总数),超出了资源数量会抛出异常,没有超出并且还是同一个线程获取的写锁,那么就将state的数量加上tryAcquire中参数的数量,并返回true,这就是读写锁中写锁之间互斥的实现原理。如果c等于0,说明当前没有线程获取了资源,那么将state再与c比较一次(防止这时候state改变了)并将state值设置为c+acquires。最后将资源的线程拥有者设置为当前线程(为了之后判断可重入)。
再回到AQS中的acquire方法,如果tryAcquire获取成功了,那么就结束,没有获取成功,会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先看addWaiter方法:
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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;
}
这个方法先以当前线程创建一个Node对象。Node是AQS中的一个内部类,Node类中有两个Node类型的成员变量prev和next,分别是Node的前一个节点和后一个节点。AQS中有head和tail两个Node类型的成员变量,分别是头节点和尾节点。由Node的结构和AQS中定义的head和tail使AQS中形成了一个双向的链表。这个双向链表就是AQS中等待获取资源的线程队列。
在addWaiter方法中,获取当前AQS的tail,如果不为空,将这个Node设为tail并设置原来tail的下一个节点和当前Node的前一个节点,并返回新的尾节点。如果AQS的尾节点为空,那么执行enq(node)方法,enq方法是将节点插入到队列中,如果尾节点为空就初始化这个队列。
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter方法就是将当前线程的Node塞到AQS的队列的尾部并返回这个尾节点。在acquire方法中将addWaiter方法的返回值作为参数执行acquireQueued方法:
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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);
}
}
在acquireQueued中首先获取当前节点的前一个节点,如果不是头节点就循环一直获取并判断是不是头节点。如果是头节点就尝试获取锁,获取失败就还是一直循环,直到获取成功之后将当前节点设置为头节点,也就是说只有在当前节点的前一个是头节点并且头节点已经释放锁才能获取到资源。因此,AQS保证了是从头节点到尾节点一个个地获取到资源的,也就是说AQS是一个先进先出(FIFO)的双向链表。
至此AQS的acquire方法就已经看完了。总结来说acquire方法就是获取锁,并将当前线程构造的Node添加到AQS队列的尾部。在ReentrantReadWriteLock中,就是使用继承了AQS的Sync类来实现读写锁的加锁。
下面我们看release方法,这是用来释放资源的方法,也就是锁用来释放锁的实现原理:
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
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尝试释放锁,tryRelease方法也和tryAcquire方法一样交由子类实现,这里不再举例说明。如果尝试获取成功就将当前的头节点执行unparkSuccessor方法,,在unparkSuccessor方法中如果当前node的下一个node为空,就从尾部开始找没有被取消的node,并将这个node锁住。
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
当前节点释放锁之后,AQS中的队列中的node一直在循环获取锁,其中头节点的下个节点就能获取到。
共享方式获取和释放资源分别是tryRelease和tryAcquire方法,实现也是类似,不再做介绍。
3.自旋锁
一般来说,获取锁的时候如果锁已经被占用,那么有两种方式来一直等待锁释放,一种是阻塞等待唤醒,一种是不断循环地判断是否可以获取锁,第二种就是自旋锁。
自旋锁的优点是不需要由阻塞状态切换到活跃状态,锁的效率高,切换快。缺点是占用cpu多,消耗大。本文中的acquireQueued方法、enq方法都是通过自旋锁实现的。