重入锁:ReentrantLock

534 阅读1分钟

介绍

重入锁ReentrantLock,相对关键字synchronized比较灵活,需要手动加锁和释放锁。在JDK5中重入锁性能远远大于关键字synchronized(synchronized隐式的支持重进入)。从JDK6开始内置锁做了大量优化,性能和重入锁已经相差不大。

重进入


public class ReenterLock implements Runnable{
    private static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    @Override
    public void run() {
        for (int j = 0; i< 10000000; i++){
            lock.lock();
            //可以看出可以多次获得锁
            lock.lock();
            try {
                i++;
            } finally {
                //多次获得锁,则需要相同次数的释放锁
                //如果释放锁的次数多了,则会抛出java.lang.IllegalMonitorStateException异常
                //如果释放的次数少了,则锁一直被当前线程持有,则其它线程无法获得该锁
                lock.unlock();
                lock.unlock();
            }

        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLock tl = new ReenterLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

可以看出lock.unlock()必须在finally块中释放锁,这个要特别注意下,不然如果加锁后代码出现异常,锁就无法释放。

ReentrantLock是通过组合自定义同步器来实现锁的获取和释放的

/**
 * Base of synchronization control for this lock. Subclassed
 * into fair and nonfair versions below. Uses AQS state to
 * represent the number of holds on the lock.
 */
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    //抽象lock方法,供公平锁和非公平锁各自实现
    abstract void lock();

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
     //非公平的尝试加锁
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //尝试修改AQS的state
            if (compareAndSetState(0, acquires)) {
                //锁的持有者设置为当前线程并返回true表示获取锁成功
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {//当前线程为持有锁线程
            int nextc = c + acquires;
            锁的数量超过限制
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            //同步状态值state增加并返回true(获取同步状态成功)
            //获取锁线程子再次获取锁增加了同步状态值,则在释放锁的时候需要同步减少同步状态值
            setState(nextc);
            return true;
        }
        return false;
    }
    
    //尝试释放同步状态(也就是尝试释放锁)
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        //持有锁的线程非当前线程报异常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //最后一次释放同步状态(释放同步锁),例如lock了n次,则此为第n次释放同步状态
        if (c == 0) {
            //返回true并将持有锁的线程置为空
            free = true;
            setExclusiveOwnerThread(null);
        }
        //修改同步状态
        setState(c);
        return free;
    }
    ......
}

中断响应

重入锁在等待锁的过程中可以被中断,对于处理死锁有一定帮助。

我们来看一下下面这个例子:


public class IntLock implements Runnable{
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    /**
     * 控制加锁顺序,方便构造死锁
     * @param lock
     */
    public IntLock(int lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            if(lock == 1){
                lock1.lockInterruptibly();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock1.lockInterruptibly();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock1.isHeldByCurrentThread()){
                lock1.unlock();
            }

            if(lock2.isHeldByCurrentThread()){
                lock2.unlock();
            }

            System.out.println(Thread.currentThread().getId() + ":线程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();t2.start();
        Thread.sleep(1000);
        //中断其中一个线程
        t2.interrupt();
    }
}

上述代码输出:

java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.wk.manage.web.controller.IntLock.run(IntLock.java:38)
	at java.lang.Thread.run(Thread.java:748)
12:线程退出
11:线程退出

上述代码在t2中断之前,t1和t2很可能会相互等待产生死锁。(t1获取lock1,再请求lock2,请求lock2之前t2已经获得lock2,并且t2会去请求获取lock1的锁。)

可以看到获取锁使用的是lockInterruptibly()方法,是可以在等待时响应中断的。因此在t2.interrupt()中断后,t2放弃对lock1的申请并释放lock2锁,此时t1便可以顺利执行。

限时等待

在响应中断中提到避免死锁可以在外部对线程进行中断,另外限时等待也可以避免死锁的发生。给定一个等待时间,超过等待时间未获得锁则会返回获取锁失败。

限时等待的使用的例子:


public class TimeLock implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if(lock.tryLock(5, TimeUnit.SECONDS)){
                Thread.sleep(6000);
            } else {
                System.out.println("get lock failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TimeLock tl = new TimeLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);
        t1.start();
        t2.start();
    }
}

上述例子使用的是tryLock带等待时间参数的方法。本例中获取锁的线程会执行6s,则另外一个线程在5s内获取不到线程则会获取失败。

ReentrantLock.tryLock()方法也可以不带参数进行执行,如果锁被其它线程占用,则当前线程不会等待。而会立即返回false。因此也不会发生线程等待导致死锁的情况。

公平和不公平

使用synchronized关键字进行锁控制,产生的锁是非公平的。重入锁默认锁是非公平的,非公平的成本相对更低,并且效率也会更高。重入锁的话可以对公平性进行设置,如下构造函数:


/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

对于非公平锁的尝试获取的方法在重进入小节中代码中已经做了分析nonfairTryAcquire(int acquires)方法,可以看到,对于非公平锁,线程时可以“插队”的,只CAS设置同步状态成功则获取了锁。

/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        //直接尝试获取锁
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //没有获取到锁则再次尝试获取一次,若还没获得则排到等待队列队尾等待
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        //非公平尝试获取锁
        return nonfairTryAcquire(acquires);
    }
}

