年前临放假我们一起学习了在Redis中如何实现分布式锁,过年期间在家里好吃好喝好玩完毕,现在,该继续学习啦!

不知诸位还是否记得上次我们说的《沙滩 - 脚印》那个例子,在Zookeeper中,实现分布式锁原理也差不多。如果你不知道,快回头先看看分布式锁之Redis实现

如果您对zookeeper还不熟悉,需要先去了解相关背景知识。

一、Zookeeper特性

在开始之前,我们重温一下zookeeper中的一些概念性知识。

1、数据节点

zookeeper的视图结构和文件系统类似,存储于内存。其中, 每个节点称为数据节点znode。每个znode可以存储数据,也可以挂载子节点。

比如在Dubbo中,我们将服务的信息注册到zookeeper中。就是由一个个的节点和子节点组成。

[zk: localhost:2181(CONNECTED) 1] ls /dubbo/com.viewscenes.netsupervisor.service.InfoUserService [consumers, configurators, routers, providers][zk: localhost:2181(CONNECTED) 2]

或者,我们将它理解为Windows系统中的文件夹,意思差不多的。

2、Watcher

Watcher(事件监听器),我们可以注册watcher监控znode的变化。比如znode删除、数据发生变化、子节点发生变化等。当触发这些事件时,客户端就会得到通知。 Watcher机制是Zookeeper实现分布式协调服务的重要特性。

3、节点类型

在zookeeper中,数据节点有着不同的类型。

  • 持久节点(PERSISTENT)

  • 持久顺序节点(PERSISTENT_SEQUENTIAL)

  • 临时节点(EPHEMERAL)

  • 临时顺序节点(EPHEMERAL_SEQUENTIAL)

持久节点,一旦被创建,除非主动进行删除操作,否则这个节点会一直保持。 临时节点,与客户端session绑定,一旦session失效,节点就会被自动删除。

有个重要的信息是,对于持久节点和临时节点,同一个znode下,节点的名称是唯一的! 就像在windows中,我们不可能在同一目录,创建两个相同名字的文件夹。记住它,这个实现分布式锁的基础。

二、实现

基于上面zookeeper的特性,对于分布式锁,我们就可以梳理出这样一条思路。

1、加锁,申请创建临时节点。 

2、创建成功,则加锁成功;完成自己的业务逻辑后,删除此节点,释放锁。

3、创建失败,则证明节点已存在,当前锁被别人持有;注册watcher,监听数据节点的变化。 

4、监听到数据节点被删除后,证明锁已被释放。重复步骤1,再次尝试加锁。

1、初始化

在构造方法中,我们先将zookeeper的客户端和锁的节点路径设置一下。

  1. public class ZookeeperLock implements Lock {


  2.    Logger logger = LoggerFactory.getLogger(this.getClass());

  3.    private String root_path = "/zookeeper";

  4.    private String current_path;

  5.    private ZooKeeper zooKeeper;


  6.    public ZookeeperLock(String lock_name,ZooKeeper zooKeeper){

  7.        current_path = root_path+"/"+lock_name;

  8.        this.zooKeeper = zooKeeper;

  9.    }

  10. }

2、加锁

