x = y;
y = t;
System.out.println("x is" + x + " y is " + y);
}
一个更加直观的例子便是,同一个线程中,某个方法的递归调用不应该被阻塞,所以如果要实现这个特性,简单的使用某个key作为Monitor是欠妥的,可以加入线程编号,来保证可重入。
使用可重入分布式锁的来测试计算斐波那契数列(只是为了验证可重入性):
@RequestMapping("testReentrant")
public void ReentrantLock() {
RLock lock = redissonClient.getLock("fibonacci");
lock.lock();
try {
int result = fibonacci(10);
System.out.println(result);
} finally {
lock.unlock();
}
}
int fibonacci(int n) {
RLock lock = redissonClient.getLock("fibonacci");
try {
if (n <= 1) return n;
else
return fibonacci(n - 1) + fibonacci(n - 2);
} finally {
lock.unlock();
}
}
最终输出:55,可以发现,只要是在同一线程之内,无论是递归调用还是外部加锁(同一把锁),都不会造成死锁。
可用性
借助于第三方中间件实现的分布式锁,都有这个问题,中间件挂了,会导致锁不可用,所以需要保证锁的高可用,这就需要保证中间件的可用性,如redis可以使用哨兵+集群,保证了中间件的可用性,便保证了锁的可用性、
其他特性
除了可重入锁,锁的分类还有很多,在分布式下也同样可以实现,包括但不限于:公平锁,联锁,信号量,读写锁。Redisson也都提供了相关的实现类,其他的特性如并发容器等可以参考官方文档。
新手遭遇并发
基本算是把项目中遇到的并发过了一遍了,案例其实很多,再简单罗列下一些新手可能会遇到的问题。
使用了线程安全的容器就是线程安全了吗?很多新手误以为使用了并发容器如:concurrentHashMap就万事大吉了,却不知道,一知半解的隐患可能比全然不懂更大。来看下面的代码:
public class ConcurrentHashMapTest {
static Map<String, Integer> counter = new ConcurrentHashMap();
public static void main(String[] args) throws InterruptedException {
counter.put("stock1", 0);
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
counter.put("stock1", counter.get("stock1") + 1);
countDownLatch.countDown();
}
});
}
countDownLatch.await();
System.out.println("result is " + counter.get("stock1"));
}
}
counter.put(“stock1″, counter.get(“stock1″) + 1)并不是原子操作,并发容器保证的是单步操作的线程安全特性,这一点往往初级程序员特别容易忽视。
总结
项目中的并发场景是非常多的,而根据场景不同,同一个场景下的业务需求不同,以及数据量,访问量的不同,都会影响到锁的使用,架构中经常被提到的一句话是:业务决定架构,放到并发中也同样适用:业务决定控制并发的手段,如本文未涉及的队列的使用,本质上是化并发为串行,也解决了并发问题,都是控制的手段。了解锁的使用很简单,但如果使用,在什么场景下使用什么样的锁,这才是价值所在。
同一个线程之间的递归调用不应该被阻塞,所以如果要实现这个特性,简单的使用某个key作为Monitor是欠妥的,可以加入线程编号,来保证可重入。