如何更好理解Lock锁?手把手教你手写Lock实现

如何更好理解Lock锁?手把手教你手写Lock实现

Lock 锁

了解多线程并发的都比较熟悉Lock,Lock实际上就是一个接口,用户可以实现Lock接口,完成对锁的控制,也可以并发包里面的Lock锁实现类ReentrantLock 使用锁,但是大部分人都是只是停留在会使用的基础上,很少去了解Lock锁底层是怎么实现的,当需要换工作的时候,面试又是经常被问到,然而却经常说不出来,去年底我也开始面试,面试了好几家公司都被问到锁的实现原理,然而我却什么都没说得上来,只是会说比较肤浅的使用而已

为什么要学习底层原理?

前几年我出去面试,动不动就被问底层实现原理,每每我都回答不上,回答上的都是没什么技术含量的会使用,仅此而已。但是很明显,回答不上实现原理,那么好的工作机会就会和你擦肩而过,就算人家给你offer,薪资也是被压得死死的,为了拿到高薪,为了能找到好工作,为了有个好的前程,必须要学会底层实现原理,不仅仅是Lock锁。学会了手写Lock,接下来再去看源码,那就简单多了

手写Lock实现

创建一个类HarryLock,实现Lock接口,我们手写Lock实现,以学习为目的,简单实现加锁和释放锁就可以,本文主要讨论的是互斥锁

了解多线程并发的都知道,互斥锁的特性,只有一个线程拥有锁,其他并发的线程只能等待锁释放后,才能抢锁,如果线程持有锁,可以再次获取锁,也就是可重入,因此,我们需要定义几个变量:
1.定义一个owner变量,也就是拥有锁的线程对象
2.定义一个count,用来记录获取锁的次数
3.定义一个等待队列waiter,用来存储抢锁失败的线程

代码如下:

    Thread owner;

    AtomicInteger count=new AtomicInteger();

    LinkedBlockingDeque<Thread>waiter=new LinkedBlockingDeque<>();

获取锁

首先我们来写尝试获取锁,如果获取失败,直接返回,代码如下

    @Override
    public boolean tryLock() {
    
    
        int c = count.get();
        if(c>0){
    
    
            //已经有线程持有锁
            if(Thread.currentThread()==owner){
    
    
                //当前线程,可重入锁+1
                c=c+1;
                count.set(c);
                return true;
            }
        }else {
    
    
            if(count.compareAndSet(c,c+1)){
    
    
                //加锁成功
                owner=Thread.currentThread();
                return true;
            }
        }
        return false;
    }

线程在获取锁的过程中,如果获取不到锁,是会阻塞,直到获取到锁,因此,获取锁的接口实现,需要有一个死循环,去获取锁,如果获取失败,则要挂起线程,避免死循环消耗性能

    @Override
    public void lock() {
    
    
        if(!tryLock()){
    
    
        	waiter.offer(Thread.currentThread());
            for (;;){
    
    
                //加锁不成功,自旋
                Thread head = waiter.peek();
                if(head!=null&&Thread.currentThread()==head){
    
    
                   if(!tryLock()){
    
    
                       //获取锁失败,挂起线程
                       LockSupport.park();
                   }else {
    
    
                       //获取锁成功,将当前线程从头部删除
                       waiter.poll();
                       return;
                   }
                }else {
    
    
                    LockSupport.park();     //将当前线程挂起
                }
            }
        }
    }

释放锁

释放锁就简单一些,只有持有锁的线程,才能释放锁,否则就抛异常,当释放锁后,要唤醒其他线程继续抢锁

    @Override
    public void unlock() {
    
    
        int c = count.get();
        if(c>0&&owner!=Thread.currentThread()){
    
    
            //当前线程不是持有锁的线程,释放锁是要报错的
            throw new IllegalMonitorStateException();       //抛IllegalMonitorStateException
        }
        count.set(c-1);
        Thread peek = waiter.peek();
        if(peek!=null){
    
    
            LockSupport.unpark(peek);
        }
        c = count.get();
        if(c==0){
    
    
            owner=null;
        }
    }

好了,现在获取锁和释放锁的代码基本实现完成,接下来我们来做一个测试

测试HarryLock锁

package com.blockman.test;

import java.util.concurrent.locks.Lock;

public class LockTest {
    
    
    static int count=0;
    public static void main(String[] args) {
    
    
        Lock lock=new HarryLock();
        for (int x=0;x<10;x++){
    
    
            new Thread(){
    
    
                @Override
                public void run() {
    
    
//                    lock.lock();
//                    lock.lock();
                    for (int y=0;y<1000;y++){
    
    
                        count++;
                    }
                    System.out.println(Thread.currentThread().getId()+"::"+count);
//                    lock.unlock();
//                    lock.unlock();

                }
            }.start();
        }
    }
}

首先,没有加锁时,直接运行,count结果理论上不是10000
在这里插入图片描述
将注释放开后,运行程序,每次结果都是10000,说明加锁和释放锁几乎没啥问题,另外如果同一个线程连续加锁多次,也是能够成功的,说明我们手写的锁也支持可重入锁

总结

当没有线程持有锁时,需要CAS去加锁,CAS成功的线程持有锁,并把当前获取到锁的线程赋值给owner对象,记录当前获取到锁的线程。其他获取锁失败的线程,进入死循环,等待被唤醒,继续抢锁。其实,Java并发包中提供的Lock实现基本思路和我们自定义的Lock锁实现大致一样的,如果自己学会了手写,再回去看源码,理解起来就会更加简单了

猜你喜欢

转载自blog.csdn.net/huangxuanheng/article/details/123072972