Java 并发包 Concurrent 的包结构共可分为五个部分:
- 原子变量类
- 锁
- collection并发集合框架
- excutor线程池
- 同步工具
本文介绍锁的一些原理和特征,比如自旋,阻塞,可重入,公平锁和非公平锁。
自旋
比如可以用 synchronized 关键字自己来实现一个简单的锁类 Lock,让它有一个标志 isLocked 来标记锁对象是否正在使用,或者已经释放。
public class Lock {
private AtomicReference<Thread> lockedBy = new AtomicReference<Thread>();
public void lock() throws InterruptedException {
Thread current = Thread.currentThread();
while (!lockedBy.compareAndSet(null, current)) {
}
}
public void unlock() {
Thread current = Thread.currentThread();
lockedBy.compareAndSet(current, null);
}
}
使用了 CAS 原子操作,当加锁时,预测之前的状态为 null,之后将 owner 设置为当前线程;解锁时,预测之前的状态为当前线程,之后将 owner 设置为 null。这样第一个线程加锁后,如果第二个线程也来加锁,就会一直在 while 中循环,直到第一个线程解锁后,第二个线程才能开始真正开始执行。自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
阻塞
阻塞锁,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
包含阻塞的方法有,synchronized 关键字,ReentrantLock,wait()\notify() , LockSupport.park()/unpart() 。
阻塞锁的优势在于,阻塞的线程不会占用 CPU 时间, 不会导致 CPU 占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。
在竞争激烈的情况下阻塞锁的性能要明显高于自旋锁。在线程竞争不激烈的情况下使用自旋锁,竞争激烈的情况下使用阻塞锁。
可重入
可重入的意思是,以 synchronized 为例,如果当前一个线程进入了代码中的 synchronized 同步块,并因此获得了该同步块使用的同步对象的锁,那么这个线程可以进入由同一个对象所同步的另一个代码块。
比如,下边的例子:
public class Reentrant1 {
public synchronized void step1(){
step2();
}
public synchronized void step2(){
// do something
}
}
如果一个线程已经拥有了一个对象上的锁,那么它就有权访问被这个对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。
如果使用上边自己定义的锁 Lock 类呢:
public class Reentrant2 {
private Lock lock = new Lock();
public void step1() throws InterruptedException {
lock.lock();
try {
step2();
} finally {
lock.unlock();
}
}
public void step2() throws InterruptedException {
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
}
}
可以看到,当执行 step1 方法,首先对 lock 对象执行加锁,然后进入 step2 方法,而 step2 方法第一步也是对 lock 对象加锁,由于 Lock 类的 while 中的条件判断,这里第二步会使当前线程进入循环。因为没有判断是哪个线程。
修改为:
public class Lock {
private AtomicReference<Thread> lockedBy = new AtomicReference<Thread>();
private AtomicInteger lockCount;
public void lock() throws InterruptedException {
Thread current = Thread.currentThread();
while (!lockedBy.compareAndSet(null, current) && !lockedBy.compareAndSet(current, current)) {
}
lockCount.incrementAndGet();
}
public void unlock() {
Thread current = Thread.currentThread();
if (lockCount.decrementAndGet()==0) {
lockedBy.compareAndSet(current, null);
}
}
}
这样修改就判断了是否是前对象加锁的线程了,如果是的话,就允许通过,然后将对象的加锁次数 lockCount 加 1 。在 unlock 方法中,每次调用都减 1 。只有当对象的加锁次数为 0 时,才能解除对象锁的控制。
公平和非公平
公平和非公平在 Reentrant 锁中详细说明。