为了更好的支持并发程序,jdk内部提供了大量实用的API和框架,重入锁就是一种对同步的扩展
ReentrantLock起源
在1.5的时候,synchronized关键的性能不是很好,这也是concurrent并发包出现的一种潜在原因,而新出现的ReentrantLock重入锁的性能那时比synchronized好太多,也提供了更加灵活、细粒度的同步操作。(在jdk1.6开始,jdk在synchronized上做了大量的优化,使得两者差距并不大,并且并发包出错性高,通常使用synchronized即可)
lock/unlock
重入锁是synchronized功能的一种扩展,它和synchronized一样能同步执行方法或者代码块,除此之外,它还能指定释放锁对象。通过lock方法来加锁,通过unlock来解锁,这两个方法签名如下:
lock
public void lock()
获取锁。
如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。
如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一
直处于休眠状态,此时锁保持计数被设置为 1。
public void unlock();
释放重入锁,并将保持计数减1
lockInterruptibly相应中断
对于synchronized关键字来说,如果一个线程在等待锁,那么它就只有两种情况:获得锁继续执行/保持等待。而对于重入锁来说,它还有另外一种可能,就是被中断:也就是说在等待锁的过程中,程序可以根据需要取消对锁的请求。这里主要使用了ReentrantLock对象的lockInterruptibly方法。
lockInterruptibly
public void lockInterruptibly() throws InterruptedException
1)如果当前线程未被中断,则获取锁。
2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
4)如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以
前,该线程将一直处于休眠状态:
1)锁由当前线程获得;或者
2)其他某个线程中断当前线程。
5)如果当前线程获得该锁,则将锁保持计数设置为 1。
如果当前线程:
1)在进入此方法时已经设置了该线程的中断状态;或者
2)在等待获取锁的同时被中断。
则抛出 InterruptedException,并且清除当前线程的已中断状态。
6)在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或
重入获取。
lockInterruptibly()和上面的第一种情况是一样的, 线程在请求lock并被阻塞时,如果被interrupt,则“此线程会被唤醒并被要求处理InterruptedException”。并且如果线程已经被interrupt,再使用lockInterruptibly的时候,此线程也会被要求处理interruptedException
立即返回的加锁方式:tryLock
重入锁还有一个加锁的方法:tryLock(),该方法签名及介绍如下:
tryLock public boolean tryLock()
//还有一个带参数运行的tryLock,接受两个参数:等待时长和计时单位,超过
指定时间后还没有获得锁就会返回false,没有参数的tryLock会立即返回
仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
1)如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。
即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),
而不管其他线程当前是否正在等待该锁。在某些情况下,此“闯入”行为可能很有用,即使它会打破公
平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS)
,它几乎是等效的(也检测中断)。
2)如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。
3)如果锁被另一个线程保持,则此方法将立即返回 false 值。
方法总结
使用lock和unlock可以很容易的操作锁的锁定和释放,从签名来看,他们不会抛出中断异常,因此也就不能响应中断,只有单纯的锁获得与所释放,如下测试代码
public static class thread extends Thread{
public static ReentrantLock lock = new ReentrantLock();
public void run(){
lock.lock();
while(true){
System.out.println(i);
Thread.currentThread().interrupt();
}
}
}
创建并启动上面的线程,会发现它将一直执行下去,而不会中断,加入第一个将会在运行时
报错:java.lang.IllegalMonitorStateException,因为lock只有一次,而unlockwhile循环了
而加入第二个将会在编译器就报错,因为while无限循环,后面的代码将没有执行的机会
使用lockInterruptibly方法可以解决类似问题:线程A需要先占用lock1,在占用lock2,而线程B需要先占用lock2,再占用lock1,这里如果简单的使用lock/unlock的话很容易形成两个线程之间的相互等待,所以可以使用lockInterruptibly响应中断:如果当前线程在等待的时候被中断就会取消对锁的请求而抛出InterruptedException中断异常,如下实现代码:
public class Main{
public static void main(String[] args) throws InterruptedException{
thread test1 = new thread(1);
thread test2 = new thread(2);
test1.start();
test2.start();
test1.join();
test2.join();
System.out.println("执行完成");
}
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
public static class thread extends Thread{
public int lock ;
public thread(int lock){
this.lock = lock;
}
public void run(){
System.out.println("starting...");
try{
if(lock == 1){
lock1.lockInterruptibly();
try{
Thread.sleep(2000);
}
catch(Exception e){
e.printStackTrace();
System.out.println("sleep exception...");
}
lock2.lockInterruptibly();
}
else{
lock2.lockInterruptibly();
try{
Thread.sleep(2000);
}
catch(Exception e){
e.printStackTrace();
System.out.println("sleep exception...");
}
lock1.lockInterruptibly();
}
}
catch(Exception e){
e.printStackTrace();
System.out.println("interrupting...");
}
finally{
if(lock1.isHeldByCurrentThread())
lock1.unlock();
if(lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println("thread is running over....");
}
}
}
}
在不加上test2.interrupt()进行中断时运行发现它们将会处于阻塞状态,因为
都在等对方控制的锁资源unlock
而加上了该行代码将会全部执行完成,不过test1是正常执行完毕,test2是抛出异常跳出
test2响应了中断,取消了对锁资源的等待,抛出了InterruptedException异常
而此时,如果将其改成lock将会一直处于等待状态
这里介绍下tryLock带参数的使用方式:
public class Main{
public static void main(String[] args) throws InterruptedException{
trythread test1 = new trythread(1);
trythread test2 = new trythread(2);
test1.start();
test2.start();
}
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
public static class trythread extends Thread{
public int lock;
public trythread(int lock){
this.lock = lock;
}
public void run(){
try{
if(lock == 1){
if(!lock1.tryLock()){
System.out.println("lock1 trylock failure in lock==1");
return;
}
try{
Thread.sleep(1000);
}
catch(Exception e){
e.printStackTrace();
System.out.println("sleep exception in lock==1...");
}
if(!lock2.tryLock()){
System.out.println("lock2 trylock failure in lock==1");
return ;
}
}
else{
if(!lock2.tryLock()){
System.out.println("lock2 trylock failure in lock==2");
return;
}
try{
Thread.sleep(1000);
}
catch(Exception e){
e.printStackTrace();
System.out.println("sleep exception in lock==2...");
}
if(!lock1.tryLock()){
System.out.println("lock1 trylock failure in lock==2");
return ;
}
}
}
catch(Exception e){
e.printStackTrace();
System.out.println("run exception...");
}
finally{
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if(lock2.isHeldByCurrentThread()){
lock2.unlock();
}
if(lock == 1)
System.out.println("running over bye in lock==1");
else
System.out.println("running over bye in lock==2");
}
}
}
}
上面代码输出结果为:
lock2 trylock failure in lock==1
running over bye in lock==1
lock1 trylock failure in lock==2
running over bye in lock==2
表示test1无法获得lock2的锁,test2无法获得lock1的锁,不过在这里使用的是
tryLock方法,两者不会僵持,会立刻返回true/false,不会阻塞线程
公平锁
大多数情况下,锁的申请都是非公平的,多个线程对锁资源的请求会进入竞争状态,系统会从该锁的等待队列中随机挑选一个。而对于公平锁而言,它会保证线程的先来先得,保证所有的线程都要进行排队,不管高优先级还是低优先级。创建公平锁可以使用ReentrantLock的一个构造器
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
如下是使用公平锁的测试代码
public class Main{
public static void main(String[] args) throws InterruptedException{
fairthread t1 = new fairthread("test1");
fairthread t2 = new fairthread("test2");
fairthread t3 = new fairthread("test3");
fairthread t4 = new fairthread("test4");
t1.start();
t2.start();
t3.start();
t4.start();
}
public static ReentrantLock fairlock = new ReentrantLock();
public static class fairthread extends Thread{
public fairthread(String name){
super(name);
}
public void run(){
fairlock.lock();
System.out.println("my name is "+Thread.currentThread().getName());
fairlock.unlock();
}
}
}
不使用公平锁的时候四个线程会随机执行,
使用公平锁的时候不管运行多少次,总是按照线程的启动顺序执行
公平锁看起来很优美,不过其实现还是需要增大系统的开销:实现公平锁必然需要系统维护一个有序队列(先进先出),导致其实现成本比较高,性能也相对比较低,所以默认锁是非公平的
Condition条件类
Condition条件类是ReentrantLock锁的好搭档,他们两个之间的关系其实和之前文章介绍的Object.wait()和Object.notify()是一样的。通过ReentrantLock的newCondition方法可以创建对应的Condition对象,其方法签名如下
public ConditionObject newCondition()
ConditionObject是Condition的一个实现类,其类声明为:
public class ConditionObject implements Condition, java.io.Serializable {...}
通过调用ReentrantLock对象的newCondition方法可以获得对应的Condition。Condition类中提供了很多的方法,方法签名列表如下:
public void await() throws InterruptedException;
该方法会使当前线程等待,同时释放当前锁lock,当其他线程调用signal/signalAll时,
线程才能重新进入就绪状态,或者当线程被interrupted中断时,
才能跳出等待状态并抛出interruptedException异常
public void awaitUninterruptibly();
它和await方法基本相同,但是它并不会在等待过程中响应中断事件
public long awaitNanos(long nanouTimeout) throws InterruptedException;
public boolean await(long time, TimeUnit unit) throws InterruptedException;
等待指定时间,指定两个参数,分别是等待时间和时间单位
public boolean awaitUntil(Date deadline) throws InterruptedException;
public void signal();
用于随机从等待队列中唤醒一个线程使之进入就绪状态
public void signalAll();
用于唤醒等待队列中的所有线程,使他们都进入就绪状态
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)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
当线程调用await进入等待状态时,它会进入到Condition内部维护的一个表中,这个表用于保存等待线程;然后,他会在获得锁之前一直进入while循环状态,知道获得该锁跳出循环,跳出循环时只是处于了就绪状态,还没有真正获得对象锁。可以看到while后面还有一系列获得锁判断的操作
而signal方法的实现代码如下:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
在Condition内部其实一直维护了等待队列的头部结点和尾部结点,该队列的作用就是用于存放等待线程队列,
public class ConditionObject implements Condition, java.io.Serializable{
public static final long serialVersionUID = 117398...L;
public static final Node firstWaiter;
public static final Node lastWaiter;
}
await/signal的本质和wait/notify是一样的,都是用于等待和唤醒,不过区别在于前者是针对ReentrantLock锁对象,而后者是处于synchronized同步代码块/方法中使用的
lock/unlock/await/signal四个方法的执行顺序为:lock->await->signal->unlock;
参考文献
java高并发程序设计第三章