ReentrantReadWriteLock中写锁加锁原理

ReentrantReadWriteLock

读读并发 读写互斥 写写互斥

基本使用

读读并发

场景:创建t1,t2线程两个线程。t1,t2线程都加读锁。
代码:

/**
 * 读读并发
 */
@Slf4j(topic = "hhb")
public class ReadReadTest {
	//创建一把读写锁
    static ReentrantReadWriteLock  readWriteLock=new ReentrantReadWriteLock();
    //读锁
    static Lock r = readWriteLock.readLock();

    public static void main(String[] args) {
        //读读并发
        new Thread(() -> {
            r.lock();
            log.debug("t1 read 获取 锁");
            try {
                TimeUnit.SECONDS.sleep(2);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }

        }, "t1").start();
        new Thread(() -> {
            r.lock();
            log.debug("t2 read 获取 锁");
            try {
                TimeUnit.SECONDS.sleep(2);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }

        }, "t2").start();
    }

}

结果打印:
读读并发
结果分析:
t1,t2都获取到读锁,并执行同步代码块。

读写互斥

场景:创建t1,t2线程两个线程。t1加读锁,加锁成功后睡眠两秒。t2加写锁,加锁成功后睡眠两秒。

代码:

/**
 * 读写互斥
 */
@Slf4j(topic = "hhb")
public class ReadWriteTest {
    static ReentrantReadWriteLock  readWriteLock=new ReentrantReadWriteLock();
    //读锁
    static Lock r = readWriteLock.readLock();
    //写锁
    static Lock w = readWriteLock.writeLock();
    public static void main(String[] args) {
        //读写互斥
        new Thread(() -> {
            r.lock();
            log.debug("t1 read 获取 锁");
            try {
                TimeUnit.SECONDS.sleep(2);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                r.unlock();
            }

        }, "t1").start();
        new Thread(() -> {
            w.lock();
            log.debug("t2 write 获取 锁");
            try {
                TimeUnit.SECONDS.sleep(2);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                w.unlock();
            }

        }, "t2").start();
    }
}

结果打印:
读写互斥
结果分析:

t1,t2线程执行。t1先获取到cpu资源先执行,获取到读锁后,打印 “t1 read 获取 锁” 然后睡眠2秒。
t1在睡眠过程中,t2线程获取到cpu资源开始执行。在执行过程中t2线程开始加写锁,发现readWriteLock对象已被t1加了读锁,读写锁互斥,导致t2加写锁失败,t2线程进入等待队列等待被唤醒。

当t1线程睡眠两秒后,t1释放了读锁,readWriteLock锁状态为无锁,唤醒t2。t2获取到cpu资源,加写锁成功后,执行t2线程的同步代码块,打印“t2 write 获取 锁”。

写写互斥

原理和读写互斥差不多!注意以上场景都是两个线程分别加读写锁。如果加读写锁在一个线程中,就要考虑锁的重入等情况。

写锁的加锁流程原理

当前用的是jdk1.8
写锁加锁调用:

  1. lock底层调用方法
w.lock();//加写锁

查看WriteLock中实现的lock方法
WriteLock中实现的lock方法
进入实现的lock最终调用acquire方法
acquire方法只要实现:先尝试加锁,如果加锁失败,当前线程睡眠并加入等待队列。
tryAcquire

我们主要看一下tryAcquire方法
读写锁中tryAcquire方法

  1. 详解ReentrantReadWriteLock写锁加锁流程
