Lock锁实现,手把手教学

Lock 锁

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

synchronized与lock

lock是一个接口,而synchronized是在JVM层面实现的。synchronized释放锁有两种方式:

  1. 获取锁的线程执行完同步代码,释放锁 。
  2. 线程执行发生异常,jvm会让线程释放锁。

lock锁的释放,出现异常时必须在finally中释放锁,不然容易造成线程死锁。lock显式获取锁和释放锁,提供超时获取锁、可中断地获取锁。

synchronized是以隐式地获取和释放锁,synchronized无法中断一个正在等待获取锁的线程。

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作。

lock源码

在阅读源码的成长的过程中,有很多人会遇到很多困难,一个是源码太多,另一方面是源码看不懂。在阅读源码方面,我提供一些个人的建议:

  • 第一个是抓主舍次,看源码的时候,很多人会发现源码太长太多,看不下去,这就要求我们抓住哪些是核心的方法,哪些是次要的方法。当舍去次要方法,就会发现代码精简和很多,会大大提高我们阅读源码的信心。
  • 第二个是不要死扣,有人看源码会一行一行的死扣,当看到某一行看不懂,就一直停在那里死扣,知道看懂为止,其实很多时候,虽然看不懂代码,但是可以从变量名和方法名知道该代码的作用,java中都是见名知意的。

接下来进入阅读lock的源码部分,在lock的接口中,主要的方法如下:

public interface Lock {
 // 加锁
 void lock();
 // 尝试获取锁
 boolean tryLock();
 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
 // 解锁
 void unlock();
} 

在lock接口的实现类中,最主要的就是ReentrantLock,来看看ReentrantLock中lock()方法的源码:

// 默认构造方法,非公平锁
public ReentrantLock() {
 sync = new NonfairSync();
}
// 构造方法,公平锁
public ReentrantLock(boolean fair) {
 sync = fair ? new FairSync() : new NonfairSync();
}
// 加锁
public void lock() {
 sync.lock();
} 

在初始化lock实例对象的时候,可以提供一个boolean的参数,也可以不提供该参数。提供该参数就是公平锁,不提供该参数就是非公平锁。

手写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

image.png
将注释放开后,运行程序,每次结果都是10000,说明加锁和释放锁几乎没啥问题,另外如果同一个线程连续加锁多次,也是能够成功的,说明我们手写的锁也支持可重入锁。更多有关Android开发进阶技术的学习,可以点击查看《Android核心技术手册》点击可查看详细类目。


总结

  • lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
  • lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
  • lock释放锁的过程:修改状态值,调整等待链表。

猜你喜欢

转载自blog.csdn.net/m0_70748845/article/details/134082162