手写一个ReentrantLock锁

本文介绍了作者在学习Java锁知识后,尝试手写ReentrantLock的心得。文中详细阐述了可重入锁与不可重入锁的原理,以及公平锁与非公平锁的区别。在实现过程中,作者特别提到了在创建可重入锁时遇到的问题及其解决方案,强调了修改owner和count的顺序对于正确实现重入的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

手写ReentrantLock

最近学习了Java语言中锁相关知识,看了一下ReentrantLock源码,自己手写了一个ReentrantLock

ReentrantLock是一个可重入锁,并且在源码中通过构造函数可以使其在公平锁和非公平锁之间转换。

可重入锁即当前线程可以在不释放锁的情况下多次获取锁,但是释放锁的次数应与获取锁的次数相同,否则会抛出IllegalMonitorStateException异常。

公平锁和非公平锁的区别在与,公平锁使先进入等待队列的线程先获取锁,每个线程都有机会获取锁。非公平锁则是每个线程获取锁的几率不确定,非公平锁并发性较好,但容易造成某些线程长时间获取不到锁。

不可重入ReentrantLock,非公平
/**
 * 不可重入锁
 */
public class WonderReentranrLock{
    //标记获取锁的线程,原子类型的引用保证原子性
    private AtomicReference<Thread> owner = new AtomicReference<>();
    //抢锁失败时进入等待队列
    private Queue<Thread> waitQueue = new LinkedBlockingQueue();

    /**
     * 加锁
     */
    public void lock(){
        //抢锁失败
        if(!tryLock()){
            Thread current = Thread.currentThread();
            //线程进入等待队列
            waitQueue.offer(current);
            //继续尝试抢锁
            for(;;){
                //获取队列头部
                Thread head = waitQueue.peek();
                //如果当前线程在队列头部
                if(current == head){
                    //抢锁
                    if(!tryLock()){
                        //如果抢锁失败
                        LockSupport.park();
                    }else{
                        //如果抢锁成功,线程出队列
                        waitQueue.poll();
                        return;
                    }
                }else{
                    //如果不在队列头部,将线程挂起
                    LockSupport.park();
                }
            }
        }
        //成功则抢到了锁
    }

    /**
     * 尝试加锁
     * @return
     */
    public boolean tryLock(){
        Thread current = Thread.currentThread();
        //CAS原子性操作修改标记,即抢锁
        return owner.compareAndSet(null,current);
    }

    /**
     * 解锁
     */
    public void unLock(){
        //当前线程释放锁成功
        if(tryUnLock()){
            Thread head = waitQueue.peek();
            //队列头部是否存在
            if(head!=null){
                //存在则唤醒队列头部线程去抢锁
                LockSupport.unpark(head);
            }
        }
    }

    /**
     * 尝试解锁
     * @return
     */
    public boolean tryUnLock(){
        Thread current = Thread.currentThread();
        //判断当前线程是否持有锁
        if(owner.get()!=current){
            throw new IllegalMonitorStateException();
        }else{
            //持有则CAS操作释放锁
            return owner.compareAndSet(current,null);
        }
    }
}

这个ReentrantLock是一个非公平,不可重入的锁。

这里有一个坑:就是虽然加锁时判断了是否时队列头部,如果是队列头部就让它去抢锁,如果不是头部则挂起,这里看上去是队列的先进先出,好像是个公平锁,但是在多线程中可能出现队列头部与不在队列中的线程抢锁,头部线程可能抢锁失败,所以是非公平的。

再写一个可重入锁(非公平),公平锁貌似实现比较复杂,先不考虑。

可重入ReentrantLock,非公平
/**
 * 可重入锁
 */
public class WonderReentrantLock2 {
    //标记获取锁的线程,抢的不是owner不用原子类型
    private Thread owner = null;
    //记录重入次数,抢锁,抢的是count
    private AtomicInteger count = new AtomicInteger();
    //抢锁失败时进入等待队列
    private Queue<Thread> waitQueue = new LinkedBlockingQueue();

