前言
对于阅读源码来说,能够提高自己的理解里,根据源码逆推代码的功能和目的。对于理解项目需求来说,能够更加的快速。
阅读源码,也能够提高自己的见识,对设计模式有更加深刻的体会。
第三点,也是最终要的一点,阅读源码,得带着问题去阅读,首先罗列自己的几点问题,然后再去理解。这点对于纯粹看源码来说要有效率多了。一开始本人阅读时,就没有一个问题,结果读的发怵了,产生了一种对编程极其厌烦的地步,在工位上就做不住,知道今天才想通了一点。最近阅读了两个锁对象,重入锁,读写锁。先拿重入锁开刀。
1、重入锁时怎样实现重入的
2、公平锁与非公平锁的区别
3、ReentranLock和AQS的关系
3、加锁和放锁的原理。
先对这四个问题进行猜想:
解决重入应该是这样:对于获得锁的线程,重新进入加锁的代码区域时,不需要获得锁,但是计数器得加1。每次释放锁的时候计数器减1。减完了,这个线程才释放锁。
第2个根据了解,公平锁获取时,必须是等待最长的一个线程,也就是说等待获得锁的线程必须排队,排在最前面的线获得锁。
非公平锁获得锁时,不用排队,能抢到锁就获得锁。
3、AQS和ReentranLock有模板方法设计模式的关系,那么我们就注重AQS与AQS子类的关系。
4、猜想加锁是对一个变量进行判断,如果成功了,就获得锁,否则就获得锁失败。
应用
先来看看,如何应用重入锁。
Lock lock = new ReentrantLock();
public void methodA() {
lock.lock();
System.out.println("methodA");
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
lock.unlock();
}
在应用来,对于重入锁的代码,有以下几步
a)实例化锁对象
b) lock方法,获得锁。防止其他线程进入该代码块。
c) unlock方法,释放锁。其他线程可以进入。
对于a来说看源码,从源码中,我们可以看到java默认是非公平锁,如果想要公平锁的话,加上参数fair为true即可。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
再看lock方法
在源码里重入锁实际上是调用的sync的lock方法,sync类是一个内部类,它继承了AQS类。而sync的lock方法呢,它是个抽象方法,具体的实现交给了FairSync类和NonfairSync。对于AQS类和Sync类,以及Sync的子类,从这里呢,我们可以看到一个模板方法模式,把具体变化的部分交给子类去实现。属于不会变化的一部分由父类实现。具体分析写到下面,先解决我们lock获得锁的实现。先看下公平锁的实现
//sync子类的方法
final void lock() {
acquire(1);
}
//AQS的acquire方法,但是tryAcquire是个抽象方法,具体实现由子类完成
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上图代码的语义是:如果获取锁失败,就把当前线程变为一个节点添加到等待队列里,然后再调用 acquireQueued方法进入获得锁阻塞等待状态。
对上面的三个方法:tryAcquire和addWaiter还有acquireQueued方法,第一个属于Sync类,其实Sync类的tryAcquire也是一个抽象方法,具体实现交给了Sync的子类。其余两个属于AQS类。到这里我们就可以清楚的看到AQS和Sync类是一个模板方法模式的应用。在《多线程并发编程的艺术》一书中,有说过利用AQS创建属于自己的同步器组件,其实tryAcquire就是创建同步器组件的关键,对于锁的实现来说,最终重要的三点:加锁,加锁不成功放入等待队列,放锁,放锁成功通知等待队列里的线程。其中
加锁不成功AQS帮我们完成了,放锁不成功AQS也帮我们完成了。所以我们实现加锁和放锁的方法就能够创建出一个同步器组件。
说到这里,来看看重入锁的公平锁对象的获得锁的策略
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;
}
}
1、判断计数器是否为0,如果为0表示没有线程获得锁,那么直接获取锁,直接进行cas操作,修改state的值。如果是线程重入的情况的话,那么直接记性计数器加accquires操作。注意前面还有一个hasQueuedPredecessors方法。来看看这个方法
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法大意就是说头结点和尾节点相同时,即第一个获得锁时,头结点和尾节点相同都为null,或者等待队列里没有节点时,头结点和尾节点是同一个节点。返回false,另一个就是头结点有后继节点,即有线程在等待队列里。
这两种情况分别说明了,公平锁实现获锁策略时,第一次获得锁时,可以直接获取锁对象,但是一旦出现第二个线程获得锁时,要想获得锁,必须进入等待队列里。
再来看看非公平锁的策略:
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;
}
这个方法说明了,进入lock方法的线程,将会直接进行CAS操作来获取锁。tryAcquire方法调用的是nonfairTryAcquire方法,而且也缺少了hasQueuedPredecessors方法的判断,这就说明了,采用非公平锁策略的线程,进入lock方法时,直接尝试获取锁,是一种抢占式获得锁。
到这里我们基本上解决了我们最开始的问题,state变量来标识获得锁的状态,如果重入的话,state进行加1操作。tryAcquire方法说明了AQS和Sync类是一种模板方法的实现。对于加锁的实现也基本上有了了解。最后看下放锁的步骤
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) {
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;
}
对于上面的步骤,无非就是对state变量进行减1操作,如果state减为0了,那么久释放重入锁当前线程,置空,等待GC。在这里我们要注意的是,公平锁和非公平锁的放锁方法是一样的。都是Sync类的放锁策略。所以公平锁和费公平锁的区别即使线程刚进入获锁策略的那段代码。如果线程一旦进入等待队列里,那么就无所谓公平锁和非公平锁了。unparkSuccessor方法在AQS里实现,会释放等待队列里的头结点方法。到这里放锁的大概过程,我们也了解了。
最后说一个特别有意思的一个事情 ,就是在唤醒头结点时,是利用尾节点从后往前遍历获得头结点,而不是利用head直接获取后继节点。有人说在往等待队列里添加节点时,刚设置完尾节点,但是pred.next还没有设置,如果此时通过head.next获得节点时会得到null。而利用尾节点的tail.prev将会得到node节点,有节点而获得null的情况,看官你怎么看呢?