显式锁Lock与隐式锁synchronized比较:
- 可重入锁:都是可重入锁
- 可中断锁:隐式锁不可中断,而显示锁可以中断。
- 公平锁:隐式锁不是公平锁,显示锁在new一个对象的时候可以设置是否生成公平锁,如:new ReentrantLock(true);
- 读写锁:显式锁的读写锁,可以做到读读、读写不互斥,只有写写互斥,这样可以提升多线程时的资源使用率。
显式锁的优势:
显示锁的本质是通过双向链表实现的,显式锁是可以人为控制的,并且显式锁可以在读操作很多的情况下,通过使用读写锁来提高并发效率。
例如一个代码块被synchronized关键字修饰,如果有一个线程获取到了对应的锁,而释放锁的操作不是人为控制的,因此在代码执行完毕线程释放锁的时候,可能出现占有锁的线程正在等待IO或者其他原因(比如调用了sleep方法)被阻塞了, 此时就不能将锁释放,这样一次其他想要获取锁的线程只得一直等待获取锁。这样是非常浪费系统资源的。使用显式锁lock,可以通过调用该对象的unlock()方法来实现主动释放锁。 此外synchronized所有的操作 读读、读写、写写都是互斥的,有时候我们只希望写写互斥来提升系统的效率,显式锁ReentrantReadWriteLock就可以实现。
关于普通显式锁ReentrantLock的解析
ReentrantLock是读读、读写、写写都互斥的显式锁,这种锁在同一时间只能有一个线程占有锁资源。
ReentrantLock内部类Sync是显示锁实现同步控制的基础,Sync的基类是AbstractQueuedSynchronizer。在我的另一篇文章中有关于AbstractQueuedSynchronizer的详细解析(AbstractQueuedSynchronizer)。而Sync又是ReentrantLock实现公平锁和非公平锁的基础。
Sync源码解析
Sync使用AQS(AbstractQueuedSynchronizer类的简写)的状态值来表示持有锁的线程的数量。
abstract static class Sync extends AbstractQueuedSynchronizer {
//公平和非公平方式获取锁都必须以这个方法为基础
abstract void lock();
//非公平方式获取锁的方法
//从方法可以看出,非公平的方式获取锁的时候并不在乎线程请求锁的先后顺序,
//只会判断线程请求锁时,锁是否被占有,锁没有被占该线程就可以马上成功拿到锁。
//因此我们可以简单的理解非公平锁的获取完全靠线程的运气-。-。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前AQS的状态,这里表示占有锁的线程数量
int c = getState();
if (c == 0) {
//c等于0表示当前占有锁的线程数为0,这通过CAS方法设置state
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;
}
//如果当前占有锁的线程数不为0,并且占有锁的线程不是当前线程,那么返回false
return false;
}
//释放锁
protected final boolean tryRelease(int releases) {
//c表示当前线程释放锁后,占有锁的线程数量
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//布尔类型的变量free用于占有锁的线程数是否为0
boolean free = false;
if (c == 0) {
//如果为0则表示当前没有线程占有锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//判断当前线程是否是当前锁的独占线程
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
//ConditionObject为AQS中的内部类,是实现条件队列(等待队列)的关键,我将在另一篇文章中做详细的解析
final ConditionObject newCondition() {
return new ConditionObject();
}
//获取当前锁的独占线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
//获取当前占有锁的线程数量
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
//当前锁是否被占有
final boolean isLocked() {
return getState() != 0;
}
//重置锁
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
公平锁的实现源码
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
//公平的方法获取锁,对比下面的代码与nonfairTryAcquire非公平的方式获取锁的方法,可以发现
//公平的方法获取锁,就是在获取锁时利用hasQueuedPredecessors判断当前线程之前有没有阻塞线程,没有的阻塞线程,当前线程才能获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/** hasQueuedPredecessors是AbstractQueuedSynchronizer中的方法,
* 当前线程为等待锁的线程同步队列头节点或者线程同步队列为空,则返回false
* 注意:我这里说等待锁的线程同步队列的头结点,而不是直接说AQS中的head节点
* 通过AQS我们知道,获取锁的线程会构成一个节点链表形式的同步队列并且阻塞线程
* 而在AQS中head节点代表整个链表的头结点,head节点为占有锁的线程构成的节点,
* head节点之后的节点都是没有获取到锁的线程构成的阻塞节点,
* 因此,等待锁的线程同步队列的头结点应该为head.next,即head节点的后继节点
* 经过上面的分析,我们可以看出,hasQueuedPredecessors方法为false的两个条件:
* 1.线程同步队列为空,即head=tail,头节点和末节点相同.
* 在AQS中enq方法源码可以看出head=tail时,同步队列为空,此时head与tail都为一个没有实际意义的节点
* 2.当前线程为head.next节点中存放的线程
*/
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;
}
}
非公平锁的实现源码
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
可以看出非公平锁实际上就是在tryAcquire方法中调用了nonfairTryAcquire方法获取锁。
关于ReentrantLock中lock方法和tryLock方法的不同
1.先看lock方法的源码
public void lock() {
sync.lock();
}
sync是ReentrantLock定义的内部类Sync类型的变量,经过追溯源码发现,不论是NonfairSync中的lock方法还是FairSync的lock方法,最终都是调用了父类AQS中的acquire方法(这里采用了模板方法模式,AQS为模板类),AQS中会将未获取到锁的线程包装成阻塞节点。因此,在普通锁ReetrantLock在通过lock方法尝试获取锁失败后,线程会进入同步队列并阻塞。
2.再看tryLock方法的源码
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
在上面我们分析了ReentrantLock的内部类Sync,可以看出,nofairTryAcquire就是Sync自己内部定义的一个方法,并不是重写AQS的方法,因此nofairTryAcquire执行的结果只有true获取到锁或者false没有获取到锁两种结果,没有获取到锁的时候并不会阻塞线程而是直接返回false结果。
总结
显示锁中对于锁的控制主要还是在AbstractQueuedSynchronizer类中。经过上面的分析,公平锁与非公平锁简而言之,公平锁是所有线程都公平的获取锁的方式,线程拿到锁的顺序会根据其请求获取锁时的先后顺序,先请求能够优先占有锁。非公平锁有带有一定的随机性,如果当前线程在获取锁时,刚好遇见锁被另一线程释放,那么该线程就能获取到锁,并不会优先考虑正在等待锁的阻塞线程。