在实际项目中,往往会存在许多并发安全问题,以及并发访问效率问题。
比如,在多个用户同时更新同一条数据的时候,往往会出现线程安全问题,实际保存的数据和预期的并不相符。面对这种问题很多人第一时间会想到内置锁---->synchronized关键字,将方法体或者代码块锁住保证线程执行的同步。这样就能从根本上解决并发的线程安全问题。
但是,如果直接使用synchronized锁住代码块。会对程序访问性能产生很大的影响。比如:多个线程同时访问欲要更新的数据并不相同,不同的数据更新之间是不会存在线程安全问题的,完全可以非阻塞执行。但是使用synchronized关键字之后,被锁住的代码块一定会阻塞执行。
那么我们就会想,可不可以拥有一种在操作同一数据时可以阻塞执行,不同数据时非阻塞执行的方案呢?
有!
请看如下代码
1)显示锁
public class LockValue {
/**
* 显示锁集合
*/
private static ConcurrentHashMap<Object,ReentrantLock> lockMap = new ConcurrentHashMap<>();
private static ReentrantLock getLock(Object code){
ReentrantLock lock = new ReentrantLock();
//如果同步集合中已经存在锁,则直接返回已经存在的锁
ReentrantLock preLock = lockMap.putIfAbsent(code,lock);
//如果添加成功则使用新锁
return preLock == null ? lock : preLock;
}
public static void lock(Object code){
getLock(code).lock();
}
public static void unlock(Object code){
ReentrantLock lock = getLock(code);
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
这是一个由ConcurrentHashMap为容器的对显示锁的一种封装。我们可以看到在getLock(code)方法中,该方法的逻辑是这样的:
如果ConcurrentHashMap容器中可以通过code(键)找到对应的ReentrantLock(锁),我们就用找到的锁。反之,如果找不到,则创建一把新锁。简单的说通过这个工具我们可以保证,同一个code获取到的锁都是同一把锁,不同的code获取的锁都不相同。
2)内置锁
synchronized(value){}
那么问题就很简单了,我们都知道java中同一把锁一旦被线程使用,如果不释放,其他线程是不可以使用该锁的,这就可以保证线程的同步阻塞。而不同的锁相当于不同的对象,如果每个线程所持有的锁都是不同的,那么自然不存在不可以使用的问题,这样就可以保证非阻塞执行了。
但是
这种解决方案只可以在单实例服务情况下解决并发线程安全问题,也就是说如果是多服务负载均衡的情况下,这种在代码层次上控制方案是无效的。
那么在多服务实例下,有方案可以解决这种问题吗?
1)从数据库方面入手,如果是更新操作,则对数据库表添加一个version字段,每次执行更新时以该字段为条件并且对该字段递增,这样下一个语句如果version不同,就不会执行成功。(这也是常说的数据库乐观锁)。如果是新增操作,为了避免数据重复,可以添加主键索引。
2)采用redis缓存,使用java锁机制将缓存中的键锁住,如果缓存中没有键,则将键添加到缓存中,在代码块执行完毕,删除缓存。这样可以保证数据的安全性,缓存技术也可以保证执行的高效率。
但是
这种解决方案只是在集群下可以适用,但是在分布式下也是无效的,那么怎么办来?
这里要说声抱歉,分布式情况下的线程安全问题目前我也不知道怎么解决,真要说的话只能从项目的业务逻辑调度入手,尽量避免针对同一条数据访问不同的分布式数据库的情况。或者可以利用hash的一致性算法,获取需要操作数据的hash值,寻找离它最近的数据库服务器。这样也是可以避免的。