文章目录
LockSupport工具类的常用方法及作用
该类的主要作用是挂起和唤醒线程,该工具类是创建锁和其它同步类的基础。
void park()
- 介绍:调用park方法的线程如果已经拿到与LockSupport关联的许可证,则调用LockSupport.park()方法会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。
- 调用park方法时最好使用循环条件判断方式,因为当其他线程以当前线程做为unpark方法的参数,或者其它线程调用了interrupted方法,当前阻塞线程都会返回。
- park()方法阻塞的线程被其它线程中断不会抛出中断异常
例子:可以发现在主线程中调用LockSupport.part()方法后,主线程受到了阻塞。
void unpark(Thread thread)方法
- 介绍:给传入的线程 颁发LockSupport的许可证
- 如果传入的线程没有许可证,则给他一个许可证,如果它还处于阻塞线程状态,则唤醒它。
- 拥有了许可证的线程,在执行park() 方法之后,会立刻返回,不会阻塞了。
void parkNanos(long nanos)方法
这个与park方法类似,区别在于这个可以传入时间,表示从当前时间开始计算,在nanos时间后,即时阻塞也可以被返回。
void park(Object blocker)方法(推荐使用,可查阻塞线程)
当线程调用这个park方法,因没有许可证而被阻塞时,这个blocker会被记录到该线程内部。这个时候,使用诊断工具可以观察线程被阻塞的原因。
blocker可以被设置为this
jstack pid 命令查看线程堆栈时可以看到更多有关阻塞对象this的信息。
void parkUntil(Object blocker,long deadline)
与parkNanos方法不同的是,等到指定时间点,返回阻塞线程
那么wait和notify呢?与park,unpark的比较
wait和notify是Object的方法,park和unpark是JUC.locks包下的方法
wait和notify需要配合同步代码块使用,必须要先获取监视锁,但是park和unpark方法并不需要,可以在任何地方使用。
wait和notify方法需要严格控制执行顺序。
park支持超时等待立刻避免阻塞,wait的超时等待只能够重新再去竞争锁。
unpark支持唤醒指定线程,notify不支持。
锁的底层支持–AQS(抽象同步队列)
- AQS是一个双端队列,维持了一个单一的状态的信息state。(线程的同步的关键是对state的操作)对于可重入锁ReentrantLock来说,可以表示获取锁的可重入次数。对于读写锁ReantrantReadWriteLock来说,state的高16位表示获取读锁的次数,低16位表示获取到写锁的可重入次数。对于Semaphore来说,表示信号量。对于CountDownlatch来说,用来表示计数器当前的值
- 队列元素是一个Node,该Node标记了该线程获取的共享资源还是独占资源,同时该Node中还记录了当前线程的等待状态waitStatus,是被取消了,还是需要被唤醒,还是在队列里等待,还是释放资源需要通知其他节点。
- AQS内部还维护了一个内部类,用来结合锁实现线程同步。可以由该类,直接访问AQS对象的内部的变量
独占锁ReentrantLock的介绍
类图结构
结合 ReentrantLock() 的构造器
public ReentrantLock() {
sync = new NonfairSync();
}
可以发现,我们创建ReentrantLock锁,最终创建的还是一个继承AQS的Sync类的子类。即最终还是通过AQS来实现的同步操作。
在ReentrantLock中,AQS对于它来说,里面的state就代表了锁的可重入次数。默认情况下,state值为0,表示当前没有被任何线程持有,当第一个线程第一次获取锁时,会使用CAS将state设置为1,如果在未释放锁的情况下,再次获取锁,则将state再次+1变为2,如果释放锁,则-1,如果最后状态值为0,则表示当前线程释放该锁。
void lock()方法
可以发现lock的方法交给了sync去实现,而sync则交给了其两个子类的lock实现方法。一个是公平锁,一个是非公平锁。
先来看一下非公平锁的lock实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//cas判断,如果当前AQS中的state状态为0,则将其改为1
if (compareAndSetState(0, 1))
//并将当前的线程,设置为锁的持有者
setExclusiveOwnerThread(Thread.currentThread());
else
//如果当前线程想要获取锁,获取不到,则会执行acquire方法
//该方法会去执行tryAcquire(),然后可能就将线程放入阻塞队列
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
由于tryAcquire,AQS并没有提供这个方法,所以ReentrantLock重写了这个方法。下面是非公平类重写的这个非公平方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取AQS的状态值
int c = getState();
if (c == 0) {
//如果状态值为0,则同样是进行一个cas设置state的操作
//让当前线程成为锁的拥有者,并把状态值改为1
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果发现状态值不为0,即该锁被别的线程持有着
//则返回false,然后该线程就在acquire的代码中被放入阻塞队列了
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//先判断当前的可重入次数,如果小于0,则抛出异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
接下来看看公平锁的类的实现
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
//直接调用的acquire方法,然后就是公平类重写的tryAcquire方法
acquire(1);
}
下面是公平类里重写的tryAcquire方法。
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;
}
ReentrantReadWriteLock的介绍
ReentrantLock是独占锁,只能由一个线程占用一个锁,适用于写多读少的场景。但实际情况中,总有需要用到读多写少的场景,所以ReentrantReadWriteLock应运而生。它采用读写分离的策略,使得多个线程可以同时获取锁。
类图结构
由上述类图结构,可以发现,读写锁内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。但AQS中只维护了一个state状态,而读写锁需要维护两个状态(读和写)那么一个state怎么表示呢?利用state的高16位表示读状态(即获取到读锁的次数),低16位表示写状态(即获取到写锁的可重入次数)。