02_zookeeper分布式锁源码分析

一. 可重入的公平锁

1.1 基本的使用方式

private static CuratorFramework getConnection() {
	RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

	CuratorFramework client = CuratorFrameworkFactory.newClient(
			"xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181",
			retryPolicy);

	client.start();

	return client;
}

private static void createMutexTest(CuratorFramework client, String lockPath) throws Exception {
	InterProcessMutex lock = new InterProcessMutex(client, lockPath);

	lock.acquire();

	TimeUnit.SECONDS.sleep(3);

	lock.release();
}

public static void main(String[] args) throws Exception {
	// 启动zookeeper客户端,获取一个zookeeper连接
	CuratorFramework client = getConnection();
    // 测试可重入锁
	createMutexTest(client, "/locks/lock_01");
}

1.2 初次加锁

1.InterProcessMutex acquire( )

public void acquire() throws Exception
{
	if ( !internalLock(-1, null) )
	{
		throw new IOException("Lost connection while trying to acquire lock: " + basePath);
	}
}

调用internalLock( )时,传入的time=-1,Unit=null。

2.InterProcessMutex internalLock( )

private boolean internalLock(long time, TimeUnit unit) throws Exception
{
	/*
	   Note on concurrency: a given lockData instance
	   can be only acted on by a single thread so locking isn't necessary
	*/
	Thread          currentThread = Thread.currentThread();
	LockData        lockData = threadData.get(currentThread);
	if ( lockData != null )
	{
		// re-entering
		lockData.lockCount.incrementAndGet();
		return true;
	}

	String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
	if ( lockPath != null )
	{
		LockData        newLockData = new LockData(currentThread, lockPath);
		threadData.put(currentThread, newLockData);
		return true;
	}

	return false;
}

通过源码上的注释,我们得知,虽然InterProcessMutex可能会被不同的线程使用,但是呢,每个线程都会有对应的lockData,lockData是线程独享的。

这里的threadData和lockData都非常重要,当前线程是否持有锁,可重入加锁的实现逻辑等等都与它息息相关,所以,我们来看看threadData的源码吧。

private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();

private static class LockData
{
	final Thread        owningThread;
	final String        lockPath;
	final AtomicInteger lockCount = new AtomicInteger(1);

	private LockData(Thread owningThread, String lockPath)
	{
		this.owningThread = owningThread;
		this.lockPath = lockPath;
	}
}

threadData是一个支持并发的Map结构,Key是线程对象,Value是LockData,也就是该线程持有锁的相关信息。

LockData由线程对象、锁的路径、加锁的次数,这三个变量构成,第一次初始化后,加锁的次数等于1。

回到internalLock( )内,初次加锁时,threadData必然是空的,更别提lockData了。此时就会继续往下执行,进入LockInternals的attemptLock( )。

 String ourPath = client.create()
 .creatingParentsIfNeeded().withProtection()
 .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);

creatingParentsIfNeeded() :如果父节点不存在,则创建父节点。
withMode(CreateMode.EPHEMERAL_SEQUENTIAL): 创建节点的方式是临时顺序节点。
path: /locks/lock_01/lock-

最终计算出的ourPath=/locks/lock_01/_c_aa0701c8-a84a-4c1e-9590-0b61b20f7db8-lock-0000000001

hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);

3.LockInternals internalLockLoop( )

查看当前线程是否持有这把锁。

获取到父目录下的所有子节点,每个子节点代表着一个线程发起获取锁的请求,然后呢,会对子节点进行排序,默认是升序排序,也就是说,先申请锁的节点会被排在队头,后申请锁的节点会被排到队尾。

4.StandardLockInternalsDriver getsTheLock( ) 核心方法

属于第三步的核心方法,用于校验当前线程是否能成功的获取锁。具体的代码如下:

public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
	int             ourIndex = children.indexOf(sequenceNodeName);
	validateOurIndex(sequenceNodeName, ourIndex);

	boolean         getsTheLock = ourIndex < maxLeases;
	String          pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);

	return new PredicateResults(pathToWatch, getsTheLock);
}

