BooleanLock 详解

BooleanLock是对Synchronized锁的扩展性封装

Synchronized锁的作用

  • 被Synchronized锁包裹,同一时刻,只能有一个线程在访问,通过线程互斥来达到同步

Synchronized锁的缺陷

  • 因争抢Synchronized锁造成线程阻塞,无法中断
  • 因争抢Synchronized锁造成线程阻塞,没有时限,会一直阻塞,直到获取到锁。

举个例子:

class MyThread extends Thread{
    @Override
    public void run() {
        synchronized (this){
            try {
                Log.e("syncMethod","我抢到了锁");
                TimeUnit.DAYS.sleep(1);
            } catch (InterruptedException e) {
                Log.e("syncMethod","我被打断");
                e.printStackTrace();
            }
        }
    }
}

public static void main(String[] args){
     Thread thread1 = new MyThread ();
     Thread thread2 = new MyThread ();
      
      thread1.start();
      // 为了保证thread1先启动
      TimeUnit.MILLISECONDS.sleep(5);
      thread2.start();
      thread2.interrupt();
}

如上所示,thread1先启动拿到了锁,睡眠1天,在这个时间内,thread1不会释放锁,于是thread2一直在等待thread1释放锁,并且无法打断,主动调用interrupt方法也无法打断,thread2会一直阻塞。

Synchronized的问题就在于拿到锁的线程如果不释放锁,其他线程就会一直阻塞,而BooleanLock的核心就在于线程在拿到锁后,会访问或者改变一个Boolean值,然后立刻释放锁,这样就不会有线程在等待其他线程释放锁,而这个Boolean值就是记录是否有人拿到了锁。

现在我们来实现一个Boolean锁,一个Boolean锁应该可以打断,并且可以设置阻塞时长,如果在指定时长内无法获取到锁,就抛出异常。

public interface Lock {
    /**
     * lock方法跟synchronized同步非常类似,除非获取到了锁,否则就会一直阻塞,区别在于此种方法可以中断,抛出异常
     * @throws InterruptedException
     */
    void lock() throws InterruptedException;

    /**
     * 相对于lock()方法多了一个超时功能
     * @param mills 超过这个时间线程还没有释放锁,会抛出超时
     * @throws InterruptedException
     */
    void lock(long mills) throws InterruptedException,TimeoutException;

    /**
     * 释放锁
     */
    void unlock();

    /**
     * 当前哪些线程陷入阻塞
     * @return 阻塞的线程
     */
    List<Thread> getLockedThreads();
}

实现Lock锁,需要有三个变量,一个Boolean值记录是否有线程获取到了锁,第二个记录当前获取到锁的线程,第三个则是记录当前正在阻塞的线程集合。

首先来实现一个可以打断的锁

    private boolean locked;

    private Thread currentThread;

    private List<Thread> blockedList = new ArrayList<>();
    
    public void lock throws InterruptedException {
        //lock方法体使用synchronized包裹,lock完就退出了锁 @1
        synchronized(this){
            while(locked){
                //如果locked标志为true,表示锁被别人获取了
                if(!blockedList.contains(Thread.currentThread())){
                    blockedList.addd(Thread.currentThrad());
                }
               try {
                    //locked标志为true,当前线程就等待locked为false,自身等待,wait方法释放真实的锁 @2
                    this.wait();
                } catch (InterruptedException e){
                    //这里线程是上面的wait方法被打断会跳出,因为线程是调用wait方法主动阻塞
                    // 而不是在等待synchronized锁释放而陷入阻塞,所以是可以打断的
                    //所以这里是整个BooleanLock机制的核心!!!
                    blockedThreadList.remove(Thread.currentThread());
                    throw new InterruptedException(Thread.currentThread().getName() + "被打断");
                }
                
                //如果走到这里,表示之前没有线程获取到锁,我获取到了
                blockedThreadList.remove(Thread.currentThread());
                //上锁
                locked = true;
                currentThread = Thread.currentThread();
            }
        }
    }

  public void unlock() {
        synchronized (this){
            //如果当前线程是获取到锁的线程,就可以去执行释放锁的操作
            if (currentThread == Thread.currentThread()){
                //释放锁
                this.locked = false;
                //唤醒所有正在wait而陷入阻塞的线程,因为locked已经被设置成false,所以可以进入下一轮争抢
                this.notifyAll();
            }
        }
    }
上面的Lock是可以主动unlock,来唤醒正在陷入BooleanLock阻塞的线程,并且打断它,之后是否需要再次抢夺锁,就看自己的需要了。核心在于上面注释的@1和@2两个地方,无论是线程是获取到锁成功,还是已经有线程抢先,而我只能wait,我都释放了真实的锁,没有线程陷入被动的阻塞。

但是,还有一种需要,就是超时,我抢夺一个锁,如果在指定时间内我没有抢到,我就主动跳出这个抢夺,去做其他事情。

实现一个有超时功能的锁