下面我们来看下公平锁的相关实现:

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
     //公平锁尝试获取锁
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //队列中如果没有等待的线程并且CAS设置同步状态成功,则设置锁的持有者为当前线程。标识成功获取到锁
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            //当前线程持有该锁并重进入了该锁,则对同步状态进行增加
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

可见公平锁需要系统维护一个有序队列,相对非公平锁开销会比较高,性能也比较低下。但公平锁有一个比较显著的优点,那就是不会产生饥饿现象,通过排队都会最终获得锁。

我们来看一个公平锁简单示例:


public class FairLock implements Runnable{
    public static ReentrantLock fairLock = new ReentrantLock(true);
    @Override
    public void run() {
        while(true){
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName() + "获得锁");
            } finally {
                fairLock.unlock();
            }

        }
    }

    public static void main(String[] args) {
        FairLock rl = new FairLock();
        Thread t1 = new Thread(rl, "Thread_t1");
        Thread t2 = new Thread(rl, "Thread_t2");
        t1.start();t2.start();
    }
}

部分输出如下:

Thread_t1获得锁
Thread_t2获得锁
Thread_t1获得锁
Thread_t2获得锁
Thread_t1获得锁
Thread_t2获得锁
Thread_t1获得锁
Thread_t2获得锁
Thread_t1获得锁
Thread_t2获得锁

可以看到两个线程交替获得锁,保证了公平性。如果我们把公平锁改成非公平锁,我们来看下部分输出结果:

Thread_t1获得锁
Thread_t1获得锁
Thread_t1获得锁
Thread_t1获得锁
Thread_t1获得锁
Thread_t2获得锁
Thread_t2获得锁
Thread_t2获得锁
Thread_t2获得锁
Thread_t2获得锁

从上面输出结果我们可以看到,非公平锁情况下一个线程会倾向于再次获取已经持有的锁,分配非公平但高效。

非公平锁和公平锁的比较:

公平锁需要维护一个有序队列,成本较高,保证了锁的获取按照FIFO原则,但代价却是进行大量的线程切换,较耗费性能,但比较好的一点是不会产生线程饥饿。非公平锁可能造成线程“饥饿”,但线程切换很少,保证了更大的吞吐量。

正常情况下我们应该优先使用公平锁,但是什么情况下我们应该使用公平锁那?当持有锁的时间比较长或者请求锁的平均时间间隔比较长的时候,应优先使用公平锁。在这些情况下,非公平锁“插队”可能不会带来吞吐量的提升,而且可能很容易产生线程“饥饿”现象。

总结

如下整理了几个ReentrantLock的几个重要的方法:

//获取锁,锁被占用则等待
public void lock()

//获取锁,但优先响应中断
public void lockInterruptibly() throws InterruptedException

//尝试获取锁,不等待,直接返回。成功返回true,失败返回false
public boolean tryLock()

//给定时间内尝试获取锁,可响应中断
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException

//释放锁
public void unlock()

另外synchronized为JVM层面的内置锁,我们应该优先使用,对于ReentrantLock可以在内置锁无法满足需求的时候使用(能提供更好的伸缩性,并且提供一些上面我们所介绍的功能)。

参考资料:《Java高并发程序设计(第2版)》《Java并发编程实战》《Java并发编程的艺术》