protected final boolean tryAcquire(int acquires) {
            /*
             * Walkthrough:
             * 1. If read count nonzero or write count nonzero
             *    and owner is a different thread, fail.
             * 2. If count would saturate, fail. (This can only
             *    happen if count is already nonzero.)
             * 3. Otherwise, this thread is eligible for lock if
             *    it is either a reentrant acquire or
             *    queue policy allows it. If so, update state
             *    and set owner.
             */
             //获取当前线程
            Thread current = Thread.currentThread();
            //获取锁的状态 默认为0 为int类型
            int c = getState();
            //因为读写锁是同一个对象,所以读锁标识和写锁标识都存在AQS中state中
            //int 类型占32位,前16位标识读锁状态 后16位标识写锁状态
            //当前获取写锁状态(后16位)
            int w = exclusiveCount(c);
            //c==0标识没人上锁 
            //c!=0 表示有人上锁但是不确定是读锁还是写锁 
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                //如果有人已经上了读锁,当前线程加写锁 就要造成锁升级 直接失败
                //如果有人已经上了写锁 ,当前线程加写锁。如果当前线程不等于已上写锁线程 直接失败
                //w==0 表示有人上了读锁 还未上写锁
                //current != getExclusiveOwnerThread() 表示当前线程不等于已上写锁线程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //表示当前线程等于已上写锁线程 就会造成锁的重入
                 //重入了把w+1 标识的长度有限 但是这个判断基本没用
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
               //没用超出重入的最大限制 则把w+1 (最大65535)
                setState(c + acquires);
                return true;
            }
            //当c==0 表示第一次加锁
            //writerShouldBlock()判断当前线程需不需要排队
            //公平锁:如果等待队列中有人排队 writerShouldBlock返回true
            //公平锁:如果等待队列中没人排队 writerShouldBlock返回false
            //非公平锁:writerShouldBlock返回false
            //如果writerShouldBlock()返回true,  !compareAndSetState(c, c + acquires)不会执行
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            //加锁成功则把当前持有锁的线程设置自己
            setExclusiveOwnerThread(current);
            return true;
        }
  1. 图解写锁上锁流程
    建议结合代码一起看
    写锁加锁流程

问题:为什么会有锁的重入?
现在有这么一个场景:类中有两个同步方法A,B,方法内都加锁解决同步问题 。

public class ReadWtiteLock {
    static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    public static void main(String[] args) {
        //创建t1线程执行A方法
        new Thread(()->{
            A();
        },"t1").start();

    }
    private static void A(){
        writeLock.lock();
        System.out.println("A");
        B();
        writeLock.unlock();
    }
    private static void B(){
        writeLock.lock();
        System.out.println("B");
        writeLock.unlock();
    }

}

启动t1线程执行A方法,本应该A方法先加一把写锁,调用B方法时,前面的写锁还没有释放。B等待A释放锁,而A等待B执行完,最终互相等待造成死锁。但是这时B获取死锁过程中发现当前线程等于已上写锁线程(都是t1线程加的写锁),就会造成锁的重入,B方法就正常加写锁成功,AB方法最终执行成功。

有人会问:为什么不可以用下面的写法,非要加两次锁 ?B不加锁 A加锁

private static void A(){
        writeLock.lock();
        System.out.println("A");
        B();
        writeLock.unlock();
    }
    private static void B(){
        System.out.println("B");
    }

为了防止其他线程直接调用B方法从而产生线程安全问题。

写锁的解锁锁流程原理

 public void unlock() {
            sync.release(1);
        }

可以看到写锁unlock调用release()方法,我们我看一下解锁的逻辑

 public final boolean release(int arg) {
 		//首先尝试解锁
        if (tryRelease(arg)) {
        //如果解锁成功,获取等待队列中头节点
            Node h = head;
            //如果头节点不为空,且Node节点中waitStatus属性不为0
            //waitStatus 默认为0,如果头节点的下一个节点需要被头节点唤醒,此时头节点的waitStatus值会在下一个节点入队列时修改为-1
            if (h != null && h.waitStatus != 0)
            	//唤醒头节点的下一个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

主要看一下尝试解锁tryRelease方法是怎么实现的?

 protected final boolean tryRelease(int releases) {
 			//判断这把写锁是否被持有,如果没有被持有,却来解锁 抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //得到当前读写锁状态state后减一
            int nextc = getState() - releases;
            //调用exclusiveCount(nextc)获取到读写锁中写锁状态(就是获取state值的后16位)
            //判断写锁状态是否为0,0表示当前锁状态未加写锁,1表示写锁被持有,2~65535表示锁被重入对应次数
            boolean free = exclusiveCount(nextc) == 0;
            //写锁状态为0,0表示当前锁状态未加写锁
            if (free)
            	//解锁成功,把当前锁持有线程设置为空
                setExclusiveOwnerThread(null);
            //把当前写锁状态改为nextc的值
            setState(nextc);
            //这里如果写锁状态是否为0就返回true 代表解锁成功
            //如果写锁状态大于0 ,说明存在重入的情况 需要多次调用unlock方法 直到写锁状态等于0 
            return free;
        }

读锁的原理正在持续更新中 !

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值