九、AQS之ReentrantLock

(一)概念

1.1 可重入锁

可重入锁又叫递归锁,同一个线程可以重复获取同一把锁,不会因为之前的锁没有释放而阻塞。

1.2 公平锁和非公平锁

假设一共有3个线程T0,T1,T2;此时T0持有资源的锁,那么公平与非公平的定义如下

  • 公平锁:按照线程的等待顺序获取锁,T1和T2线程到达后会进行等待。
  • 非公平锁:T1或T2线程到达后会先尝试获取锁,如果可以拿到则直接用。

1.3 独占锁

像synchronized、ReentranLock都是独占锁,同一个时刻只允许一个线程获取锁。

1.4 ReentrantLock

它是一种可重入的独占锁,默认非公平锁,功能类似于synchronized,相对于synchronized它具备如下特点:

  • 可以手动释放锁
  • 可以设置超时时间
  • 可以设置为公平锁

(二)常用API

ReentrantLock实现了Lock接口规范,常见API如下:

void lock()

[阻塞] 获取锁,调用该方法当前线程会获取锁

void lockInterruptibly() throws InterruptedException

[阻塞] 可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程

boolean tryLock()

[非阻塞] 尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false

boolean tryLock(long time, TimeUnit unit) throws InterruptedException

超时获取锁,当前线程在以下三种情况下会被返回false:

当前线程在超时时间内获取了锁

当前线程在超时时间内被中断

超时时间结束

void unlock()

释放锁

Condition newCondition()

获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁

2.1 基本使用

在使用时要注意 4 个问题:

  1. 默认情况下 ReentrantLock 为非公平锁而非公平锁;
  2. 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
  3. 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
  4. 释放锁一定要放在 finally 中,否则会导致线程阻塞。
//加锁  阻塞 
lock.lock(); 
try {  
    ...
} finally { 
    // 解锁 
    lock.unlock();  
}


//尝试加锁   非阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

2.2 基本案例

余票有8张,现在有10个用户去抢票,未对资源加锁的情况

public class Test {
    //余票总数
    static int count = 8;
    public static void main(String[] args) {
        //开启10个线程去抢票
        for (int i = 0; i < 10; i++) {
            new Thread(() ->{
                if (count > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + "抢到了第" + count +"张票");
                } else {
                    System.out.println(Thread.currentThread().getName() + "没有抢到票");
                }
            },i + "号用户").start();
        }
    }
}

因为没有对资源进行加锁处理,本应有两个用户没有抢到票,现在出现了超卖的情况

2.2.1 加锁处理
public class Test {
    //余票总数
    static int count = 8;
    public static void main(String[] args) {
        //创建锁对象
        ReentrantLock lock = new ReentrantLock();
        //开启10个线程去抢票
        for (int i = 0; i < 10; i++) {
            new Thread(() ->{
                lock.lock();
                try {
                    if (count > 0) {
                        try {
                            Thread.sleep(20);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName() + "抢到了第" + count +"张票");
                    } else {
                        System.out.println(Thread.currentThread().getName() + "没有抢到票");
                    }
                }finally {
                    lock.unlock();
                }
            },i + "号用户").start();
        }
    }
}

2.3 tryLock使用场景

2.3.1 尝试获取锁

尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false

案例:执行装车任务,一个车道同时只允许一个车辆执行,其他车辆执行要打印错误报告。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new CarNo(),"一号线程");
        Thread thread2 = new Thread(new CarNo(),"二号线程");
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }

    static class CarNo implements Runnable{
        static ReentrantLock lock = new ReentrantLock();
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            if (lock.tryLock()) {
                try{
                    System.out.println(name + "开始执行装车任务...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ignored) {
                    }
                    System.out.println(name + "装车任务执行完成!");
                }finally {
                    lock.unlock();
                }
            } else {
                System.out.println(name + "进入装车站失败,当前存正在执行的任务");
            }
        }
    }
}

2.3.2 超时获取锁

超时获取锁,当前线程在以下三种情况下会被返回false:

  • 当前线程在超时时间内获取了锁
  • 当前线程在超时时间内被中断
  • 超时时间结束

案例:执行装车任务,一个车道同时只允许一个车辆执行,其他车辆超时未执行要打印错误报告。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new CarNo(),"一号线程");
        Thread thread2 = new Thread(new CarNo(),"二号线程");
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }

    static class CarNo implements Runnable{
        static ReentrantLock lock = new ReentrantLock();
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            try {
                if (lock.tryLock(1100, TimeUnit.MILLISECONDS)) {
                    try{
                        System.out.println(name + "开始执行装车任务...");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException ignored) {
                        }
                        System.out.println(name + "装车任务执行完成!");
                    }finally {
                        lock.unlock();
                    }
                } else {
                    System.out.println(name + "进入装车站失败,当前存正在执行的任务");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
2.3.3 等待锁时允许中断

当某个线程在等待锁时,如果不想一直等待我们可以给它设置超时时间,也可以通过外部来中断等待。

案例:公司有两名员工,一个是甩锅员工,霸占着电脑不用;另外一名是背锅员工,想干活没有电脑干;老板看着背锅员工一直没有活干就把他开除了。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread thread0 = new Thread(() -> {
            lock.lock();
            try {
                while (true) {
                }
            } finally {
                lock.unlock();
            }
        },"甩锅员工");
        Thread thread1 = new Thread(() -> {
            try {
                //此时锁一直被thread0锁持有
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName()+"被开除了");
            }finally {
                lock.unlock();
            }
        },"背锅员工");
        Thread thread2 = new Thread(thread1::interrupt,"老板");
        thread0.start();
        thread1.start();
        Thread.sleep(1000);
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

2.3.4 等待与通知

与Object的wait和notify类似,只不过Object的notify无法通知到具体的某个线程及某个任务;而Condition可以作为多任务条件,比如ReentrantLock创建了两个执行条件A和B,你可以控制到具体的条件是否执行。

注意:必须先获得锁才能执行该API

案例:学生填写请假单等待老师审批,只有老师审批过后学生才能离校。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        //请假审批执行条件
        Condition A = lock.newCondition();
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                String name = Thread.currentThread().getName();
                System.out.println(name + "提交请假单,等待班主任审批...");
                A.await();
                System.out.println(name + "班主任审批完成,允许离校。");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        },"学生");
        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                A.signal();
            } finally {
                lock.unlock();
            }
        },"班主任");
        thread1.start();
        Thread.sleep(1500);
        thread2.start();
        thread1.join();
        thread2.join();
    }
}
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值