【多线程与并发】:Java中的锁
锁的概念
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁可以防止多个线程同时访问共享资源(但有些锁可以允许多个线程并发的访问共享资源,如读写锁)。
在JDK1.5之前,Java是通过synchronized关键字实现锁功能的:隐式地获取锁和释放锁,但不够灵活。
在JDK1.5,java.util.concurrent
包中新增了Lock接口以及相关实现类,用来实现锁功能。它提供了与synchronized关键字类似的同步功能,但功能更强大和灵活:获取锁和释放锁的可操作性、可中断地获取锁、超时获取锁等,见下表:
特性 | 描述 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这个时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 获取到锁的线程能够响应中断(而synchronized则不会响应中断操作) |
超时获取锁 | 在指定的截止时间之前获取锁,如果在截止时间到了仍无法获取锁,则返回。 |
Lock接口具体的方法及释义:
public interface Lock {
/**
* 获取锁
*
* 如果当前线程无法获取到锁(可能其他线程正在持有锁),则当前线程就会休眠,直到获取到锁
*/
void lock();
/**
* 可中断地获取锁
*
* 如果如果当前线程无法获取到锁(可能其他线程正在持有锁),则当前线程就会休眠,
* 直到发生下面两种情况的任意一种:
* ①获取到了锁
* ②被其他线程中断
*/
void lockInterruptibly() throws InterruptedException;
/**
* 尝试非阻塞地获取锁
*
* lock()和lockInterruptibly()在获取不到锁的时候,都会阻塞当前线程,直到获取到锁
* 而该方法则不会阻塞线程,能立即获取到锁则返回true,获取不到则立即返回false
*
* 该方法的常用方式如下:
*
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }}
*
* 这种使用方式,可以保证只在获取到锁的时候才去释放锁
*/
boolean tryLock();
/**
* 超时获取锁
*
* 当前线程在以下三种情况下会返回:
* ①当前线程在超时时间内获取到了锁,返回true
* ②当前线程在超时时间内被中断,返回false(即该方法可以响应其他线程对该线程的中断操作)
* ③超时时间结束,没有获取到锁,返回false
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 获取与该锁绑定的Condition
*
* 当前线程只有在获得了锁,才能调用Condition的wait()方法(表示我已经到了某一条件),
* 调用Condition的wait()方法之后,当前线程会释放锁
*/
Condition newCondition();
}
包java.util.concurrent.locks
的类图
其中:
AbstractOwnableSynchronizer
、AbstractQueuedLongSynchronizer
、AbstractQueuedSynchronizer
是同步器,是锁实现相关的内容。
ReentrantLock(重入锁)
和ReentrantReadWriteLock(重入读写锁)
是具体的实现类。
LockSupport
是一个工具类,提供了基本的线程阻塞和唤醒功能。
Condition
是实现线程间实现多条件等待/通知模式用到的。
同步器的实现原理
TODO
重入锁:ReentrantLock
重入锁,顾名思义,就是支持重新进入的锁:即某线程在获取到锁之后,可以再次获取锁而不会被阻塞。
ReentrantLock类是通过组合自定义同步器来来实现这种重入特性的,除此之外,该类还支持公平地获取锁(获取锁的顺序与请求锁的顺序是相同的,等待时间最长的线程最优先获取到锁),还支持绑定多个Condition。(synchronized
关键字隐式地支持重进入,比如synchronized
修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,不会出现阻塞自己的情况)。
ReentrantLock内部重进入的实现(非公平获取锁的情况)代码如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()) {
//如果是当前持有锁的线程再次获取锁,则将同步值进行增加并返回true
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
ReentrantLock公平锁的内部实现代码如下:
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;
}
与非公平获取锁的方法nonfairTryAcquire(int acquires)
相比,多了一个hasQueuedPredecessors()
判断:同步队列中当前节点(当前想要获取锁的线程)是否有前驱节点,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要前驱线程获取并释放锁之后才能继续获取锁。
公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换;
非公平锁虽然可能造成线程“饥饿”(即某线程可能需要等很久才得到锁),但线程切换极少,可以保证更大的吞吐量。
读写锁:ReentrantReadWriteLock
ReentrantLock
在在同一时刻,只允许一个线程进行访问(无论读还是写)。而读写锁是指:在同一时刻,允许多个线程进行读操作,而写操作则会阻塞其他所有的线程(无论是读还是写,都会被阻塞)。读写锁维护了一对锁:读锁和写锁,通过分离读锁和写锁,使得并发性能相比一般的排他锁有了很大的提升。
Java中读写锁的实现类是ReentrantReadWriteLock
,它支持:①重进入;②公平性选择;③锁降级:写锁可以降级为读锁,其提供了一些便于外界监控其内部状态的方法,如下:
int getReadLockCount()
返回当前读锁被获取的次数
注意:该次数并不等于获取读锁的线程数,
因为同一线程可以连续获得多次读锁,获取一次,返回值就加1,
比如,仅一个线程,它连续获得了n次读锁,那么占据读锁的线程数是1,但该方法返回n
int getReadHoldCount()
返回当前前程获取读锁的次数
boolean isWriteLock()
判断读锁是否被获取
int getWriteHoldCount()
返回当前写锁被获取的次数
使用举例:
public class Cache{
//非线程安全的HashMap
private static Map<String, Object> map = new HashMap<>();
//读写锁
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//读锁
private static Lock readLock = reentrantReadWriteLock.readLock();
//写锁
private static Lock writeLock = reentrantReadWriteLock.writeLock();
/**
* 获取key对应的value
*
* 使用读锁,使得并发访问该方法时不会被阻塞
*/
public static final Object get(String key){
readLock.lock();
try{
return map.get(key);
}finally {
readLock.unlock();
}
}
/**
* 设置key对应的value
*
* 当有线程对map进行put操作时,使用写锁,阻塞其他线程的读、写操作,
* 只有在写锁被释放后,其他读写操作才能继续
*/
public static Object put(String key, Object value){
writeLock.lock();
try {
return map.put(key, value);
}finally {
writeLock.unlock();
}
}
/**
* 清空map
*
* 当有线程对map进行清空操作时,使用写锁,阻塞其他线程的读、写操作,
* 只有在写锁被释放后,其他读写操作才能继续
*/
public static void clear(){
writeLock.lock();
try {
map.clear();
}finally {
writeLock.unlock();
}
}
}
TODO:读写锁的实现原理
LockSupport工具类
LockSupport
定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,是构建同步组件的基础工具,它主要有两类方法:
①以park
开头的方法:阻塞当前线程
②以unpark
开头的方法:唤醒被阻塞的线程
void park()
阻塞当前线程,只有当前线程被中断或其他线程调用unpark(Thread thread),才能从park()方法返回
void parkNanos(long nanos)
阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回
void parkUntil(long deadline)
阻塞当前线程,直到deadline这个时间点(从1970年开始到deadline时间的毫秒数)
void unpark(Thread thread)
唤醒处于阻塞状态的thread线程
在JDK1.6中,该类增加了void park(Object blocker)
、void parkNanos(Object blocker, long nanos)
、void parkUntil(Object blocker, long deadline)
方法,相比之前的park方法,多了一个blocker对象,该对象用来标识当前线程在等待的对象(阻塞对象),主要用来问题排查和系统监控(对线程dump时,可以提供阻塞对象的信息),可以用来代替原有的park方法。
Condition接口
任意一个Java
对象都有一组监视器方法(定义在java.lang.Object
上):wait()、wait(long timeout)、notify()、notifyAll()
,这些方法与sychronized
配合使用,可以实现等待/通知模式。
Condition
接口也提供了类似的监视器方法,与Lock
配合使用,可以实现等待/通知模式。
两者的区别如下:
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock()获取锁→调用Lock.newCondition()获取Condition对象 |
调用方式 | 直接调用,如object.wait() | 直接调用,如condition.await() |
等待队列个数 | 1个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来的某个时间点 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition中的方法如下:(一般会将Condition对象作为成员变量)
说明:当前线程调用await()方法后,当前线程会释放锁并在此等候,当其他线程调用signal()方法通知当前线程后,当前线程才从await()方法中返回,并且在返回前已经获取了锁(re-acquire
)。
public interface Condition {
/**
* 当前线程进入等待状态直到被通知(signalled)或中断(interrupted)
*
* 如果当前线程从该方法返回,则表明当前线程已经获取了Condition对象所对应的锁
*
* @throws InterruptedException
*/
void await() throws InterruptedException;
/**
* 与await()不同是:该方法对中断操作不敏感
*
* 如果当前线程在等待的过程中被中断,当前线程仍会继续等待,直到被通知(signalled),
* 但当前线程会保留线程的中断状态值
*
*/
void awaitUninterruptibly();
/**
* 当前线程进入等待状态,直到被通知或被中断或超时
*
* 返回值表示剩余时间,
* 如果当前线程在nanosTimeout纳秒之前被唤醒,那么返回值就是(nanosTimeout-实际耗时),
* 如果返回值是0或者负数,则表示等待已超时
*
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
* 该方法等价于awaitNanos(unit.toNanos(time)) > 0
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
* 当前线程进入等待状态,直到被通知或被中断或到达时间点deadline
*
* 如果在没有到达截止时间就被通知,返回true
* 如果在到了截止时间仍未被通知,返回false
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
* 唤醒一个等待在Condition上的线程
* 该线程从等待方法返回前必须获得与Condition相关联的锁
*/
void signal();
/**
* 唤醒所有等待在Condition上的线程
* 每个线程从等待方法返回前必须获取Condition相关联的锁
*/
void signalAll();
}
使用Condition实现一个有界阻塞队列的例子:当队列为空时,队列的获取操作将会阻塞当前线程,直到队列中有新增元素;当队列已满时,队列的插入操作就会阻塞插入线程,直到队列中出现空位。(其实这个例子就是简化版的ArrayBlockingQueue
)
class BoundedBlockingQueue<T> {
//使用数组维护队列
private Object[] queue;
//当前数组中的元素个数
private int count = 0;
//当前添加元素到数组的位置
private int addIndex = 0;
//当前移除元素在数组中的位置
private int removeIndex = 0;
private Lock lock = new ReentrantLock();
private Condition notEmptyCondition = lock.newCondition();
private Condition notFullCondition = lock.newCondition();
private BoundedBlockingQueue() {
}
public BoundedBlockingQueue(int capacity) {
queue = new Object[capacity];
}
public void put(T t) throws InterruptedException {
lock.lock();//获得锁,保证内部数组修改的可见性和排他性
try {
//使用while,而非if:防止过早或意外的通知,
//加入当前线程释放了锁进入等待状态,然后其他线程进行了signal,
//则当前线程会从await()方法中返回,再次判断count == queue.length
//todo:哪些情况下的过早或意外???
while (count == queue.length) {
notFullCondition.await();//释放锁,等待队列不满,即等待队列出现空位
}
queue[addIndex] = t;
addIndex++;
if (addIndex == queue.length) {
addIndex = 0;
}
count++;
notEmptyCondition.signal();
} finally {
//确保会释放锁
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmptyCondition.await();//释放锁,等待队列不为空,即等待队列中至少有一个元素
}
Object x = queue[removeIndex];
removeIndex++;
if (removeIndex == queue.length) {
removeIndex = 0;
}
count--;
notFullCondition.signal();//通知那些等待队列非空的线程,可以向队列中插入元素了
return (T) x;
} finally {
//确保会释放锁
lock.unlock();
}
}
}
TODO:Condition的实现分析
参考
大部分来自《Java并发编程的艺术》,部分参考JDK中的注释说明。
作者:maxwellyue
链接:https://www.jianshu.com/p/6e0982253c01
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。