synchronized
这个关键字,是锁的意思,而且还是一种可重入锁,使用它可以保证线程的互斥性,保证被synchronized修饰的代码块或者方法在同一时刻只能被一个线程访问,所以在并发编程中可以保证数据的准确性,由于他是锁住了整个代码块或者方法,这样就会大大的降低程序的性能,在并发很高的时候会导致很卡。
那有没有比synchronized更快的锁呢?java这么强大,肯定是有的,那就是今天的主角:ReentrantReadWriteLock,读写锁,
介绍读写锁之前我们先看一个普通的程序:
public User getUserInfo(){
//从缓存中获取数据
User user = (User) redisTemplate.opsForValue().get("user");
if(null != user){
log.info("在缓存中拿到了数据");
return user;
}
log.info("缓存中没有数据,往数据库获取");
//缓存中没有数据,从数据库捞取数据(这里我就不去数据库拿了,直接用map集合模拟)
user = map.get("user");
if(null != user){
//将数据存到缓存中
redisTemplate.opsForValue().set("user",user);
}
return user;
}
这是一个获取用户信息的方法,模拟了先从缓存中获取数据,如果缓存中不存在,则向数据库中获取,这段代码看起来因该是没有任何问题,但事实是这样吗?
我们来测试一下,使用jmeter做并发测试:
模拟一百个线程同时访问,结果会是如何呢?请看下图:
看到没有,居然会有8次访问走到了数据库,这就是我们经常提到的线程安全问题,在生产环境中肯定是不允许这样的事情发生,如何避免呢,可以在方法名加上synchronized关键字,这个确实能保证线程安全,但是性能不会很好,所以我们可以使用性能更好的ReentrantReadWriteLock(读写锁)。
读写锁,顾名思义,读的时候一把锁,写的时候一把锁,为什么两把锁的性能还会比synchronized的一把锁性能更好的呢?
那是因为ReentrantReadWriteLock的读锁是可以支持并发的,所以向刚刚上面的哪个例子,读多写少的情况家,非常推荐读写锁,虽然读锁是支持并发,但是写锁依然保持线程的互斥,并且在获取写锁之后,读锁也会暂时处于阻塞状态,等待释放写锁。
好了,废话不多,我们来看看如何实现,上代码:
package com.ymy.service;
import com.ymy.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Service
@Slf4j
public class ReadWriteLockService {
@Autowired
private RedisTemplate redisTemplate;
private static Map<String,User> map = new ConcurrentHashMap<String,User>();
static {
User user = User.builder().id(1).userName("独孤求败").age(20).build();
map.put("user",user);
}
public User getUserInfo(){
//实例化读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//获取读锁
Lock readLock =readWriteLock.readLock();
User user = null;
try{
//加锁
readLock.lock();
//从缓存中获取数据
user = (User) redisTemplate.opsForValue().get("user");
}finally {
//释放读锁
readLock.unlock();
}
if(null != user){
log.info("在缓存中拿到了数据");
return user;
}
//获取写锁
Lock writeLock = readWriteLock.writeLock();
try{
//加锁
writeLock.lock();
//再往缓存中获取一次,防止在加写锁的时候数据被更新
user = (User) redisTemplate.opsForValue().get("user");
if(null == user){
log.info("缓存中没有数据,往数据库获取");
//缓存中没有数据,从数据库捞取数据(这里我就不去数据库拿了,直接用map集合模拟)
user = map.get("user");
if(null != user){
//将数据存到缓存中
redisTemplate.opsForValue().set("user",user);
}
}
log.info("在缓存中拿到了数据");
}finally {
//释放写锁
writeLock.unlock();
}
return user;
}
}
加上读写锁之后会发生什么呢?同样用jmeter模拟一百次并发,结果如下:
为什么加了读写锁之后还是在数据库中读取了8次呢?其实这里就牵涉到锁的一个小特性,那就是加锁的对象
请看这行代码:
实例化读写锁,很明显问题就出现在这里,为什么呢?因为lock锁住的是当前实例,然后每次进入这个方法的时候都会产生一个新的实例对象,所以每次加锁的对象都不一样,怎么能实现我们想要的效果呢?
我们只需要做一下小小的改动即可:
那就是让读写锁只实例化一次就可以了,这样就能保证锁住的就是同一个对象,我们来看结果:
成功了,完美的做到了第一次在数据库中获取,第二次以后在缓存中获取,ReentrantReadWriteLock和synchronized一样,是可重入锁,最后在补充一句:ReentrantReadWriteLock虽然支持并发读操作,但是当某个线程获取到读锁的时候,写操作是需要处于阻塞状态。
那能不能在获取到读锁之后让写操作不处于阻塞状态呢?肯定是有的,StampedLock,比读写锁更快的一种锁,因为他支持在获取读锁的同时支持一个写操作。