文章目录
java.util.concurrent包中的Lock包中主要时AQS框架和LockSupport
Locks 锁
此包中实现的最基本的锁,阻塞线程的LockSupport。核心是AQS框架(AbstractQueuedSynchronizer),是J U C(java util concurrent) 最复杂的一个类。
Lock 和Synchronized
J U C 中的Lock和synchronized具有同样的语义和功能。不同的是,synchronized 锁在退出块时自动释放。而Lock 需要手动释放,且Lock更加灵活。Syschronizd 是 java 语言层面的,是系统关键字;Lock则是java 1.5以来提供的一个类。
Synchronized 具有以下缺陷,它无法中断一个正在等候获得锁的线程;也无法通过投票得到锁,如果不想等下去,也就没法得到锁;同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行。
而Lock(如ReentrantLock )除了与Synchronized 具有相同的语义外,还支持锁投票、定时锁等候和可中断锁等候(就是说在等待锁的过程中,可以被中断)的一些特性。
除了ReentrantLock是外部类,其他的3个都是内部类,Segment是ConcurrentHashMap的内部类,ReadLock和WriteLock都是ReentrantReadWriteLock的内部类
Lock接口中主要定义了加锁和解锁操作。
Lock. lockInterruptibly ,调用后,或者获得锁,或者被中断后抛出异常。优先响应异常。
LockSupport 和java内置锁
在LockSupport出现之前,如果要block/unblock某个Thread,除了使用Java语言内置的monitor机制之外,只能通过Thread.suspend()和Thread.resume()。然而Thread.suspend()和Thread.resume()基本上不可用,除了可能导致死锁之外,它们还存在一个无法解决的竞争条件:如果在调用Thread.suspend()之前调用了Thread.resume(),那么该Thread.resume()调用没有任何效果。LockSupport最主要的作用,便是通过一个许可(permit)状态,解决了这个问题。LockSupport 只能阻塞当前线程,但是可以唤醒任意线程。
那么LockSupport和Java语言内置的monitor机制有什么区别呢?它们的语义是不同的。LockSupport是针对特定Thread来进行block/unblock操作的;wait()/notify()/notifyAll()是用来操作特定对象的等待集合的。正如每个Object都有一个锁,每个Object也有一个等待集合(wait set),它有wait、notify、notifyAll和Thread.interrupt方法来操作。Java语言内置的monitor机制:同时拥有锁和等待集合的实体,通常被成为监视器(monitor)。每个Object的等待集合是由JVM维护的。等待集合一直存放着那些因为调用对象的wait方法而被阻塞的线程。由于等待集合和锁之间的交互机制,只有获得目标对象的同步锁时,才可以调用它的wait、notify和notifyAll方法。这种要求通常无法靠编译来检查,如果条件不能满足,那么在运行的时候调用以上方法就会导致其抛出IllegalMonitorStateException。
· 如果当前线程已经被中断,那么该方法立刻退出,然后抛出一个InterruptedException异常。否则线程会被阻塞。
· JVM把该线程放入目标对象内部且无法访问的等待集合中。
· 目标对象的同步锁被释放,但是这个线程锁拥有的其他锁依然会被这个线程保留着。当线程重新恢复质执行时,它会重新获得目标对象的同步锁。
notify()方法被调用后,会执行如下操作:
· 如果存在的话,JVM会从目标对象内部的等待集合中任意移除一个线程T。如果等待集合中的线程数大于1,那么哪个线程被选中完全是随机的。
· T必须重新获得目标对象的同步锁,这必然导致它将会被阻塞到调用Thead.notify()的线程释放该同步锁。如果其他线程在T获得此锁之前就获得它,那么T就要一直被阻塞下去。
· T从执行wait()的那点恢复执行。
notifyAll()方法被调用后的操作和notify()类似,不同的只是等待集合中所有的线程(同时)都要执行那些操作。然而等待集合中的线程必须要在竞争到目标对象的同步锁之后,才能继续执行。
在标准的Sun jdk 中,Locksupport的实现基于Unsafe,都是本地代码,采用park和unpark进行加锁和解锁的操作。JDK并发中的AQS框架使用的就是LockSupport中的park/unpark操作。
一个线程调用park阻塞之后,如果被其他线程调用interrupt(),那么他它会响应中断,解除阻塞,但是不会抛出interruption 异常。这点在构造可中断获取锁的时候用到了。
AbstractQueuedSynchronizer
AQS框架是 J U C包的核心。是构建同步、锁、信号量和自定义锁的基础。也是构建高级工具的基础。
锁,信号量的实现内部都有两个内部类,都继承AQS。
**由于AQS的构建上采用模板模式(Template mode),即 AQS定义一些框架,而它的实现延迟到子类。如tryAcquire()方法。由于这个模式,我们如果直接看AQS源码会比较抽象。所以从某个具体的实现切入简单易懂。这里选泽ReentrantLock ,**它和Synchronized具有同样的语义。
简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全 部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用 sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把 线程交给系统内核进行阻塞。
ReentrantLock
从ReentrantLock(可重入锁)开始,分析AQS。首先需要知道这个锁和java 内置的同步Synchronized具有同样的语义。如下代码解释重入的意思
Lock lock = new ReentrantLock();
public void test() {
lock.lock();
System.out.print("I am test1");
test(); // 递归调用 ……………………………1 递归调用不会阻塞,因为已经获得了锁,这就是重入的含义
// test2();// 调用test2 ………………………2
lock.unlock();// 这里应该放在finally 块中,这里简单省略,以后一样。
}
public void test2() {
lock.lock();
System.out.println("I am test1");
test2();//
lock.unlock();
}
重入的意思就是,如果已经获得了锁,如果执行期间还需要获得这个锁的话,会直接获得锁,不会被阻塞,获得锁的次数加1;每执行一次unlock,持有锁的次数减1,当为0时释放锁。这点,Synchronized 具有同样语义。
lock
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
从源码中可以看出,ReentrantLock 对Lock接口的实现,把所有的操作都委派给一个叫Sync的类。Sync类继承了AQS接口,通过ReentrantLock可以看出Sync有存在两个子类
final static class NonfairSync extends Sync
final static class FairSync extends Sync
从名称可以看出是为了支持公平锁和非公平锁而定义,默认情况下为非公平锁,因此先从默认的非公平锁的lock()方法开始看。