Java并发编程——独占锁ReentrantLock

PunkLu 2020年01月14日 56次浏览
独占锁ReentrantLock

独占锁ReentrantLock

ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他锁获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。

ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是一个公平锁还是非公平锁,默认是非公平锁。

public ReentrantLock(){
	sync = new NonfairSync();
}

public ReentrantLock(boolean fair){
	sync = fair ? new FairSync() : new NonfairSync();
}

其中Sync类直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的非公平和公平策略。

在这里,AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,状态值被设置为2,这就是可重入次数。在该线程释放该锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。

获取锁

1、void lock()方法

当一个线程调用该方法时,说明该线程希望获取该锁。如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。如果当前线程之前已经获取过该锁,则这次只是简单地把AQS的状态值加1后返回。如果该锁已经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起。

public void lock(){
	sync.lock();
}

在如上代码中,ReentrantLock的lock()委托给了sync类,根据创建ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync,这个锁是一个非公平锁或者公平锁。先看sync的子类NonfairSync的情况,也就是非公平锁时:

final void lock(){
	// 1、CAS设置状态值
	if(compareAndSetState(0,1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		// 2、调用AQS的acquire方法
		acquire(1);
}

在代码1中,因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1,CAS成功则表示当前线程获取到了锁,然后setExclusiveOwnerThread设置该锁持有者是当前线程。

如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然会会调用AQS的acquire方法。传递参数为1,再贴下AQS的acquire的核心代码:

public final void acquire(int arg){
	// 3、调用ReentrantLock重写的tryAcquire方法
	if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
	selfInterrupt();
}

AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制,所以代码3会调用ReentrantLock重写的tryAcquire方法。先看下非公平锁的代码:

protected final boolean tryAcquire(int acquires){
	return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires){
	final Thread current = Thread.currentThread();
	int c = getState();
	// 4、当前AQS状态值为0
	if(c == 0){
		if(compareAndSetState(0,acquires)){
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	// 5、当前线程是该锁持有者
	else if(current == getExclusiveOwnerThread()){	
		int nextc = c + acquires;
		if(nextc < 0){
			throw new Error("Maximum lock count exceeded");
		}
		setState(nextc);
		return true;
	}
	// 6
	return false;
}

首先代码4会查看当前锁的状态值是否为0,为0则说明当前该锁空闲,那么就尝试CAS获取该锁,将AQS的状态值设置为1,并设置当前锁的持有者为当前线程然后返回true。如果当前状态值不为0则说明该锁已经被某个线程持有,所以代码5查看当前线程是否是该锁的持有者,如果当前线程是该锁的持有者,则状态值加1,然后返回true,如果当前线程不是锁的持有者则返回false,然后其会被放入AQS阻塞队列。

非公平锁是说先尝试获取并不一定比后尝试获取锁的线程优先获取锁。假设线程A调用lock()方法时执行到nonfairTryAcquire的代码4,发现当前状态值不为0,所以执行代码5,发现当前线程不是线程持有者,则执行代码6返回false,然后当前线程被放入AQS阻塞队列。

这时候线程B也调用了lock()方法执行到nonfairTryAcquire的代码4,发现当前状态值为0了(假设占用该锁的其他线程释放了该锁),所以通过CAS设置获取到了该锁。虽然是线程A先请求获取该锁,但是却被线程B获取到了,这就是非公平的体现。这里获取锁前并没有查看当前AQS队列里面是否有比自己更早请求该锁的过程,而是使用了抢夺策略。下面是公平锁的FairSync重写的tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 7、当前AQS状态值为0
            if (c == 0) {
            	// 8、公平性策略
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 9、当前线程是该锁持有者
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 10
            return false;
}

如以上代码所示,公平的tryAcquire策略与非公平的类似,不同之处在于,代码8在设置CAS前添加了hasQueuedPredecessors方法,该方法是实现公平性的核心代码,代码如下:

public final boolean hasQueuedPredecessors(){
	Node t  = tail;
	Node h = head;
	Node s;
	return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

在如上代码中,如果当前线程节点有前驱节点则返回true,否则如果当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。其中如果h==t则说明当前队列为空,直接返回false;如果h!=t并且s==null则说明有一个元素将要作为AQS的第一个节点入队列(enq函数的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵头节点后面),那么返回true,如果h!=t并且s!=null和s.thread != Thread.currentThread()则说明队列里面的第一个元素不是当前线程,那么返回true。

2、void lockInterruptly()方法

该方法与lock()方法类似,它的不同之处在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。

public void lockInterrupptibly() throws InterruptiblyException{
    sync.acquireInterruptibly();
}

public final void acquireInterruptibly(int arg) throws InterruptiblyException{
    // 如果当前线程被中断,则直接抛出异常
    if(Thread.interrupted()){
        throw new InterruptedException();
    }
    
    // 尝试获取资源
    if(!tryAcquire(arg)){
        // 调用AQS可被中断的方法
        doAcquireInterruptibly(arg);
    }
}

3、boolean tryLock()方法

尝试获取锁。如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。该方法不会引起当前线程阻塞。

public boolean tryLock(){
    return sync.nonfairTryAcquire(1);
}

final boolean nonfairTryAcquire(int acquires){
    final Thread current = Thread.currentThread();
    int c = getState();
    if(c == 0){
        if(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;
}

如上代码与非公平锁的tryAcquire()方法代码类似,所以tryLock()使用的是非公平策略。

4、boolean tryLock(long timeout,TimeUnit unit)方法

尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间到没有获取到该锁则返回false。

public boolean tryLock(long timeout,TimeUnit unit) throws InterruptedExcption{
    // 调用AQS的tryAcquireNanos方法
    return sync.tryAcquireNanos(1,unit.toNanos(timeout));
}

释放锁

1、void unlock()方法

尝试释放锁,如果当前线程池有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常,代码如下:

public void unlock(){
    sync.release(1);
}

protected final boolean tryRelease(int releases){
    // 11 如果不是锁持有者调用unlock则抛出异常
    int c = getState() - releases;
    if(Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 12 如果当前可重入次数为0则清空锁持有线程
    if(c == 0){
        free = true;
        setExclusiveOwnerThread(null);
    }
    
    // 13 设置可重入次数
    setState(c);
    return free;
}

如代码11所示,如果当前线程不是该锁持有者则直接抛出异常,否则查看状态值是否为0,为0则说明当前线程要放弃对该锁的持有权,则执行代码12把当前锁持有者设为null。如果状态值不为0,则仅仅让当前线程对该锁的可重入次数减1。

案例

使用ReentrantLock来实现一个简单的线程安全的list。

public static class ReentrantLockList{
    // 线程不安全的list
    private ArrayList<String> array = new ArrayList<String>();
    // 独占锁
    private volatile ReentrantLock lock = new ReentrantLock();
    
    // 添加元素
    public void add(String e){
        lock.lock();
        try{
            array.add(e);
        }finally{
            lock.unlock();
        }
    }
    
    // 删除元素
    public void remove(String e){
        lock.lock();
        try{
            array.remove(e);
        }finally{
            lock.unlock();
        }
    }
    
    // 获取数据
    public String get(int index){
        lock.lock();
        try{
            return array.get(index);
        }finally{
            lock.unlock();
        }
    }
}

如上代码通过在操作array元素前进行加锁保证同一时间只有一个线程可以对array数组进行修改,但是也只能有一个线程对array元素进行访问。