在多线程的软件世界里,对共享资源的争抢过程(Data Race)就是并发,而对共享资源数据进行访问保护的最直接办法就是引入锁。html
POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套经常使用的API。线程同步(Thread Synchronization)是并行编程中很是重要的通信手段,其中最典型的应用就是用Pthreads提供的锁机制(lock)来对多个线程之间共 享的临界区(Critical Section)进行保护(另外一种经常使用的同步机制是barrier)。java
无锁编程也是一种办法,但它不在本文的讨论范围,并发多线程转为单线程(Disruptor),函数式编程,锁粒度控制(ConcurrentHashMap桶),信号量(Semaphore)等手段均可以实现无锁或锁优化。redis
技术上来讲,锁也能够理解成将大量并发请求串行化,但请注意串行化不能简单等同为** 排队 ,由于这里和现实世界没什么不一样,排队意味着你们是公平Fair的领到资源,先到先得,然而不少状况下为了性能考量多线程之间仍是会不公平Unfair**的去抢。Java中ReentrantLock可重入锁,提供了公平锁和非公平锁两种实现。sql
再注意一点,串行也不是意味着只有一个排队的队伍,每次只能进一个。固然能够好多个队伍,每次进入多个。好比餐馆一共10个餐桌,服务员可能一次放行最多10我的进去,有人出来再放行同数量的人进去。Java中Semaphore信号量,至关于同时管理一批锁。数据库
自旋锁是一种非阻塞锁,也就是说,若是某线程须要获取自旋锁,但该锁已经被其余线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。apache
互斥锁是阻塞锁,当某线程没法获取互斥锁时,该线程会被直接挂起,再也不消耗CPU时间,当其余线程释放互斥锁后,操做系统会唤醒那个被挂起的线程。编程
可重入锁是一种特殊的互斥锁,它能够被同一个线程屡次获取,而不会产生死锁。缓存
java环境下能够经过synchronized和lock开实现本地锁。多线程
//synchronized
public synchronized void demoMethod(){}
public void demoMethod(){
synchronized (this)
{
//other thread safe code
}
}
private final Object lock = new Object();
public void demoMethod(){
synchronized (lock)
{
//other thread safe code
}
}
public synchronized static void demoMethod(){}
//lock
private final Lock queueLock = new ReentrantLock();
public void printJob(Object document) {
queueLock.lock();
try
{
Long duration = (long) (Math.random() * 10000);
System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a Job during " + (duration / 1000) + " seconds :: Time - " + new Date());
Thread.sleep(duration);
} catch (InterruptedException e)
{
e.printStackTrace();
} finally
{
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
queueLock.unlock();
}
}
复制代码
锁非静态是锁了对象的实例;锁静态是锁了对象的类型。并发
synchronized void testRead(){
this.testWrite();
}
synchronized void testWrite(){}
复制代码
名称 | 优势 | 缺点 |
---|---|---|
synchronized | 实现简单,语义清晰,便于JVM堆栈跟踪,加锁解锁过程由JVM自动控制,提供了多种优化方案,使用更普遍 | 悲观的排他锁,不能进行高级功能 |
lock | 可定时的、可轮询的与可中断的锁获取操做,提供了读写锁、公平锁和非公平锁 | 需手动释放锁unlock,不适合JVM进行堆栈跟踪 |
使用分布式锁的目的有两个,一个是避免屡次执行幂等操做提高效率;一个是避免多个节点同时执行非幂等操做致使数据不一致。 接下来咱们来看如何实现分布式锁,在java环境下有三种也即经过数据库,经过redis及经过Zk来实现。
经过主键及其余约束使用抛异常来实现分布式锁不在本文讨论范围。一下为基于数据库排他锁来实现分布式锁
/** * 超时获取锁 * @param lockID * @param timeOuts * @return * @throws InterruptedException */
public boolean acquireByUpdate(String lockID, long timeOuts) throws InterruptedException, SQLException {
String sql = "SELECT id from test_lock where id = ? for UPDATE ";
long futureTime = System.currentTimeMillis() + timeOuts;
long ranmain = timeOuts;
long timerange = 500;
connection.setAutoCommit(false);
while (true) {
CountDownLatch latch = new CountDownLatch(1);
try {
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, lockID);
statement.setInt(2, 1);
statement.setLong(1, System.currentTimeMillis());
boolean ifsucess = statement.execute();//若是成功,那么就是获取到了锁
if (ifsucess)
return true;
} catch (SQLException e) {
e.printStackTrace();
}
latch.await(timerange, TimeUnit.MILLISECONDS);
ranmain = futureTime - System.currentTimeMillis();
if (ranmain <= 0)
break;
if (ranmain < timerange) {
timerange = ranmain;
}
continue;
}
return false;
}
/** * 释放锁 * @param lockID * @return * @throws SQLException */
public void unlockforUpdtate(String lockID) throws SQLException {
connection.commit();
}
复制代码
加锁
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
复制代码
第一个为key,咱们使用key来当锁,由于key是惟一的。
第二个为value,咱们传的是requestId,不少童鞋可能不明白,有key做为锁不就够了吗,为何还要用到value?缘由就是咱们在上面讲到可靠性时,分布式锁要知足第四个条件解铃还须系铃人,经过给value赋值为requestId,咱们就知道这把锁是哪一个请求加的了,在解锁的时候就能够有依据。requestId可使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数咱们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,咱们进行set操做;若key已经存在,则不作任何操做;
第四个为expx,这个参数咱们传的是PX,意思是咱们要给这个key加一个过时的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,表明key的过时时间。
解锁
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
复制代码
第一行代码,咱们写了一个简单的Lua脚本代码
第二行代码,咱们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
基于Redlock实现分布式锁的争论见
使用curator来实现分布式锁。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public boolean unlock() {
try {
interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
复制代码
方式 | 优势 | 缺点 |
---|---|---|
基于DB | 直接借助数据库,容易理解 | 会有各类各样的问题,在解决问题的过程当中会使整个方案变得愈来愈复杂 操做数据库须要必定的开销,性能问题须要考虑 使用数据库的行级锁并不必定靠谱,尤为是当咱们的锁表并不大的时候 |
基于缓存 | 性能好,实现起来较为方便 | 经过超时时间来控制锁的失效时间并非十分的合理 |
基于ZK | 有效的解决单点问题,不可重入问题,非阻塞问题以及锁没法释放的问题。实现起来较为简单 | 性能上不如使用缓存实现分布式锁。 须要对ZK的原理有所了解 |
zookeeper可靠性比redis强太多,只是效率低了点,若是并发量不是特别大,追求可靠性,首选zookeeper。为了效率,则首选redis实现。