Java并发编程之重入锁(ReentrantLock)

2021-04-20 11:03:04 浏览数 (3)

简介

ReentrantLock重入锁,由于它的加锁和解锁操作需要手动来完成,所以也称为显式锁

ReentrantLock是基于AQS独占模式实现的独占锁,同时只能有一个线程能获取到该锁,获取不到锁的线程将被放入该锁的AQS阻塞队列中等待。

ReentrantLock的简单使用

一个ReentrantLock使用示例,对临界资源进行加锁,当线程访问临界资源时需要先获得锁,如果锁已经被其他线程所持有,则需要等待其他线程释放锁后再重新获取锁,直到获取锁之后才能继续执行。

代码语言:javascript复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName()   "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName()   "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

/*
 * 输出结果:
 * Thread-1获得了锁
 * Thread-1释放了锁
 * Thread-0获得了锁
 * Thread-0释放了锁
 */
可重入性

可重入性是指线程获得锁之后,没有释放锁,当再次需要获得同一把锁的时候仍然可以成功。如果锁是不可重入的,第二次获得锁的时候就会导致死锁。

state变量记录了锁的重入次数,每次lock()方法的调用会使state加1,每次unlock()方法的调用会使state减1,当state为0时锁被释放,将锁的持有者设置为null。

如下示例,多次调用lock()方法是可以的,但是对应的unlock()方法也要调用对应次数。

代码语言:javascript复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName()   "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName()   "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

/*
 * 输出结果:
 * Thread-1获得了锁
 * Thread-1释放了锁
 * Thread-0获得了锁
 * Thread-0释放了锁
 */

当unlock()方法次数少于lock()方法调用次数时,state变量值大于0,相当于当前线程还持有该锁,其他线程依然不能获取到锁,只能继续等待。如下所示:

代码语言:javascript复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName()   "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName()   "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

/*
 * 输出结果:
 * Thread-1获得了锁
 * Thread-1释放了锁
 */

当unlock()方法次数大于lock()方法调用次数时,相当于锁已经被释放,锁的持有者不再是当前线程,此时再次调用unlock()方法将抛出IllegalMonitorStateException异常。

代码语言:javascript复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName()   "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName()   "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

输出结果:

代码语言:javascript复制
Thread-0获得了锁
Thread-1获得了锁
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
	at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
	at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
	at java.base/java.lang.Thread.run(Thread.java:834)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
	at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
	at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
	at java.base/java.lang.Thread.run(Thread.java:834)
公平策略

使用无参构造函数创建的ReentrantLock默认是非公平锁,带参数的构造函数可以指定公平策略,true表示公平锁,false表示非公平锁。

公平锁是指先请求锁的线程一定先获得锁,反之非公平锁则是随机的。

示例:

代码语言:javascript复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            for(int i = 0; i < 2; i  ) {
                try {
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName()   "获得了锁");
                    Thread.sleep(3000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                } finally {
                    reentrantLock.unlock();
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);
        Thread thread4 = new Thread(runnable);
        Thread thread5 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}

锁的获取是随机的,即使刚释放锁的线程也有可能立即获得锁。

输出结果:

代码语言:javascript复制
Thread-0获得了锁
Thread-0获得了锁
Thread-1获得了锁
Thread-1获得了锁
Thread-2获得了锁
Thread-2获得了锁
Thread-3获得了锁
Thread-4获得了锁
Thread-4获得了锁
Thread-3获得了锁

修改ReentrantLock的创建方式,改为公平锁

代码语言:javascript复制
ReentrantLock reentrantLock = new ReentrantLock(true);

先请求锁的线程一定先获得锁,锁的获取是有序的。

输出结果:

代码语言:javascript复制
Thread-2获得了锁
Thread-1获得了锁
Thread-0获得了锁
Thread-3获得了锁
Thread-4获得了锁
Thread-2获得了锁
Thread-1获得了锁
Thread-0获得了锁
Thread-3获得了锁
Thread-4获得了锁

