前言

最近在用Apache的Zookeeper客户端库Curator,Curator实现了一套的分布式锁,有可重入和不可重入,想起其实在单机环境下,Java提供的synchronized 和 ReentrantLock的锁工具,这两个都是可重入锁,所以可重入锁和不可重入锁有什么区别呢,带着这个问题,去网上找答案。

主题

很多的博客上都是列了怎么实现这两种锁,例如像下面的两段代码:

public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}

上面实现的是一个不可重入锁,下面这段实现的是一个可重入锁:

public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock() throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}

从代码实现来看,可重入锁增加了两个状态,锁的计数器和被锁的线程,实现基本上和不可重入的实现一样,如果不同的线程进来,这个锁是没有问题的,但是如果进行递归计算的时候,如果加锁,不可重入锁就会出现死锁的问题。

所以这个不可重入是对同一个线程而言,能否第二次获取锁,下面是另一篇博客总结的:

可重入锁:可以再次进入方法A,就是说在释放锁前此线程可以再次进入方法A(方法A递归)。

不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。

那这两种锁除了在可能会导致死锁方面的区别外,效率有差别了,我就利用Curator做了一个实验,实验的代码如下:

private int count = 0;
@Test
public void testDistribute() throws InterruptedException, ExecutionException {
startClient();
ThreadPoolExecutor pool = new ThreadPoolExecutor(4, Runtime.getRuntime().availableProcessors(),
5000, TimeUnit.SECONDS, new ArrayBlockingQueue(1000));
List> callables = Lists.newArrayList();
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
Callable runnable = new Callable() {
//InterProcessMutex lock = new InterProcessMutex(client,ZOOKEEPER_PATH); //可重入锁
InterProcessSemaphoreMutex lock = new InterProcessSemaphoreMutex(client,ZOOKEEPER_PATH); //不可重入锁
@Override
public Object call() {
String name = Thread.currentThread().getName();
System.out.println("current thread name is " + name);
try {
if (lock.acquire(10*1000,TimeUnit.SECONDS)) {
count ++;
Thread.sleep(500);
}
} catch (Exception e) {
System.out.println("====" + e.getMessage());
} finally {
try {
lock.release();
} catch (Exception e) {
System.out.println("===== lock release ");
}
}
return count;
}
};
callables.add(runnable);
}
List> futures = pool.invokeAll(callables);
for (Future f: futures ) {
Object o = f.get();
System.out.println("future get is " + o);
}
long end = System.currentTimeMillis();
System.out.println("time spend = " + (end - start));
}
/**
* must be priority running in testcase
*/
private void startClient() {
RetryPolicy policy = new ExponentialBackoffRetry(SLEEP_TIME, MAX_RETRIES);
client = CuratorFrameworkFactory.newClient(ZOOKEEPER_ADDRESS, policy);
client.start();
}

在跑上面的测试用例的时候,请分别放开上面的可重入锁和不可重入锁:

不可重入锁的花费的时间是:time spend = 91544

可重入锁的花费时间是:time spend = 52796

我在想为什么这两种的实现的效率会差这么多,于是去看了下两种锁的源码,第一个是可重入锁的关键实现代码,第二个是不可重入的关键实现代码:

private boolean internalLock(long time, TimeUnit unit) throws Exception {
Thread currentThread = Thread.currentThread();
InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
if(lockData != null) {
lockData.lockCount.incrementAndGet();
return true;
} else {
String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());
if(lockPath != null) {
InterProcessMutex.LockData newLockData = new InterProcessMutex.LockData(currentThread, lockPath, null);
this.threadData.put(currentThread, newLockData);
return true;
} else {
return false;
}
}
}
public Collection acquire(int qty, long time, TimeUnit unit) throws Exception
{
long startMs = System.currentTimeMillis();
boolean hasWait = (unit != null);
long waitMs = hasWait ? TimeUnit.MILLISECONDS.convert(time, unit) : 0;
Preconditions.checkArgument(qty > 0, "qty cannot be 0");
ImmutableList.Builder builder = ImmutableList.builder();
boolean success = false;
try
{
while ( qty-- > 0 )
{
int retryCount = 0;
long startMillis = System.currentTimeMillis();
boolean isDone = false;
while ( !isDone )
{
switch ( internalAcquire1Lease(builder, startMs, hasWait, waitMs) )
{
case CONTINUE:
{
isDone = true;
break;
}
case RETURN_NULL:
{
return null;
}
case RETRY_DUE_TO_MISSING_NODE:
{
// gets thrown by internalAcquire1Lease when it can't find the lock node
// this can happen when the session expires, etc. So, if the retry allows, just try it all again
if ( !client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )
{
throw new KeeperException.NoNodeException("Sequential path not found - possible session loss");
}
// try again
break;
}
}
}
}
success = true;
}
finally
{
if ( !success )
{
returnAll(builder.build());
}
}
return builder.build();
}
private InternalAcquireResult internalAcquire1Lease(ImmutableList.Builder builder, long startMs, boolean hasWait, long waitMs) throws Exception
{
if ( client.getState() != CuratorFrameworkState.STARTED )
{
return InternalAcquireResult.RETURN_NULL;
}
if ( hasWait )
{
long thisWaitMs = getThisWaitMs(startMs, waitMs);
if ( !lock.acquire(thisWaitMs, TimeUnit.MILLISECONDS) )
{
return InternalAcquireResult.RETURN_NULL;
}
}
else
{
lock.acquire();
}
Lease lease = null;
try
{
PathAndBytesable createBuilder = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL);
String path = (nodeData != null) ? createBuilder.forPath(ZKPaths.makePath(leasesPath, LEASE_BASE_NAME), nodeData) : createBuilder.forPath(ZKPaths.makePath(leasesPath, LEASE_BASE_NAME));
String nodeName = ZKPaths.getNodeFromPath(path);
lease = makeLease(path);
if ( debugAcquireLatch != null )
{
debugAcquireLatch.await();
}
try
{
synchronized(this)
{
for(;;)
{
List children;
try
{
children = client.getChildren().usingWatcher(watcher).forPath(leasesPath);
}
catch ( Exception e )
{
if ( debugFailedGetChildrenLatch != null )
{
debugFailedGetChildrenLatch.countDown();
}
returnLease(lease); // otherwise the just created ZNode will be orphaned causing a dead lock
throw e;
}
if ( !children.contains(nodeName) )
{
log.error("Sequential path not found: " + path);
returnLease(lease);
return InternalAcquireResult.RETRY_DUE_TO_MISSING_NODE;
}
if ( children.size() <= maxLeases )
{
break;
}
if ( hasWait )
{
long thisWaitMs = getThisWaitMs(startMs, waitMs);
if ( thisWaitMs <= 0 )
{
returnLease(lease);
return InternalAcquireResult.RETURN_NULL;
}
wait(thisWaitMs);
}
else
{
wait();
}
}
}
}
finally
{
client.removeWatchers();
}
}
finally
{
lock.release();
}
builder.add(Preconditions.checkNotNull(lease));
return InternalAcquireResult.CONTINUE;
}

首先可重入锁的关键代码逻辑非常简单,而且使用了Atomic原子操作,效率非常高,但是不可重入锁代码量非常大,为了实现一个类似于Semaphore的工具,进行很多的判断,效率非常低,有兴趣的可以升入研究下这两种锁。

结论

重入锁和不可重入锁主要的差别在对相同线程是否能够重复获取,从效率来说,不可重入锁效率更高,当然这个是用Curator client测试,其代码实现也很复杂,可以试试用其他的工具测一下两者的区别。