java高并发程序设计总结三:JDK并发包之ReentrantLock重入锁

时间:2022-10-21 23:50:10

为了更好的支持并发程序,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,该方法将返回 true3)如果锁被另一个线程保持,则此方法将立即返回 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();
                //lock.unlock();
            }
            //lock.unlock();
        }
    }

创建并启动上面的线程,会发现它将一直执行下去,而不会中断,加入第一个将会在运行时
报错: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();
        //test2.interrupt();
        test1.join();
        test2.join();
        System.out.println("执行完成");
    }


    //使用重入锁ReentrantLock:synchronized的扩展
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();

    public static class thread extends Thread{

        //指定需要获得的锁的序号1/2
        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();
    }


    //使用重入锁ReentrantLock:synchronized的扩展
    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 ReentrantLock fairlock = new ReentrantLock(true);

    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(); // 将当前线程包装下后,
                                      // 添加到Condition自己维护的一个链表中。
    int savedState = fullyRelease(node);// 释放当前线程占有的锁

    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {// 释放完毕后,遍历AQS的队列,看当前节点是否在队列中,
        // 不在 说明它还没有竞争锁的资格,所以继续将自己沉睡。
        // 直到它被加入到队列中,聪明的你可能猜到了,
        // 没有错,在singal的时候加入不就可以了?
        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; // firstWaiter为condition自己维护的一个链表的头结点,
                              // 取出第一个节点后开始唤醒操作
    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高并发程序设计第三章
如何理解Condition
Java中Lock,tryLock,lockInterruptibly有什么区别?
lock-unlock流程详解
java-api1.7官方文档