使用公平锁不会产生饥饿现象,由于要维护线程的顺序,使公平锁的效率低下,所以没特殊情况应该使用非公平锁。

和synchronized的比较

synchronized和ReentrantLock都属于独占锁,两者性能相差不大,但是在使用上,ReentrantLock要更灵活。

  • synchronized的加锁和解锁是自动进行的,易于操作,但不够灵活。ReentrantLock的加锁和解锁是手动进行的,不易操作,但更灵活。
  • ReentrantLock支持公平锁和非公平锁,而synchronized只支持非公平锁。
  • ReentrantLock可以关联多个条件队列,而synchronized最多只能关联一个条件队列。
  • 在等待锁的过程中,使用ReentrantLock能使等待的线程响应中断,而使用synchronized时,线程只能保持等待直到获取到锁后继续执行。
  • ReentrantLock支持锁申请等待超时,当在给定的时间内还没有获取到锁,则主动放弃获取锁。
  • ReentrantLock支持尝试获取锁,如果锁没有被其他线程占用,则获得锁成功并立即返回true,如果锁被其他线程所占用,则锁获取失败直接返回false,不会进行等待。
  • ReentrantLock和synchronized都是可重入锁。
源码分析

ReentrantLock和AQS的关系如下图:

ReentrantLock的内部类Sync继承自AQS,它的子类NonfairSync和FairSync分别实现了非公平锁和公平锁策略。

通过ReentrantLock的构造函数参数来指定公平策略:

代码语言:javascript复制
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

下面以非公平锁为例分析下加锁和解锁的过程。

加锁

非公平锁加锁调用的是NonfairSync的lock方法,默认state为0,通过CAS将state设置为1,设置成功则表示加锁成功,并设置当前线程为锁的持有者。设置失败则调用AQS的acquire方法。

代码语言:javascript复制
//java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
final void lock() {
	//CAS设置状态值
	if (compareAndSetState(0, 1))
		//设置当前线程为锁的持有者
		setExclusiveOwnerThread(Thread.currentThread());
	else
		//调用AQS的acquire方法
		acquire(1);
}

acquire方法,当tryAcquire方法返回false时,会把当前线程放入AQS阻塞队列。

代码语言:javascript复制
//java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

tryAcquire方法由AQS的子类实现,这里就是Sync以及它的子类来实现。

tryAcquire方法中会判断state值,如果state为0,则会尝试获取锁,使用CAS设置state的值,设置成功返回true,设置失败返回false。

如果state大于0,则判断持有锁的线程是不是当前线程,如果是则需要更新重入次数,重新设置state的值后返回true,反之则表示锁已经被其他线程所持有,直接返回false。

代码语言:javascript复制
//java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
	return nonfairTryAcquire(acquires);
}
代码语言:javascript复制
//java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	//获取锁的状态值
	int c = getState();
	//判断锁的状态,为0则表示锁没有被任何线程所持有
	if (c == 0) {
		//CAS设置状态值
		if (compareAndSetState(0, acquires)) {
			/设置当前线程为锁的持有者
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	//判断持有锁的线程是否为当前线程
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c   acquires;
		if (nextc < 0) // overflow
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}
解锁

非公平锁和公平锁的解锁过程是一致的,解锁unlock方法调用的是AQS的release方法,release方法会调用tryRelease方法尝试释放锁,释放锁成功之后的处理由AQS完成。同tryAcquire方法,tryRelease方法也是需要AQS子类实现的。

代码语言:javascript复制
//java.util.concurrent.locks.AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}
代码语言:javascript复制
//java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
	//减少重入次数
	int c = getState() - releases;
	//如果不是锁的持有者调用unlock方法则抛出异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	//如果重入次数为0,则清空锁的持有线程
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}

如果当前持有锁的线程不是当前线程则直接抛出异常,否则继续判断state的值,如果state为0,则清空锁的持有线程,如果state不为0,则减少重入次数。

参考资料: 《Java并发编程之美》 一文彻底理解ReentrantLock可重入锁的使用

0 人点赞