上面我们说了,加锁就是创建节点的过程。代码也很简单。

  1. public class ZookeeperLock implements Lock {


  2.    /**

  3.     * 加锁

  4.     * 失败后调用waitForRelease方法等待。

  5.     */

  6.    public void lock() {

  7.        if (tryLock()){

  8.            logger.info("获取锁成功!");

  9.        }else {

  10.            waitForRelease();

  11.        }

  12.    }

  13.    /**

  14.     * 尝试加锁

  15.     * 创建临时节点,创建成功则加锁成功,返回true

  16.     * @return

  17.     */

  18.    public boolean tryLock() {

  19.        try {

  20.            zooKeeper.create(current_path, "1".getBytes(),

  21.                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

  22.            logger.info("创建节点成功!");

  23.            return true;

  24.        } catch (Exception e) {

  25.            logger.error("创建节点失败!{}",e.getMessage());

  26.        }

  27.        return false;

  28.    }

  29. }

3、等待锁释放

waitForRelease就是等待锁释放的方法。这里先判断一次锁的数据节点是否还存在,如果不存在,再次调用lock方法尝试加锁;如果存在,则通过countDownLatch来等待,直到触发NodeDeleted事件。

  1. public class ZookeeperLock implements Lock {


  2.    /**

  3.     * 注册Watcher 监听znode节点删除事件

  4.     */

  5.    private void waitForRelease(){

  6.        CountDownLatch countDownLatch = new CountDownLatch(1);


  7.        Watcher watcher = watchedEvent -> {

  8.            Watcher.Event.EventType type = watchedEvent.getType();

  9.            //触发NodeDeleted事件,跳过await方法

  10.            if (Watcher.Event.EventType.NodeDeleted.equals(type)){

  11.                countDownLatch.countDown();

  12.            }

  13.        };

  14.        try {

  15.            //判断当前锁的数据节点是否存在

  16.            Stat exists = zooKeeper.exists(current_path, watcher);

  17.            if (exists==null){

  18.                lock();

  19.            }else {

  20.                countDownLatch.await();

  21.                lock();

  22.            }

  23.        } catch (KeeperException e) {

  24.            e.printStackTrace();

  25.        } catch (InterruptedException e) {

  26.            e.printStackTrace();

  27.        }

  28.    }

  29. }

4、释放锁

我们把锁的数据节点删除之后,就释放了锁。

public class ZookeeperLock implements Lock {    public void unlock() {        try {            zooKeeper.delete(current_path,-1);        } catch (Exception e) {            logger.error("删除节点失败:{}",e.getMessage());        }        logger.info("释放锁成功!");    }}

5、测试

  1. public class ZkTest1 {


  2.    static final Logger logger = LoggerFactory.getLogger(ZkTest1.class);

  3.    static final int thread_count = 100;

  4.    static int num = 0;


  5.    public static void main(String[] args) throws IOException, InterruptedException {


  6.        CountDownLatch c1 = new CountDownLatch(1);

  7.        ZooKeeper zooKeeper = new ZooKeeper("192.168.139.131:2181", 99999, watchedEvent -> {

  8.            if (Watcher.Event.KeeperState.SyncConnected == watchedEvent.getState()) {

  9.                c1.countDown();

  10.            }

  11.        });

  12.        c1.await();


  13.        long start = System.currentTimeMillis();

  14.        ExecutorService executorService = Executors.newFixedThreadPool(thread_count);

  15.        CountDownLatch countDownLatch = new CountDownLatch(thread_count);

  16.        for (int i=0;i<thread_count;i++){

  17.            executorService.execute(() -> {

  18.                Lock lock = new ZookeeperLock("lock",zooKeeper);

  19.                try {

  20.                    lock.lock();

  21.                    num++;

  22.                }finally {

  23.                    if (lock!=null){

  24.                        lock.unlock();

  25.                    }

  26.                }

  27.                countDownLatch.countDown();

  28.            });

  29.        }

  30.        countDownLatch.await();

  31.        long end = System.currentTimeMillis();

  32.        logger.info("线程数量为:{},统计NUM数值为:{},共耗时:{}",thread_count,num,(end-start));

  33.        executorService.shutdown();

  34.    }

  35. }

大家可以运行测试一下,重点可以看看输出的日志,来帮助我们理解整个代码流程。这种方式有很大的性能问题,当请求数高了之后,会变得非常非常慢。

15:07:51.713 [main] INFO  - 线程数量为:100,统计NUM数值为:100,共耗时:162515:07:25.056 [main] INFO  - 线程数量为:1000,统计NUM数值为:1000,共耗时:125062

如上,在笔者虚拟机环境中的测试结果。问题出在哪呢?

三、改进版实现

我们说上面的代码有较大的性能问题,事实上造成这种问题的原因,有个专业名词:惊群效应。

惊群问题是计算机科学中,当许多进程等待一个事件,事件发生后这些进程被唤醒,但只有一个进程能获得CPU执行权,其他进程又得被阻塞,这造成了严重的系统上下文切换代价。

就好比,一只凶恶的大灰狼,跑进羊群。虽然一次只会有一只羊被吃掉,但可爱的羊儿们为了保住自己的小命,都会四处奔逃。没有被吃掉的羊儿,继续埋头吃草...直到下一只狼的到来,历史总是惊人的相似。

回到我们上面的代码中,多个线程都会注册Watcher事件,等待锁释放;当触发数据节点删除事件,线程被唤醒,然后争先抢后的尝试加锁。只有一个线程加锁成功,其余的线程继续阻塞,等待唤醒...

我们怎么改进这个问题呢?

1、临时顺序节点

上面我们说zookeeper数据节点分为4个类型,其中有一个就是临时顺序节点。

首先,它是一个临时节点;其次,它的节点名称是有顺序的。比如,我们在/lock节点创建几个临时顺序节点,它看起来是这样的:

[zk: localhost:2181(CONNECTED) 2] create -s -e /lock/test 1234Created /lock/test0000000230[zk: localhost:2181(CONNECTED) 3] create -s -e /lock/test 1234Created /lock/test0000000231[zk: localhost:2181(CONNECTED) 4] create -s -e /lock/test 1234Created /lock/test0000000232[zk: localhost:2181(CONNECTED) 5] create -s -e /lock/test 1234Created /lock/test0000000233[zk: localhost:2181(CONNECTED) 6] create -s -e /lock/test 1234Created /lock/test0000000234[zk: localhost:2181(CONNECTED) 7] create -s -e /lock/test 1234Created /lock/test0000000235[zk: localhost:2181(CONNECTED) 8] ls /lock[test0000000230, test0000000231, test0000000232, test0000000233, test0000000234, test0000000235][zk: localhost:2181(CONNECTED) 9]

可以看到,我们创建的/lock/test节点,都被加上了一个自增的序号。比如 test0000000230test0000000231这样。这个自增序号是zookeeper内部机制保证的,我们暂且不用管它怎么生成。

2、实现原理

基于zookeeper临时顺序节点的特性,针对惊群效应,业界又改进了一种实现方法。它的思路是这样的:

1、加锁,在/lock锁节点下创建临时顺序节点并返回,比如:test0000000235 

2、获取/lock节点下全部子节点,并排序。 

3、判断当前线程创建的节点,是否在全部子节点中顺序最小。

4、如果是,则代表获取锁成功;完成自己的业务逻辑后释放锁。 

5、如果不是最小,找到比自己小一位的节点,比如test0000000234;对它进行监听。 

6、当上一个节点删除后,证明前面的客户端已释放锁;然后尝试去加锁,重复以上步骤。 

3、实现

初始化

  1. public class ZkClientLock implements Lock {


  2.    private Logger logger = LoggerFactory.getLogger(ZkClientLock.class);

  3.    private String lock_path = "/lock";

  4.    private ZkClient client;

  5.    private CountDownLatch countDownLatch;


  6.    private String beforePath;// 当前请求的节点前一个节点

  7.    private String currentPath;// 当前请求的节点


  8.    public ZkClientLock(ZkClient client){

  9.        this.client = client;

  10.    }

  11. }

加锁

我们看加锁的过程,主要是判断自己是否处在全部子节点的第一位,是的话加锁成功;否则找到自己的上一个节点,调用waitForRelease方法等待锁释放。

  1. public class ZkClientLock implements Lock {


  2.    /**

  3.     * 加锁

  4.     * 如未成功获取锁,则等待锁释放后再次尝试加锁

  5.     */

  6.    public void lock() {

  7.        if (tryLock()){

  8.            logger.info("获取锁成功");

  9.        }else {

  10.            waitForRelease();

  11.            lock();

  12.        }

  13.    }

  14.    /**

  15.     * 当前节点排在全部子节点的第一位,则加锁成功

  16.     * 否则找出前一个节点

  17.     * @return

  18.     */

  19.    public boolean tryLock() {

  20.        if (currentPath==null || currentPath.length()==0){

  21.            currentPath = client.createEphemeralSequential(lock_path+"/","lock");

  22.            logger.info("当前锁路径为:{}",currentPath);

  23.        }

  24.        //获取全部子节点并排序

  25.        List<String> children = client.getChildren(lock_path);

  26.        Collections.sort(children);


  27.        //当前节点如果排在第一位,返回成功

  28.        if (currentPath.equals(lock_path+"/"+children.get(0))){

  29.            return true;

  30.        }else {

  31.            //找出上一个节点

  32.            String sequenceNodeName = currentPath.substring(lock_path.length()+1);

  33.            int i = Collections.binarySearch(children, sequenceNodeName);

  34.            beforePath = lock_path + '/' + children.get(i - 1);

  35.        }

  36.        return false;

  37.    }

  38. }

等待锁释放

这里就是对上一个节点进行监听,节点被删除后,方法返回。

  1. public class ZkClientLock implements Lock {


  2.    /**

  3.     * 等待锁释放

  4.     * 对上一个节点进行监听,节点删除后返回

  5.     */

  6.    private void waitForRelease(){


  7.        IZkDataListener listener = new IZkDataListener() {

  8.            public void handleDataDeleted(String dataPath) throws Exception {

  9.                if (countDownLatch != null) {

  10.                    countDownLatch.countDown();

  11.                }

  12.            }

  13.            public void handleDataChange(String dataPath, Object data) throws Exception {}

  14.        };

  15.        this.client.subscribeDataChanges(beforePath, listener);

  16.        if (this.client.exists(beforePath)) {

  17.            countDownLatch = new CountDownLatch(1);

  18.            try {

  19.                countDownLatch.await();

  20.            } catch (InterruptedException e) {

  21.                e.printStackTrace();

  22.            }

  23.        }

  24.        this.client.unsubscribeDataChanges(beforePath, listener);

  25.    }

  26. }

释放锁

public void unlock() {    client.delete(currentPath);    logger.info("释放锁成功");}

这种方式改进之后,性能会得到大幅度提升。同样的测试方法,得到结果如下:

15:19:19.327 [main] INFO  - 线程数量为:100,统计NUM数值为:100,共耗时:63615:18:58.728 [main] INFO  - 线程数量为:1000,统计NUM数值为:1000,共耗时:5398
四、Curator

我们上面写的两个锁实现,并不能用于生产环境中。还需要考虑很多细节才行,比如锁的可重入性、锁超时。 Curator是什么,想必不用再介绍。如果在生产环境中,使用到zookeeper分布式锁,笔者推荐使用开源组件,除非自己写的比人家的要好,哈哈。 首先,引入它的Maven坐标。

<dependency>    <groupId>org.apache.curator</groupId>    <artifactId>curator-framework</artifactId>    <version>4.0.1</version></dependency><dependency>    <groupId>org.apache.curator</groupId>    <artifactId>curator-recipes</artifactId>    <version>4.0.1</version></dependency>

1、用法

Curator帮我们封装了zookeeper分布式锁的实现逻辑,用起来非常简单。

首先,连接到zookeeper客户端。然后创建一个互斥锁的实例,调用即可。

  1. public static void main(String[] args) throws Exception {


  2.    RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

  3.    CuratorFramework client =

  4.            CuratorFrameworkFactory.newClient(

  5.                    "192.168.139.131:2181",

  6.                    999999,

  7.                    3000,

  8.                    retryPolicy);

  9.    client.start();


  10.    InterProcessMutex lock = new InterProcessMutex(client, "/lock");

  11.    //加锁

  12.    lock.acquire();

  13.    //解锁

  14.    lock.release();

  15. }

用同样的测试方法,得到结果如下:

15:43:34.306 [main] INFO  - 线程数量为:100,统计NUM数值为:100,共耗时:60615:43:02.427 [main] INFO  - 线程数量为:1000,统计NUM数值为:1000,共耗时:6785

2、InterProcessMutex

我们先看下InterProcessMutex类有哪些属性。

  1. public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex> {


  2.    //内部锁的实例对象

  3.    private final LockInternals internals;

  4.    //锁的基本路径

  5.    private final String basePath;

  6.    //线程和锁的映射

  7.    private final ConcurrentMap<Thread, InterProcessMutex.LockData> threadData;

  8.    //锁的名称前缀

  9.    private static final String LOCK_NAME = "lock-";

  10. }

这里的重点是threadData,它是一个ConcurrentMap对象,保存着当前线程和锁的映射关系,是实现可重入锁的基础。我们再看下LockData这个类。

  1. private static class LockData {


  2.    //当前线程

  3.    final Thread owningThread;

  4.    //当前锁的节点

  5.    final String lockPath;

  6.    //锁的次数

  7.    final AtomicInteger lockCount;

  8.    private LockData(Thread owningThread, String lockPath) {

  9.        this.lockCount = new AtomicInteger(1);

  10.        this.owningThread = owningThread;

  11.        this.lockPath = lockPath;

  12.    }

  13. }

接着再看InterProcessMutex的构造方法。

public InterProcessMutex(CuratorFramework client, String path) {    this(client, path, new StandardLockInternalsDriver());}

StandardLockInternalsDriver是锁的驱动类,它主要就是创建锁的节点和判断当前节点是不是处于第1 位。接着看,就是初始化一些属性。

  1. InterProcessMutex(CuratorFramework client, String path, String lockName,

  2.                int maxLeases, LockInternalsDriver driver) {


  3.    this.threadData = Maps.newConcurrentMap();

  4.    this.basePath = PathUtils.validatePath(path);

  5.    this.internals = new LockInternals(client, driver, path, lockName, maxLeases);

  6. }

3、加锁

加锁的方式有两种,有超时时间的和不带超时时间的。

lock.acquire();lock.acquire(5000, TimeUnit.SECONDS);

不过没关系, 它们都会调用到 internalLock方法。

  1. public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex> {


  2.    //加锁

  3.    private boolean internalLock(long time, TimeUnit unit) throws Exception {


  4.        //当前线程

  5.        Thread currentThread = Thread.currentThread();

  6.        InterProcessMutex.LockData lockData = this.threadData.get(currentThread);

  7.        //当前线程有锁的信息,锁次数加1,返回

  8.        if (lockData != null) {

  9.            lockData.lockCount.incrementAndGet();

  10.            return true;

  11.        } else {

  12.            //尝试加锁,返回当前锁的节点路径

  13.            String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());

  14.            if (lockPath != null) {

  15.                //将当前线程和锁的关系缓存到lockData

  16.                InterProcessMutex.LockData newLockData =

  17.                            new InterProcessMutex.LockData(currentThread, lockPath);

  18.                this.threadData.put(currentThread, newLockData);

  19.                return true;

  20.            } else {

  21.                return false;

  22.            }

  23.        }

  24.    }

  25. }