查看本次的节点处于所有子节点的哪个位置,由于是初次获取锁,此时outIndex必然等于0。

看到boolean getsTheLock = ourIndex < maxLeases,由于maxLeases等于1,getsTheLock=0<1=true。这就表明,当前线程成功获取锁。

5.LockInternals internalLockLoop( )的剩余代码
回到InterProcessMutex的internalLock( )方法,此时我们已经获取到了锁,并且知道了锁对应节点的路径。所以会根据当前线程和锁路径创建出LockData,最后放到threadData中。

1.3 同客户端、同线程,重入加锁的过程

相同客户端、相同线程进行加锁时,请求会落入InterProcessMutex的internalLock( )。此时,threadData有值,并且lockData也不为空。所以会执行以下语句,将加锁次数加1,然后就执行返回了,锁获取成功,就这么简单。

注意: Curator框架重入加锁时,不会对zookeeper发起任何请求,这一点与Redis不同。

lockData.lockCount.incrementAndGet();

1.4 加锁时的锁互斥

相同的客户端,不同的线程,或者不同的客户端尝试获取一把已经被其他人持有的锁时,又会发生什么呢?

我们还是回到InterProcessMutex的internalLock( ),由于当前客户端尚未获得这把锁,所以lockData一定为空。

接着就开始尝试获取锁了。

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

就像1.1节中写的那样,获取锁的过程无非就是查询锁目录下的所有节点,组成一个list,查看当前节点是不是list中的头节点,由于已经有人持有锁,所以当前节点肯定不是list的下标为0的节点。

此时,我们拿到当前节点的前一个节点的绝对路径,对其做一个监听器,然后就用Object wait( )陷入无限的等待。

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

synchronized(this) {
	 Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
	 ... 代码省略
}

过了一段时间后,这把锁被释放,对应在zookeeper的节点也会被回收,此时zookeeper会通知我们之前配置好的监听器,由监听器调用Object notifyAll( ),唤醒所有处于等待中的线程。于是,我们进入下一轮的循环,继续尝试获取锁。之后重复上述过程,直到我们前面没有其它节点,获取锁成功。

@Override
private final Watcher watcher = new Watcher()
{
	@Override
	public void process(WatchedEvent event)
	{
		notifyFromWatcher();
	}
};

从这里也不难看出,curator底层实现zookeeper分布式锁时使用了的数据结构是顺序节点,严格的控制了客户端获取锁的顺序。想要获得锁,就必须等待比你之前获取锁的客户端释放锁,否则只能老老实实的处于wait状态。

1.5 释放锁

既然是释放锁,那么代码转到InterProcessMutex的release( )。

首先,判断待释放锁的客户端是不是目前持有这把锁的线程,如果不是,则直接抛出异常。

接着,减小当前线程对于这把锁的加锁次数。

最后,观察锁的加锁次数,如果加锁次数大于0,说明当前线程对这把锁进行了多次请求,而每次加锁都需要对应一次解锁,这里还有剩余的加锁次数需要扣减,所以直接返回,表示解锁成功。如果加锁次数正等于0,说明本次解锁,刚好对应上了加锁的次数,当前线程已经不再需要这把锁,此时会调用LockInternals的releaseLock( )方法释放这把锁,底层其实就是去删除对应路径的节点(就是/leases目录下对应的临时顺序节点)。

@Override
public void release() throws Exception
{
	Thread      currentThread = Thread.currentThread();
	LockData    lockData = threadData.get(currentThread);
	if ( lockData == null )
	{
		throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
	}

	int newLockCount = lockData.lockCount.decrementAndGet();
	if ( newLockCount > 0 )
	{
		return;
	}
	if ( newLockCount < 0 )
	{
		throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
	}
	try
	{
		internals.releaseLock(lockData.lockPath);
	}
	finally
	{
		threadData.remove(currentThread);
	}
}

