锁的基本概念
- 可重入锁:指当一个线程获得锁A,进入互斥区域后,可以再次获取到锁A。而不是说一个线程获得锁A之后,还可以进入锁B。很显然,通常的锁都要设计成可重入的,否则就会发生死锁。 如下代码展示了锁的可重入性:
private int value;
private Lock lock = new ReentrantLock();
public void increment(){
try {
lock.lock();
lock.lock();
value ++;
} finally {
lock.unlock();
lock.unlock();
}
}
synchronized关键字也是可重入锁。基于monitor机制实现。而ReentrantLock基于AQS实现。
- 公平锁与非公平锁
- 公平锁:一个新的线程来了之后,看到有很多线程处于阻塞状态,即AQS中的阻塞队列不为空,自己排到队伍末尾,进入阻塞状态。
- 公平锁:一个线程来了之后,不管有没有其他线程处于阻塞状态,直接抢锁,抢到了则执行,否则进入阻塞队列。
- Concurrent包下的Lock接口:Lock接口定义了锁的基本方法,所有的锁的实现类都实现了Lock接口。
public interface Lock {
// 获取锁,获取到则返回,否则处于阻塞状态直到获取到锁。且不可响应中断
void lock();
// 获取锁,获取到则返回,否则处于阻塞状态直到获取到锁。但是可以响应中断
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,无论能否获取到锁,都返回。获取到则返回true,获取不到则返回false
boolean tryLock();
// 尝试获取锁,获取到则直接返回true,获取不到则阻塞,并等待指定的time时间。
// 若到达指定的time时间后,仍然无法获取到锁则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放当前持有的锁
void unlock();
// 返回一个Condition对象,通过condition可以达到唤醒指定线程的目的。稍后会细讲
Condition newCondition();
}
锁实现的基本原则(AQS)
- 基本上Concurrent包中的Lock的实现类,都只是一个代理类,内部有Sync对象完成真正的同步操作,而Sync的父类
AbstractQueuedSynchronizer
经常被称为队列同步器(AQS)。如ReentrantLock的类继承关系如下:
- AQS实现的核心要素:
- 需要一个state变量,标记该锁的状态。state变量至少有两个值:0,1。对state变量的操作,要确保线程安全,也就是要用到CAS。
- 需要记录当前是哪个线程持有锁,以此做到锁的可重入性。
- 需要底层支持对一个线程进行阻塞或者唤醒操作。
LockSupport
类的park()
方法和unpark()
方法,通过底层Unsafe
类的park()
方法和unpark()
方法,很好的支持了线程的阻塞与唤醒。
- park():在当前线程调用park(),当前线程t就会进入阻塞状态。
- unpark(Thread t): 在另外一个线程中,调用unpark(t),传入一个阻塞的线程,就可以唤醒阻塞在park()方法的线程。从而实现一个线程对另一个线程的“精准唤醒”。
- 需要有一个队列维护所有阻塞的线程。这个队列也必须是线程安全的无锁队列,因此也需要用到CAS。AQS中利用双向链表+CAS实现的一个线程安全的阻塞队列。
public class LockSupport {
......
// 在当前线程调用park(),当前线程t就会进入阻塞状态。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
//调用unpark(t),传入一个阻塞的线程,就可以唤醒阻塞在park()方法的线程t。
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
......
}
本人水平有限,因此给大家推荐一篇介绍AQS的经典好文,与大家共勉。有兴趣的童鞋可以关注:https://www.javadoop.com/post/AbstractQueuedSynchronizer