java并发编程之ReentrantReadWriteLock读写锁

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,比读写锁更快的一种锁,因为他支持在获取读锁的同时支持一个写操作。

发布了41 篇原创文章 · 获赞 79 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_33220089/article/details/102754036