以上代码我们分为两部分来看。

  • 获取当前线程,从threadData缓存中获取锁的相关数据lockData。

  • 如果,lockData不为空,说明当前线程是一个重入锁,则锁次数加1,返回。

  • lockData为空就尝试去加锁。返回当前锁的节点路径,创建LockData 实例,放入到threadData缓存中。

然后我们接着看 attemptLock方法,它是如何尝试加锁的。

  1. public class LockInternals {


  2.    //尝试加锁

  3.    //如果加锁成功,则返回当前锁的节点路径

  4.    String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {

  5.        //方法执行开始时间

  6.        long startMillis = System.currentTimeMillis();

  7.        //锁超时时间

  8.        Long millisToWait = unit != null ? unit.toMillis(time) : null;

  9.        //锁节点数据

  10.        byte[] localLockNodeBytes = this.revocable.get() != null ? new byte[0] : lockNodeBytes;

  11.        int retryCount = 0;    

  12.        //是否已经持有锁

  13.        boolean hasTheLock = false;

  14.        boolean isDone = false;

  15.        while(!isDone) {

  16.            isDone = true;        

  17.            try {

  18.                //创建锁

  19.                ourPath = this.driver.createsTheLock(this.client, this.path, localLockNodeBytes);

  20.                //加锁

  21.                hasTheLock = this.internalLockLoop(startMillis, millisToWait, ourPath);

  22.            } catch (NoNodeException var14) {

  23.                if (!this.client.getZookeeperClient().getRetryPolicy().

  24.                        allowRetry(retryCount++, System.currentTimeMillis() - startMillis,

  25.                            RetryLoop.getDefaultRetrySleeper())) {

  26.                    throw var14;

  27.                }


  28.                isDone = false;

  29.            }

  30.        }

  31.        return hasTheLock ? ourPath : null;

  32.    }

  33. }