1.6 同一个InterProcessMutex对象是否支持多个线程使用?

答: 支持。在InterProcessMutex的源码中,我们知道threadData是一个支持并发的ConcurrentMap数据结构,会为每个线程维护独立的LockData对象,存放了对应线程的锁信息。既然的锁信息是独立的,那么多个线程使用同一个InterProcessMutex当然也是可以的。

二. Semaphore信号量

2.1 基本的使用方式

private static void semaphoreTest(CuratorFramework client) throws Exception {
	int maxLease = 3;
	String path = "/semaphore/semaphore_01";
	InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, path, maxLease);

	Lease lease = semaphore.acquire();

	TimeUnit.SECONDS.sleep(3);

	semaphore.returnLease(lease);
}

maxLease: 信号量的个数。同时允许获取信号量的最大线程数量。

上述代码中,同时只允许最多3个线程获取信号量,其它的线程想要获取信号量,必须等到其它线程归还信号量才可以。

2.2 获取信号量

假设有这样一个场景:若某个线程希望获取锁,且这个信号量还有空闲的锁。

1.获取第一把锁 /locks
获取第一把锁“/locks”,它的路径为: /semaphore/semaphore_01/locks。在获取信号量之前,必须保证能获取这把锁。

这是一把可重入的、公平的、互斥锁。若已经有其他人获取到这把锁了,则当前线程会陷入等待,直到获取到这把锁。

2.获取第二把锁 /leases
只要能获取到/locks这把锁,那么当前线程就需要向xxx/leases目录下创建临时顺序节点。

接着,查看xxx/leases目录下的节点个数,若节点个数 <= 信号量的个数,那么获取信号量成功。若不满足,则当前线程会陷入等待状态,并监听xxx/leases目录下的所有节点,只要有任何一个节点被删除,说明有人归还了信号量,此时线程就会被唤醒,并再一次查询xxx/leases目录下节点的个数,之后不断地循环上述过程,直到能成功获得信号量为止。

可能有人觉得奇怪,为什么要先添加临时顺序节点,然后再检查目录下节点的个数?难道就不怕在当前线程的等待过程中,其它的线程过来尝试添加临时顺序节点吗?不用担心,因为其它线程根本就不可能执行到添加临时顺序节点的代码位置,还记得第一把锁吗,此时当前线程处于wait状态,尚未释放第一把锁,因此其它线程都会阻塞在获取第一把锁的地方!

3. 释放第一把锁 /locks
执行InterProcessMutex release()释放锁。

总结一下,Curator对于semaphore的实现,无非就是搞了两把锁。

第一把锁的作用:控制所有的客户端严格按照顺序来获取信号量,保证并发下的顺序性。

第二把锁的作用:保证只有有限个信号量可以供客户端获取。信号量的个数是通过限制指定目录下节点的个数来保证的,这一点比较巧妙。

三. 不可重入的公平锁

3.1 基本的使用方式

InterProcessSemaphoreMutex semaphoreMutex = new InterProcessSemaphoreMutex(client, "/mutex/semaphore_01");

semaphoreMutex.acquire();

semaphoreMutex.acquire();

semaphoreMutex.release();

3.2 初次加锁

看到InterProcessSemaphoreMutex,不可重入的公平锁,获取锁的逻辑底层使用的就是Semaphore,只不过调用了Semaphore的acquire()无参方法,信号量的个数等于1。也就是说,同时只能有一个线程获得信号量。

private final InterProcessSemaphoreV2 semaphore;

public void acquire() throws Exception
{
	lease = semaphore.acquire();
}

3.2 再次加锁(重入锁)

这里完全就是在重放一遍Semaphore的逻辑。假设线程A已经获取到了这把不可重入的公平锁了。此时又来了线程B尝试获取锁。让我们来看看会发生什么。

首先,线程B尝试获取第一把锁,假设此时能获取到。

