前言

    最近在用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<Runnable>(1000));

        List<Callable<Object>> callables = Lists.newArrayList();

        long start = System.currentTimeMillis();

        for (int i = 0; i < 100; i++) {
            Callable<Object> runnable = new Callable<Object>() {
                //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<Future<Object>> futures = pool.invokeAll(callables);

        for (Future<Object> 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<Lease> 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<Lease> 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<Lease> 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<String> 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<String> 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测试,其代码实现也很复杂,可以试试用其他的工具测一下两者的区别。