以上代码看着多,其实也不复杂,如果已经持有锁,就返回锁的节点路径。我们重点看两个地方。

  • 创建锁

创建锁就是在zookeeper中创建临时顺序节点,返回锁的节点名称。

  1. public class StandardLockInternalsDriver implements LockInternalsDriver {


  2.    //创建锁

  3.    public String createsTheLock(CuratorFramework client,

  4.                String path, byte[] lockNodeBytes) throws Exception {

  5.        String ourPath;

  6.        if (lockNodeBytes != null) {

  7.            //lockNodeBytes默认为空

  8.        } else {

  9.            ourPath = (String)((ACLBackgroundPathAndBytesable)client.create().

  10.                creatingParentContainersIfNeeded().

  11.                withProtection().

  12.                withMode(CreateMode.EPHEMERAL_SEQUENTIAL)).

  13.                forPath(path);

  14.        }

  15.        return ourPath;

  16.    }

  17. }

创建后锁的节点路径 = UUID + lock + zookeeper自增序列。它看起来是这样的: _c_d5815293-f5b1-484d-b789-9f11df69149c-lock-0000006168

  • 循环加锁

创建锁的节点之后,不断的循环去加锁,直到加锁成功或者锁超时退出。

  1. public class LockInternals {


  2.    private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {

  3.        //是否已经持有锁

  4.        boolean haveTheLock = false;

  5.        boolean doDelete = false;

  6.        try {

  7.            if (this.revocable.get() != null) {

  8.                ((BackgroundPathable)this.client.getData().usingWatcher(this.revocableWatcher)).forPath(ourPath);

  9.            }

  10.            //如果没有持有锁,就一直循环

  11.            while(this.client.getState() == CuratorFrameworkState.STARTED && !haveTheLock) {    

  12.                //获取所有子节点并排序

  13.                List<String> children = this.getSortedChildren();

  14.                //获取当前锁的自增序列

  15.                String sequenceNodeName = ourPath.substring(this.basePath.length() + 1);

  16.                //判断当前锁是否处于第1位

  17.                PredicateResults predicateResults = this.driver.getsTheLock(this.client,

  18.                                                    children, sequenceNodeName, this.maxLeases);


  19.                //处于第1位,返回true

  20.                if (predicateResults.getsTheLock()) {

  21.                    haveTheLock = true;

  22.                } else {

  23.                    //获取上一个序列路径

  24.                    String previousSequencePath = this.basePath + "/" + predicateResults.getPathToWatch();

  25.                    synchronized(this) {

  26.                        try {

  27.                            //对上一个序列路径进行监听,并等待

  28.                            ((BackgroundPathable)this.client.getData().usingWatcher(this.watcher)).forPath(previousSequencePath);

  29.                            if (millisToWait == null) {

  30.                                this.wait();

  31.                            } else {

  32.                                millisToWait = millisToWait - (System.currentTimeMillis() - startMillis);

  33.                                startMillis = System.currentTimeMillis();

  34.                                if (millisToWait > 0L) {

  35.                                    this.wait(millisToWait);

  36.                                } else {

  37.                                    doDelete = true;

  38.                                    break;

  39.                                }

  40.                            }

  41.                        } catch (NoNodeException var19) {

  42.                        }

  43.                    }

  44.                }

  45.            }

  46.        } catch (Exception var21) {

  47.            ThreadUtils.checkInterrupted(var21);

  48.            doDelete = true;

  49.            throw var21;

  50.        } finally {

  51.            if (doDelete) {

  52.                this.deleteOurPath(ourPath);

  53.            }


  54.        }

  55.        return haveTheLock;

  56.    }

  57. }