    /**
     * 加锁,与不可重入锁一样
     */
    public void lock(){
        //抢锁失败
        if(!tryLock()){
            Thread current = Thread.currentThread();
            //线程进入等待队列
            waitQueue.offer(current);
            //继续尝试抢锁
            for(;;){
                //获取队列头部
                Thread head = waitQueue.peek();
                //如果当前线程在队列头部
                if(current == head){
                    //抢锁
                    if(!tryLock()){
                        //如果抢锁失败
                        LockSupport.park();
                    }else{
                        //如果抢锁成功,线程出队列
                        waitQueue.poll();
                        return;
                    }
                }else{
                    //如果不在队列头部,将线程挂起
                    LockSupport.park();
                }
            }
        }
        //成功则抢到了锁
    }

    /**
     * 尝试加锁
     * @return
     */
    public boolean tryLock(){
        Thread current = Thread.currentThread();
        //抢到锁的次数
        int ct = count.get();
        //次数不为0,意味着有线程已经抢到锁
        if (ct!=0){
            //如果抢到锁的线程是当前线程
            if(owner==current){
                //重入,次数+1
                count.set(ct+1);
                return true;
            }else{
                //不是当前线程,抢锁失败
                return false;
            }
        }else{
            //次数为0,意味着没有线程抢到锁,CAS操作抢锁
            if(count.compareAndSet(ct,ct+1)){
                //抢到锁后将owner标记为当前线程
                owner = current;
                return true;
            }else{
                //CAS抢锁失败
                return false;
            }
        }
    }

    /**
     * 解锁
     */
    public void unLock(){
        //当前线程释放锁成功
        if(tryUnLock()){
            Thread head = waitQueue.peek();
            //队列头部是否存在
            if(head!=null){
                //存在则唤醒队列头部线程去抢锁
                LockSupport.unpark(head);
            }
        }
    }

    /**
     * 尝试解锁
     * @return
     */
    public boolean tryUnLock(){
        Thread current = Thread.currentThread();
        //判断当前线程是否持有锁
        if(owner!=current){
            throw new IllegalMonitorStateException();
        }else{
            //获取加锁次数
            int ct = count.get();
            //解锁后count应为next的值
            int next = ct - 1;
            //count.set(next);  坑!
            if(next==0){
                //解锁后count为0的话,即所加的锁全部解开,将owner置为null
                owner = null;
                //owner为null后再将count值修改为0
                count.set(next);
                return true;
            }else{
                //如果next不为0,意味着还有重入的锁未解,修改count即可,owner不变
                count.set(next);
                return false;
            }
        }
    }
}

可重入锁和不可重入锁的区别在于:不可重入锁是线程争抢owner(线程标记)的过程,可重入锁增加了count记录一个线程重入的次数。因此,可重入锁在抢锁时,首先判断count是否为0,count为0则锁空闲,先使用CAS修改count,成功后修改owner为当前线程,抢锁成功。若count不为0,说明锁被某一线程占用,则判断占用锁的线程是否是当前线程,如果是,为count加1即可实现重入,如果不是当前线程则抢锁失败挂起。

在写可重入锁时遇到了一个坑,代码中tryUnLock()方法如果owner是当前线程则开始执行解锁,刚开始写的是先修改count值为原值减1,再将owner置为null。运行后发现一直报IllegalMonitorStateException错误,就是说当前线程加锁后,尝试解锁时owner不是当前线程。这是因为先修改了count后修改owner,如果修改后count值为0,那么就会有其他线程抢到锁并且修改owner,此时owner就不是原先的线程了。这里将修改count放在owner=null之后就可以解决问题。也可以将owner改为AtomicReference类型解决。

测试代码
public class Test {
    static WonderReentrantLock lock = new WonderReentrantLock();
    static WonderReentrantLock2 lock2 = new WonderReentrantLock2();

    volatile static int count=0;

    public static void main(String[] args) throws InterruptedException {
        for(int i=0; i<10; i++){
            new Thread(){
                @Override
                public void run() {
                    for(int j=0; j<10000; j++){
                        lock2.lock();//重入
                        count++;
                    }
                    for(int j=0; j<10000; j++){
                        lock2.unLock();//加锁多少次,解锁多少次
                    }
                    System.out.println("over...");
                }
            }.start();
        }
        Thread.sleep(4000);
        System.out.println(count);

        /*for(int i=0; i<10; i++){
            new Thread(){
                @Override
                public void run() {
                    for(int j=0; j<10000; j++){
                        lock.lock();//不可重入
                        count++;
                        lock.unLock();
                    }
                    System.out.println("over...");
                }
            }.start();
        }
        Thread.sleep(4000);
        System.out.println(count);*/
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值