接着,针对第二把锁,添加临时顺序节点,添加成功后,判断第二把锁目录下的节点的个数是否小于等于信号量的个数。由于2 <=1 不成立,因此当前线程陷入无线等待(wait),并且监听第二把锁下目录内的所有节点。

过了一段时间,线程A释放了不可重入的公平锁,那么针对第二把锁(/leases),就会删除它自己对应的那个临时顺序节点。此时,线程B的监听器就会感知到zookeeper上发生的这一系列变故,接着就会执行notifyAll()唤醒被这个锁对象wait的线程,这就包括了线程B。

四. 可重入的读写锁

源码的分析过程看一遍,脑子里有大概的印象,之后直接看结论就好了。

4.1 基本的使用方式

InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, "/read_write_lock/lock01");

InterProcessMutex readLock = readWriteLock.readLock();

readLock.acquire();

readLock.release();

InterProcessMutex writeLock = readWriteLock.writeLock();

writeLock.acquire();

writeLock.release();

4.2 同客户端同线程,先读锁,后写锁

结论: 同一个客户端,同线程,先读锁,后写锁,是互斥的。

获取读锁且没有释放的前提下,再次获取写锁时,当前线程会被阻塞住,直到读锁被释放。

通过观察InterProcessReadWriteLock的内部结构,我们发现,它就是搞了两把可重入的公平锁InterProcessMutex。

public class InterProcessReadWriteLock {
	private final InterProcessMutex readMutex;
    private final InterProcessMutex writeMutex;
	
	private static final String READ_LOCK_NAME  = "__READ__";
    private static final String WRITE_LOCK_NAME = "__WRIT__";
	... 省略代码
}

需要注意的是,InterProcessReadWriteLock重写了判断是否成功获取锁的代码逻辑,也就是LockInternalsDriver的getsTheLock( )方法。

针对加读锁而言,判断能否成功获取锁的代码如下:

public PredicateResults getsTheLock(CuratorFramework client, List<String> children, 
String sequenceNodeName, int maxLeases) throws Exception
{
	return readLockPredicate(children, sequenceNodeName);
}

private PredicateResults readLockPredicate(List<String> children, String sequenceNodeName) throws Exception
{
	if ( writeMutex.isOwnedByCurrentThread() )
	{
		return new PredicateResults(null, true);
	}

	int         index = 0;
	int         firstWriteIndex = Integer.MAX_VALUE;
	int         ourIndex = Integer.MAX_VALUE;
	for ( String node : children )
	{
		if ( node.contains(WRITE_LOCK_NAME) )
		{
			firstWriteIndex = Math.min(index, firstWriteIndex);
		}
		else if ( node.startsWith(sequenceNodeName) )
		{
			ourIndex = index;
			break;
		}

		++index;
	}
	StandardLockInternalsDriver.validateOurIndex(sequenceNodeName, ourIndex);

	boolean     getsTheLock = (ourIndex < firstWriteIndex);
	String      pathToWatch = getsTheLock ? null : children.get(firstWriteIndex);
	return new PredicateResults(pathToWatch, getsTheLock);
}

首先呢,为了获取读锁,当前线程一定会在指定目录下创建一个临时顺序节点。我们的锁目录是/read_write_lock/lock01,

此时锁目录的节点情况如下:

/read_write_lock/lock01/_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000

接着,就会检查当前线程是否持有写锁,如果持有,则直接返回,加读锁成功。但是我们现在是在加读锁啊,所以肯定是不成立的。

继续往下看,curator拿到了锁目录下的所有临时顺序节点,检查当前线程是否曾经尝试获取过写锁,此时不成立,所以会遍历所有的临时顺序节点,找到本次请求创建出的节点,下标是0,然后就会终止循环。

重点来了,这里会去判断,本次请求获取读锁时,创建的临时节点的下标是否小于Integer.MAX_VALUE。这不是废话吗?肯定小于啊,所以标记为获取读锁成功,pathToWatch=null,意味着不会去监听任何的路径或者节点(我都已经成功获取锁了,干嘛还要监听别的节点呢?)。