public void lock(long mills) throws InterruptedException,TimeoutException {
        synchronized (this){
            //如果传入的时间不合法,就直接调用没有超时功能的Lock
            if(mills <= 0){
                this.lock();
            }else{
                long lastMills = mills;
                //这里记录一个时间戳,totalMills表示到达这个时刻,如果还没有获取到锁,就报超时
                long totalMills = lastMills + System.currentTimeMillis();
                while (locked){
                    if (lastMills <= 0){
                        throw new TimeoutException(Thread.currentThread().getName() + "超时" + mills + ".mills" + "没有获取到锁");
                    }

                    if(!blockedThreadList.contains(Thread.currentThread())){
                        blockedThreadList.add(Thread.currentThread());
                    }
                    //这里释放了真实的锁
                    this.wait(lastMills);
                    // lastMills表示还需要等待多久时间,而lastMills减少表示两种情况
                    // 第一种: 线程已经wait到时间了,自动唤醒了,这个时候已经过去了lastMills的时间,下面这个操作就把lastMills置0
                    // 第二种: 线程wait没有到时间,被unlock唤醒,但是被唤醒后还是没有抢到锁,于是lastMills被减少,
                    //        继续等待,时间减去已经等待了多长时间
                    lastMills = totalMills - System.currentTimeMillis();
                }
                blockedThreadList.remove(Thread.currentThread());
                locked = true;
                currentThread = Thread.currentThread();
            }
        }

Boolean的核心就是防止线程被动阻塞,上面注释里面的锁表示一个Boolean值,true表示已经上锁,false表示没有上锁。而注释里面的真实的锁表示synchronized锁。获取到真实的锁后,线程总是很快的释放真实的锁,没有线程需要等待synchronized锁被释放,转而去等待locked值被释放(locked被置为false),这种等待的阻塞,完全是线程主动的阻塞,所有可以被打断。

Tips: 希望没有人问出,为什么locked不标示为volatile...

完整代码

public class BooleanLock implements Lock{
    private Thread currentThread;

    private boolean locked = false;

    private final List<Thread> blockedThreadList = new ArrayList<>();

    @Override
    public void lock() throws InterruptedException {
        synchronized (this){
            // lock方法体使用synchronized包裹,lock完就释放真实的锁 @1
            while (locked){
                 if(!blockedThreadList.contains(Thread.currentThread())){
                     blockedThreadList.add(Thread.currentThread());
                 }
                try {
                    //locked标志为true,当前线程就等待locked为false,自身等待,wait方法释放真实的锁 @2
                    this.wait();
                }catch (InterruptedException e){
                     //这里线程是上面的wait方法被打断会跳出,因为线程是调用wait方法主动阻塞
                     // 而不是在等待synchronized锁释放而陷入阻塞,所以是可以打断的,
                     //所以这里是整个BooleanLock机制的核心!
                    blockedThreadList.remove(Thread.currentThread());
                    throw new InterruptedException(Thread.currentThread().getName() + "被打断");
                }
            }

            //如果走到这里,表示之前没有线程获取到锁,我获取到了
            blockedThreadList.remove(Thread.currentThread());
            //上锁
            locked = true;
            currentThread = Thread.currentThread();
        }
    }

    @Override
    public void lock(long mills) throws InterruptedException,TimeoutException {
        synchronized (this){
            //如果传入的时间不合法,就直接调用没有超时功能的Lock
            if(mills <= 0){
                this.lock();
            }else{
                long lastMills = mills;
                //这里记录一个时间戳,totalMills表示到达这个时刻,如果还没有获取到锁,就报超时
                long totalMills = lastMills + System.currentTimeMillis();
                while (locked){
                    if (lastMills <= 0){
                        throw new TimeoutException(Thread.currentThread().getName() + "超时" + mills + ".mills" + "没有获取到锁");
                    }

                    if(!blockedThreadList.contains(Thread.currentThread())){
                        blockedThreadList.add(Thread.currentThread());
                    }
                    //这里释放了真实的锁
                    this.wait(lastMills);
                    // lastMills表示还需要等待多久时间,而lastMills减少表示两种情况
                    // 第一种: 线程已经wait到时间了,自动唤醒了,这个时候已经过去了lastMills的时间,下面这个操作就把lastMills置0
                    // 第二种: 线程wait没有到时间,被unlock唤醒,但是被唤醒后还是没有抢到锁,于是lastMills被减少,
                    //        继续等待,时间减去已经等待了多长时间
                    lastMills = totalMills - System.currentTimeMillis();
                }
                blockedThreadList.remove(Thread.currentThread());
                locked = true;
                currentThread = Thread.currentThread();
            }
        }

    }

    @Override
    public void unlock() {
        synchronized (this){
            //如果当前线程是获取到锁的线程,就可以去执行释放锁的操作
            if (currentThread == Thread.currentThread()){
                //释放锁
                this.locked = false;
                //唤醒所有正在wait而陷入阻塞的线程,因为locked已经被设置成false,所以可以进入下一轮争抢
                this.notifyAll();
            }
        }
    }

    @Override
    public List<Thread> getLockedThreads() {
        //unmodifiableList表示不可修改,防止阻塞队列被强行主动修改,出现错误
        return Collections.unmodifiableList(blockedThreadList);
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 摘要: 我们已经知道,synchronized 是Java的关键字,是Java的内置特性,在JVM层面实现了对临界...
    kingZXY2009阅读 1,847评论 0 20
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 3,008评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,517评论 1 15
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。在这之前...
    4ea0af17fd67阅读 597评论 2 17
  • 我:哇撒,你给我拍得照片太好看了,好专业。每一张都特别美,又高又瘦,我都很喜欢。哈哈,太感谢啦! 陌生人:景美人更...
    肖馨肖馨阅读 127评论 0 0