java中JUC工具中提供了ReentrantLock实现灵活的加锁操作。
首先查下累的继承关系图,ReentrantLock实现跟图中的类息息相关

重入锁
允许相同线程获取锁后再次获得锁,不会阻塞线程,只需要记录获得锁的次数。
synchronized和reentrantLock都是可重入锁,注意ReentrantLock获取了N次锁同样需要释放N次锁。
ReentrantLock中的AQS(AbstractQueuedSynchronizer)
AQS是jvm中提供的同步工具,实现锁的独占与共享。
在AQS中维护了head、tail、state(锁状态)信息,AQS的父类包含属性exclusiveOwnerThread,标示持有锁的线程
head 和tail是Node节点,Node节点是一个双向链表,包含prev、next、waitStatus信息。
当线程尝试获取锁时,加锁操作通过CAS设置锁状态为1,并将exclusiveOwnerThread设置为自己,如果成功则获得锁。失败时会再进行一次尝试(因为锁可能很快就被释放),成功即获得锁。如果是相同的线程则state加1,标示冲入次数。失败则进入等待队列。当前想成会被分装成node节点,通过自旋的方式入队。此时如果等待队列为空,head、tail会先指向一个空节点,再通过cas将线程node节点的prev指向tail节点,tail节点的next指向线程node节点,最后将tail节点标记为线程node节点,完成队尾插入操作。
这时只是对node节点的封装,封装完成在进行一次锁竞争,失败则阻塞等待。node节点此时阻塞通过不断循环(注意这里会不断循环,下面讲到线程唤醒时会继续循环)的方式判断prev节点是否为头节点,并尝试获得锁。如果还是失败则进入等待的判断。等待判断当前节点的waitStatus是否为SIGNAL,如果是则循环阻塞等待,如果为CANCEL状态,则移除节点。其他状态则标记waitStatus为SIGNAL,这时还会判断线程是否为CANCEL状态,如果是从队尾节点开始(因为入队的过程中可能存在next未构造的情况,所以从尾部开始)删除节点,如果不是CANCEL状态,调用LockSupport.park()挂起线程。所以所有等待队列中的节点等待状态都为SIGNAL,并且都挂起。
当持有锁的线程调用unLock时,将AQS的state-1,当state==0是exclusiveOwnerThread设置为null,并设置state为0。此时获取head节点,设置waitStatus为0,并获取head的next节点,判断waitStatus部位CANCEL,则调用LockSupport.unPark唤醒线程,进行锁的竞争(因为此时可能还有队列外的线程竞争锁),如果成功者设置head为当前node节点,并将node的thread和prev设成null,标示head节点。
以上是ReentrantLock通过封装AQS实现加锁、解锁的功能。其他的如Condition也有使用AQS封装自己的功能,Condition实现的是单向队列,实现等待唤醒的功能。
ReentrantLock锁的过程

ReentrantLock与Synchronized的区别
- 两者都是可重入锁
- ReentrantLock可以更方便的自由控制锁的粒度,使用更灵活
- synchronized在程序运行结束或异常是释放锁,ReentrantLock一般都在finally中释放
- synchronized是非公平锁,ReentrantLock可公平可不公平。
拓展-ReentrantReadWriteLock
ReentrantReadWriteLock可以分别获取读锁和写锁,当有线程操作写锁是,其他线程才会阻塞,所有线程都只操作读锁是不会阻塞。
public class ReentranReadWriteLockDemo {
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
static Lock readLock = lock.readLock();
static Lock writeLock = lock.writeLock();
static void get() {
readLock.lock();
try {
System.out.println("get");
} finally {
readLock.unlock();
}
}
static void save() {
// 这里会阻塞其他尝试lock的线程,不管是readLock还是writeLock
writeLock.lock();
try {
System.out.println("save");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(() -> {
ReentranReadWriteLockDemo.save();
}).start();
new Thread(() -> {
ReentranReadWriteLockDemo.get();
}).start();
}
}
网友评论