现在读锁已经获取完毕了,我们开始获取写锁。

首先呢,写锁同样是一个InterProcessMutex,为了获得写锁,这里会创建的临时顺序节点。

此时锁目录的节点情况如下:

_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001

然后就是判断本次请求获取写锁是否成功啊?这儿又会走getsTheLock( ),与读锁不同的是,InterProcessReadWriteLock没有覆盖写锁的getsTheLock( ),使用的默认的代码逻辑。这个逻辑我们熟悉的很呐,想要获得锁,当前请求添加的节点必须位于节点列表的第一个位置,但遗憾的是,现在的index=1,不等于0啊,所以这里就会阻塞住,并且搞了一个监听器,监听前一个节点,也就是读锁,当且仅当读锁被释放掉,再尝试获取写锁,最后才能获取写锁成功。

4.3 同客户端同线程,先写锁,后读锁

结论: 同一个客户端,同线程,先写锁,后读锁,可以加锁成功。

首先,加一把写锁,此时锁目录的节点情况如下:

_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001

接着,尝试加一把读锁,这里又会向锁目录创建一个临时顺序节点,此时锁目录的节点情况如下:

_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000

然后就是判断是否加读锁成功的逻辑了,之前我们看到过,InterProcessReadWriteLock重写了读锁的getsTheLock(),这个方法内有一个非常重要的代码:

private PredicateResults readLockPredicate(List<String> children, String sequenceNodeName) throws Exception
{
	if ( writeMutex.isOwnedByCurrentThread() )
	{
		return new PredicateResults(null, true);
	}

	后面的代码都不用看了...
}

写锁就是由当前线程持有的,所以直接返回true,获取读锁成功。

4.4 同客户端同线程,先写锁,后重入写锁

结论: 同一个客户端,同线程,先写锁,后重入写锁,可以加锁成功。

你先加一个写锁,接着后面加无数个写锁都没问题,无非就是累加lockData中,加锁的次数而已。

4.5 同客户端同线程,先读锁,后重入读锁

结论: 同一个客户端,同线程,先读锁,后重入读锁,可以加锁成功。

你先加一个读锁,接着后面加无数个读锁都没问题,无非就是累加lockData中,加锁的次数而已。

4.6 不同客户端,或相同客户端不同线程,先读锁,后写锁

结论: 不同客户端,或者相同客户端的不同线程,先读锁,后写锁,是互斥的!

首先,客户端A加一把读锁,此时锁目录的情况如下:

_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000

接着,客户端B,或者客户端A的另一个线程过来加一把写锁,这里一上来又会添加一个临时顺序节点,此时锁目录的情况如下:

_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001

看到写锁的节点前面有其他的节点,那想都不用想,肯定就是互斥的,后面就是阻塞啊,监听器啊,等待前面的节点释放锁啊之类的过程。具体可以参考4.2节。

4.7 不同客户端,或相同客户端不同线程,先写锁,后读锁

结论: 不同客户端,或者相同客户端的不同线程,先写锁,后读锁,是互斥的!

首先,加一把写锁,此时锁目录的节点情况如下:

_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001

接着,尝试加一把读锁,这里又会向锁目录创建一个临时顺序节点,此时锁目录的节点情况如下:

_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000

然后就是判断是否加读锁成功的逻辑了,之前我们看到过,InterProcessReadWriteLock重写了读锁的getsTheLock(),这个方法内有一个非常重要的代码:

