ReentrantLock
可重入锁:内建锁隐式支持可重入锁,通过自增获取,自减释放的方式来实现可重入。
特点:1)线程获取锁的时候,如果当前线程已经持有锁,直接尝试再次获取
2)由于锁可以被获取N次,所以释放的时候要释放N次之后才能算是真正的释放成功。
源码及分析:
final boolean nonfairTryAcquire(int acquires) {
//拿到当前线程
final Thread current = Thread.currentThread();
//获取当前同步状态
int c = getState();
if (c == 0) {
//当前线程还没有被获取,当前线程尝试CAS获取同步状态
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//此时同步状态不为0,表示已有线程获取到了同步状态;判断持有线程是否为当前线程
else if (current == getExclusiveOwnerThread()) {
//若是当前线程,同步状态再次加一(可重入特性的实现)和monitor机制一样
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//将再次加一后的状态写回内存
setState(nextc);
return true;
}
return false;
}
公平锁和非公平锁:
公平锁(FairLock):锁的获取满足时间上的绝对顺序(请求资源上的绝对顺序)来获取锁,也就是说等待的时间越长越容易获取到锁,等待时间最长的线程一定最先获取到锁。
非公平锁(NonFairLock):锁的获取不一定满足时间上的绝对顺序。Lock方法先进行CAS,不再队列中的线程可能先获取到锁。
二者的区别:tryAcquire方法不同,都是先获取线程状态,公平锁增加了!hasQueuedPredecessors进行判断,当同步队列中存在非空节点,就将这个线程直接封装为Node进入队列进行排队。举个例子:A线程是持有锁线程,B、C、D在队列中处于阻塞,A线程通过unLock方法释放锁release方法唤醒B线程,不在队列中的E、F也要竞争锁,那么这个时候B、E、F三个线程同时竞争这把锁,非公平锁就会让这三个线程进行CAS操作来获取lock;公平锁就不会进行CAS操作,而是让B线程拿到锁,将E、F线程包装为节点尾插入队列,排队等待获取锁。公平锁需要频繁的进行切换,唤醒与阻塞;非公平锁不需要频繁切换,降低了性能的开销,但是有可能会导致部分线程一直无法获取到Lock造成线程饥饿现象。一般没有特定的公平性要求尽量选择非公平锁。
ReentrantLock采用的是非公平锁机制,因为非公平锁的吞吐量大,效率高,速度快。
ReentrantReadWriteLock:可重入读写锁。
先来说说读写者模型,读写锁运行同一时刻可以被多个读线程访问,但是一旦被写线程访问,所有的线程均不能访问,所有的读线程和其他写线程都会被阻塞。所以我们可以看出来读锁是一个共享锁,而写锁是一个独占锁。
ReentrantReadWriteLock写锁的获取也是tryAcquire方法,比ReentrantLock多了一些判断:1.读锁不为0或写锁不为0一定失败,因为写锁是独占锁。2.如果读锁超过了饱和值,那么也一定失败。
获取读锁状态用getState方法,返回值是int;获取写锁状态使用exclusiveCount返回值也是int;State是32位同步状态int型的变量,高十六位表示读线程获取的次数,低十六位是写线程获取的次数。释放:同步状态减去写状态,判断当前状态是否为0,为0就释放写锁,不为0就更新同步状态。
ReentrantReadWriteLock读锁的获取是tryAcquireShared,返回值为获取次数。共享锁如果单独作为一个锁来使用并没有什么意义,一般都是和写锁结合使用来实现读写者模型。
只要当前写线程没有获取锁,并且读线程没有到达最大值就可以获取成功。
释放:同步状态减去读状态,为0就返回true
读写锁的应用场景:缓存【内存就是最大的一个缓存,缓存实际上就是Map】的实现。