以上代码比较长,但逻辑比较清晰。我们重点看while循环内的代码。

  • 获取所有子节点并排序。

  • 获取当前锁的节点名称,判断是否处于全部子节点的第1位。

  • 如果是,则返回true,退出循环。

  • 如果不是,则对上一个序列节点进行监听,并等待。

  • 如果没有锁超时时间,则一直等待;反之等待millisToWait时间后,退出循环,并删除当前锁的节点,返回false。

4、解锁

既然是可重入锁,解锁的时候必然先判断锁的重入次数。当次数为0时,删除zookeeper中的锁节点信息等。

  1. public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex> {


  2.    //解锁

  3.    public void release() throws Exception {


  4.        //获取当前线程的锁相关信息lockData

  5.        Thread currentThread = Thread.currentThread();

  6.        InterProcessMutex.LockData lockData = this.threadData.get(currentThread);

  7.        if (lockData == null) {

  8.            throw new IllegalMonitorStateException("You do not own the lock: " + this.basePath);

  9.        } else {

  10.            //锁次数递减1

  11.            int newLockCount = lockData.lockCount.decrementAndGet();

  12.            if (newLockCount <= 0) {

  13.                if (newLockCount < 0) {

  14.                    throw new IllegalMonitorStateException("....");

  15.                } else {

  16.                    try {

  17.                        //移除Watch、删除锁节点、移除线程和锁的映射关系

  18.                        this.client.removeWatchers();

  19.                        this.revocable.set((Object)null);

  20.                        this.deleteOurPath(lockPath);

  21.                    } finally {

  22.                        this.threadData.remove(currentThread);

  23.                    }


  24.                }

  25.            }

  26.        }

  27.    }

  28. }

至此,Curator中关于互斥锁的实现逻辑我们已经分析完了。可以看到,它的实现原理跟我们自己改进后的代码实现基本上是相同的。不过,多了锁的可重入和锁超时。