private PredicateResults readLockPredicate(List<String> children, String sequenceNodeName) throws Exception
{
	if ( writeMutex.isOwnedByCurrentThread() )
	{
		return new PredicateResults(null, true);
	}

	int         index = 0;
	int         firstWriteIndex = Integer.MAX_VALUE;
	int         ourIndex = Integer.MAX_VALUE;
	for ( String node : children )
	{
		if ( node.contains(WRITE_LOCK_NAME) )
		{
			firstWriteIndex = Math.min(index, firstWriteIndex);
		}
		else if ( node.startsWith(sequenceNodeName) )
		{
			ourIndex = index;
			break;
		}

		++index;
	}
	StandardLockInternalsDriver.validateOurIndex(sequenceNodeName, ourIndex);

	boolean     getsTheLock = (ourIndex < firstWriteIndex);
	String      pathToWatch = getsTheLock ? null : children.get(firstWriteIndex);
	return new PredicateResults(pathToWatch, getsTheLock);
}

看到for循环,首先当前锁目录下存在一个包含了"__WRIT__"的节点,换句话说,就是之前加过写锁,所以执行这句话:

firstWriteIndex = Math.min(index, firstWriteIndex);

index指的是当前循环到的这个元素的下标,firstWriteIndex等于Interger.MAX_VALUE,这里取较小的元素,必然就是index,此时等于0。

boolean     getsTheLock = (ourIndex < firstWriteIndex);

ourIndex等于本次请求添加的临时顺序节点的下标,此时等于1。

boolean getsTheLock = (1 < 0) = false 获取读锁失败。

也就是说,只要在你之前有人加了写锁,你再去加读锁时,是加不了的,是互斥的。

4.8 不同客户端,或相同客户端不同线程,先写锁,后写锁

结论: 不同客户端,或者相同客户端的不同线程,先写锁,后写锁,是互斥的!

参考4.6节,反正写锁对应的节点如果没有位于临时顺序节点的第一个位置,那肯定就是没办法加上锁的。

4.9 不同客户端,或相同客户端不同线程,先读锁,后读锁

结论: 不同客户端,或者相同客户端的不同线程,先读锁,后读锁,可以加锁成功。

读锁检查加锁是否成功的逻辑被InterProcessReadWriteLock重写了,重写了,重写了。 这句话我都要写吐了。。。

只要本次加读锁对应的临时顺序节点在列表中的下标不超过Integer.MAX_VALUE,那就可以成功加锁。

Integer.MAX_VALUE可是2147483647,这么大的数值,显然无数个客户端同时加读锁,都是没问题的。

五. MultiLock

5.1 基本的使用方式

InterProcessLock lock01 = new InterProcessMutex(client, "/multi_lock/lock01");
InterProcessLock lock02 = new InterProcessMutex(client, "/multi_lock/lock02");
InterProcessLock lock03 = new InterProcessMutex(client, "/multi_lock/lock03");

List<InterProcessLock> lockList = new ArrayList<>(4);
lockList.add(lock01);
lockList.add(lock02);
lockList.add(lock03);

InterProcessMultiLock multiLock = new InterProcessMultiLock(lockList);

multiLock.acquire();

multiLock.release();

5.2 加锁的过程

MultiLock的内部就是搞了一个集合,存放了多把锁。

private final List<InterProcessLock> locks;

获取锁时,就是在循环集合内的每一把锁,并尝试加锁,把加成功的锁放到放到一个集合里面。

只要任何一把锁加锁失败,就会依次释放之前成功加到的锁,最后返回false。

当且仅当所有的锁全部加锁成功,则返回true。

5.3 解锁的过程

循环遍历集合内的每一把锁,接着就是去解锁。

public synchronized void release() throws Exception
{
	Exception       baseException = null;

	for ( InterProcessLock lock : reverse(locks) )
	{
		try
		{
			lock.release();
		}
		catch ( Exception e )
		{
			if ( baseException == null )
			{
				baseException = e;
			}
			else
			{
				baseException = new Exception(baseException);
			}
		}
	}

	if ( baseException != null )
	{
		throw baseException;
	}
}

总感觉这坨代码有bug,如果释放前一把锁失败,接着释放下一把锁也释放,那么后面捕获的异常岂不是把前者给覆盖掉了?那么抛出的异常就是按照顺序的,最后一个解锁失败的锁对应的异常了。

也许这就是curator的想要实现的逻辑吧…

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值