Lock接口
public interface Lock {
//下面4个方法都是获得锁
void lock();
void lockInterruptibly() throws InterruptedException; // 如果当前线程未被中断,则获取锁,可以响应中断
boolean tryLock(); //如果获取到锁返回true,否则false
boolean tryLock(long var1, TimeUnit var3) throws InterruptedException; //如果获取不到锁,就等待一段时间,超时返回false。
//解除锁,在finally里调用
void unlock();
//返回Condition实例
Condition newCondition();
}
对于Lock接口方法的实现,大多都是调用AQS的方法来实现。实现Lock接口的类含有继承了AQS一个内部类(例如ReentrantLock的Sync内部类),从而调用内部类的继承自AQS的方法或者重写的方法来实现Lock接口的方法。
Lock接口最后还有一个返回Condition实例的方法。Condition是和Lock配合使用的。
在一个AQS同步器中,可以定义多个Condition,每一个Condition是一条FIFO队列。只需要多次lock.newCondition(),每次都会返回一个新的ConditionObject对象。在ConditionObject中,通过一个条件队列来维护等待的线程,这个队列跟AQS的队列不是同一条队列,一个同步器中可以有多个条件队列。
Condition接口
public interface Condition {
// 当前线程进入等待状态直到被通知(signal)或被中断
void await() throws InterruptedException;
// 不响应中断等待,直到被通知(signal)
void awaitUninterruptibly();
// 等待指定时长直到被通知或中断或超时。
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 等待指定时长直到被通知或中断或超时。
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定事件就被通知,方法返回true,否则false。
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁
void signal();
// 唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁
void signalAll();
}
ConditionObject
ConditionObject是AQS中的内部类,提供了条件锁的同步实现,实现了Condition接口,并且实现了其中的await(),signal(),signalALL()等方法。因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个FIFO等待队列,该队列是Condition对象实现等待/通知功能的关键。
当线程获取到锁之后,Condition对象调用await相关的方法:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加当前线程到条件队列
Node node = addConditionWaiter();
// 释放已经获取的锁资源,并返回释放前的同步状态
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果当前节点不在同步队列中,线程进入阻塞状态,等待被唤醒
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
可以看到因为ConditionObject是AQS内部类,可以获取到外部类的数据,调用await将线程放进条件队列,然后同样调用LockSupport类的park方法进行阻塞。
Condition对象调用signal或者signalAll方法时:
// 将条件队列中第一个有效的元素移除并且添加到同步队列中
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 将条件队列中等待最久的那个有效元素添加到同步队列中
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 将条件队列中的节点转换到同步队列中
final boolean transferForSignal(Node node) {
// 如果节点的等待状态不能被修改,说明当前线程已经被取消等待
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入到同步队列中,并且尝试将前驱节点设置为可唤醒状态
Node p = enq(node);
int ws = p.waitStatus;
// 如果前驱节点不需要唤醒,或者设置状态为‘唤醒’失败,则唤醒线程时期重新争夺同步状态
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
singal方法将节点放回同步队列尾端,调用enq方法,该方法在AQS的时候分析过,该方法使用了死循环, 即以自旋方式将节点插入队列,如果失败则不停的尝试, 直到成功为止,运用到了乐观锁的原理,该方法也负责在队列为空时, 初始化队列。
为什么要有Condition呢?
在synchronized中,我们可以调用object.await和object.notify,让线程等待和唤醒,但方法仅在synchronized中可以使用。通过Condition能够更加精细的控制多线程的休眠与唤醒,synchronized相比ReentrantLock、ReadWriteLock等实现了Lock接口和内含继承AQS类的子类的锁来说,synchronized显得笨重,不够灵活,适合小段代码使用。
condition.signal和object.notify区别
- obj.notify(); 随机唤醒一个处于等待状态的线程,可能有多个线程处于等待状态,继续执行wait后面的代码。与synchronized同步关键字配合
- condition.signal(); 唤醒在同条件队列的线程,与Lock配合
使用要求:
- signal()、await()、signalAll()方法使用之前必须要先进行lock()获取锁,类似使用Object的notify()、wait()、notifyAll()之前必须要对Object对象进行synchronized操作,且两类方法不能混合使用,否则都会抛IIlegalMonitorStateException。
Synchronized和Lock比较
- Synchronized是关键字,内置语言实现,Lock是接口。
- Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
- Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。
- Lock可以使用读锁提高多线程读效率。
关系图:
重入锁
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
为什么需要再次获取锁呢?
一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁,从而避免了死锁。
实现原理
通过为每个锁关联一个请求计数器和一个获得该锁的线程。该计数器不是AQS的state,当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁。
重入锁有:
- synchronized
- ReentrantLock
- ReentrantReadWriteLock
两者区别
- synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
- synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
- synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
ReentrantLock
ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
Lock lock=new ReentrantLock();//默认非公平锁
Lock lock=new ReentrantLock(true);//公平锁
Lock lock=new ReentrantLock(false);//非公平锁
getHoldCount() 查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数。
getQueueLength()返回正等待获取此锁的线程数,比如启动10个线程,1个线程获得锁,此时返回的是9。
getWaitQueueLength(Condition condition)返回在该条件队列的线程数。
hasWaiters(Condition condition) 查询该条件队列是否有等待线程。
hasQueuedThread(Thread thread) 查询指定的线程是否正在等待获取Lock锁。
hasQueuedThreads() 是否有线程等待此锁
isFair() 该锁是否公平锁
isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
isLock() 此锁是否有任意线程占用
lockInterruptibly() 如果当前线程未被中断,获取锁
tryLock() 线程尝试获取锁,如果获取成功,返回 true,否则返回 false
tryLock(long timeout,TimeUnit unit) 线程如果在指定等待时间内获得了锁,就返回true,否则返回 false
ReentrantReadWriteLock
Java并发包中ReadWriteLock是一个接口,主要有两个方法,如下:
public interface ReadWriteLock {
//返回读锁
Lock readLock();
//返回写锁
Lock writeLock();
}
ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。
有两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
读写锁的机制:
- "读-读" 不互斥
- "读-写" 互斥
- "写-写" 互斥
线程进入读锁的前提条件:
- 没有其他线程的写锁或者有写请求,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
3个特性:
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- 重进入:读锁和写锁都支持线程重进入。
- 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。不支持锁升级。