重入锁ReentrantLock

线程的同步控制,最基础的就是synchronized关键字。但是在使用的过程中有一些限制,并没有那么的灵活。

可重入

所以下面介绍一下ReentrantLock的使用和相关特性,一个简单的demo如下:

public class ReenterLock implements Runnable {

	public static ReentrantLock lock = new ReentrantLock();
	public static int i = 0;

	@Override
	public void run() {
		for (int j = 0; j < 10000000; j++) {
			lock.lock();
			try {
				i++;
			} finally {
				lock.unlock();
			}
		}
	}

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

}

上面的demo创建了两个线程 t1、t2,并且同时使用了lock作为锁,完成了i++的操作。这里面需要注意的是,lock.lock()调用了几次,响应的lock.unlock()就应该调用几次。并且无论执行体如何,最终都要调用lock.unlock()。

响应中断

在两个线程同时抢占同一个锁资源的时候,很有可能产生死锁的现象。ReentrantLock可以支持中断响应,从而规避死锁的产生。

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 (Exception e) {
				}
				lock2.lockInterruptibly();
			} else {
				lock2.lockInterruptibly();
				try {
					Thread.sleep(500);
				} catch (Exception e) {
				}
				lock1.lockInterruptibly();
			}

		} catch (Exception 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线程
		t2.interrupt();
	}

}

从上面代码可以看出来,此时使用的是lock.lockInterruptibly(),而不是lock.lock()。当使用lock.lockInterruptibly()加锁等待的时候,可以响应中断,从而使线程对出死锁状态。

上面代码一共起了两个线程,故意造成了死锁的状态。此时在主线程中调用了中断方法,此时t2线程对出等待,释放锁资源,从而可以让t1继续执行下去。

超时限制

当多线程对同一监视器对象加锁时,如果超过一定时间还没获得锁,则立即返回false。这里使用到的是tryLock()。

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("I got lock failed.");
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (lock.isHeldByCurrentThread()) {
				lock.unlock();
			}
		}
	}

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

}

上述代码启动了2个线程,在5S内必定一个线程获取锁对象失败。

另外tryLock()不带参的方法,表示对当前对象加锁,如果获取不到则立即返回false。通过这个特性,可以在程序中用无限循环,使其不断的去尝试加锁。

public class TryLock implements Runnable {

	public static ReentrantLock lock1 = new ReentrantLock();
	public static ReentrantLock lock2 = new ReentrantLock();
	int lock;

	public TryLock(int lock) {
		this.lock = lock;
	}

	@Override
	public void run() {
		if (lock == 1) {
			while (true) {
				if (lock1.tryLock()) {
					try {
						try {
							Thread.sleep(500);
						} catch (Exception e) {
						}
						if (lock2.tryLock()) {
							try {
								System.out.println(Thread.currentThread().getId() + ": my job done");
								return;
							} finally {
								lock2.unlock();
							}
						}
					} finally {
						lock1.unlock();
					}
				}
			}
		} else {
			while (true) {
				if (lock2.tryLock()) {
					try {
						try {
							Thread.sleep(500);
						} catch (Exception e) {
						}
						if (lock1.tryLock()) {
							try {
								System.out.println(Thread.currentThread().getId() + ": my job done");
								return;
							} finally {
								lock1.unlock();
							}
						}
					} finally {
						lock2.unlock();
					}
				}
			}
		}

	}

	public static void main(String[] args) {
		TryLock r1 = new TryLock(1);
		TryLock r2 = new TryLock(2);
		Thread t1 = new Thread(r1);
		Thread t2 = new Thread(r2);
		t1.start();
		t2.start();
	}

}

公平锁性

在大部分的应用场景下,锁的申请获取都是不公平的。但是我们通过ReentrantLock可以构造出公平的锁对象。

public ReentrantLock(boolean fair)

使用上述方法可以构造出公平锁出来,我们通常默认的fair都是false。公平锁本身会维护一个有序队列,开销相对较高。

公平锁实现

ReentrantLock 的公平锁和非公平锁都委托了 AbstractQueuedSynchronizer#acquire 去请求获取。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

公平锁FairSync

公平锁的实现在于,当有线程获取锁资源时, 都会检查有没有等待队列。如果有,则按先进先出的原则进行。

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

非公平锁NonfairSync

加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列。

下面看一个公平锁的测试Demo

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 r1 = new FairLock();
		Thread t1 = new Thread(r1, "thread t1");
		Thread t2 = new Thread(r1, "Thread t2");
		t1.start();
		t2.start();
	}

}

通过运行上述程序可以看出,锁的获取都是有序的,公平的。

综上所述

一般来说,根据JVM的调度,系统更倾向于把锁给当前已经获得锁的线程。从而减少线程频繁切换带来的系统消耗。

对于ReentrantLock主要有以下几个方法:

  • lock():获取锁,如果已被占用,则等待;
  • lockInterruptibly():获得锁,但是响应中断;
  • tryLock():获得锁,如可以立即获得,则返回true,如不可以,则立即返回false;
  • tryLock(long time, TimeUnit unit):超时未获得锁,则返回;
  • unlock():释放锁;