ReentrantLock详解

文章目录

  • 前言
  • 常用API
    • Lock接口
    • 基本语法
  • ReentrantLock使用
    • 独占锁:模拟抢票场景
    • 公平锁和非公平锁
    • 可重入锁
    • 结合Condition实现生产者消费者模式
  • 应用场景总结


前言

ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于 synchronized,ReentrantLock具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与 synchronized 一样,都支持可重入

它的主要应用场景是在多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全性

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

常用API

Lock接口

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

接口 作用
void lock() 获取锁,调用该方法当前线程会获取锁,当锁获得后,该方法返回
void lockInterruptibly() throws InterruptedException 可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock() 尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 超时获取锁,当前线程在以下三种情况下会被返回:
当前线程在超时时间内获取了锁
当前线程在超时时间内被中断
超时时间结束,返回false
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁

基本语法

//加锁 阻塞
lock.lock();
try {
	...
} finally {
	// 解锁
	lock.unlock();
}


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

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

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

ReentrantLock使用

独占锁:模拟抢票场景

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();//默认非公平
    private static int tickets = 8;

    public void buyTicket(){
        lock.lock();
        try{
            if(tickets>0){
                try {
                    Thread.sleep(10);//休眠10ms,模拟出并发效果
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"购买了第"+tickets--+"张票");
            }else {
                System.out.println("票已经卖完了,"+Thread.currentThread().getName()+"抢票失败");
            }

        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
        for(int i = 1;i<=10;i++){
            new Thread(()->{
                ticketSystem.buyTicket();
            },"线程"+i).start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("剩余票数:"+tickets);

    }
}
/**
 *
 * 不加锁效果:出现超卖问题
 * 线程4购买了第8张票
 * 线程6购买了第8张票
 * 线程10购买了第5张票
 * 线程5购买了第8张票
 * 线程9购买了第6张票
 * 线程7购买了第7张票
 * 线程8购买了第4张票
 * 线程3购买了第8张票
 * 线程1购买了第3张票
 * 线程2购买了第8张票
 * 剩余票数:2
 *
 *
 * 加锁效果:正常,两个人抢票失败
 * 线程1购买了第8张票
 * 线程5购买了第7张票
 * 线程6购买了第6张票
 * 线程4购买了第5张票
 * 线程8购买了第4张票
 * 线程3购买了第3张票
 * 线程7购买了第2张票
 * 线程9购买了第1张票
 * 票已经卖完了,线程2抢票失败
 * 票已经卖完了,线程10抢票失败
 * 剩余票数:0
 */

公平锁和非公平锁

ReentrantLock支持公平锁和非公平锁两种模式:

  • 公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
  • 非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁。ReentrantLock默认是非公平锁
ReentrantLock lock = new ReentrantLock();//参数默认false,不公平锁
ReentrantLock lock = new ReentrantLock(true);//公平锁

比如买票的时候就有可能出现插队的场景,允许插队就是非公平锁,如下图:
ReentrantLock详解_第1张图片

public class ReentrantLockDemo {
    private final UseLock lock = new UseLock();//默认非公平
    private static int tickets = 999;

    private static Thread nextThread = null;

    private class UseLock extends ReentrantLock{
        public UseLock(){
            super();
        }

        public UseLock(boolean fair){
            super(fair);
        }

        @Override
        public Collection<Thread> getQueuedThreads(){
            return super.getQueuedThreads();
        }

        @Override
        public Thread getOwner(){
            return super.getOwner();
        }
    }
    public void buyTicket(){
        lock.lock();
        try {
            if(tickets>0){
                try {
                    Thread.sleep(10);//休眠10ms,模拟出并发效果
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                Thread tempThread = lock.getQueuedThreads().stream().sorted((pre,next)->-1).collect(Collectors.toList()).get(0);
                System.out.println((nextThread==Thread.currentThread()||nextThread==null? "":Thread.currentThread().getName()
                +"插队成功")+Thread.currentThread().getName()+"购买了第"+tickets--+"张票"+"-----下一个线程应为:"+tempThread.getName());
                //System.out.println(lock.getQueuedThreads().stream().map(e->e.getName()).sorted((pre,next)->-1).collect(Collectors.toList()));
                nextThread = tempThread;
            }else {
                System.out.println("票已经卖完了,"+Thread.currentThread().getName()+"抢票失败");
            }
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
        for(int i = 1;i<=1000;i++){
            new Thread(()->{
                ticketSystem.buyTicket();
            },"线程"+i).start();
            Thread.sleep(2);//让线程一个个启动来插队
        }

//        try {
//            Thread.sleep(3000);
//        } catch (InterruptedException e) {
//            throw new RuntimeException(e);
//        }
//        System.out.println("剩余票数:"+tickets);

    }
}


/**
 * 线程1购买了第999张票-----下一个线程应为:线程2
 * 线程2购买了第998张票-----下一个线程应为:线程3
 * 线程3购买了第997张票-----下一个线程应为:线程4
 * 线程4购买了第996张票-----下一个线程应为:线程5
 * 线程5购买了第995张票-----下一个线程应为:线程6
 * 线程6购买了第994张票-----下一个线程应为:线程7
 * 线程7购买了第993张票-----下一个线程应为:线程8
 * 线程8购买了第992张票-----下一个线程应为:线程9
 * 线程9购买了第991张票-----下一个线程应为:线程10
 * 线程10购买了第990张票-----下一个线程应为:线程11
 * 线程11购买了第989张票-----下一个线程应为:线程12
 * 线程12购买了第988张票-----下一个线程应为:线程13
 * 线程13购买了第987张票-----下一个线程应为:线程14
 * 线程14购买了第986张票-----下一个线程应为:线程15
 * 线程15购买了第985张票-----下一个线程应为:线程16
 * 线程16购买了第984张票-----下一个线程应为:线程17
 * 线程62插队成功线程62购买了第983张票-----下一个线程应为:线程17
 * 线程17购买了第982张票-----下一个线程应为:线程18
 * 线程18购买了第981张票-----下一个线程应为:线程19
 * 线程19购买了第980张票-----下一个线程应为:线程20
 * 线程20购买了第979张票-----下一个线程应为:线程21
 * 线程21购买了第978张票-----下一个线程应为:线程22
 * 线程22购买了第977张票-----下一个线程应为:线程23
 * 线程23购买了第976张票-----下一个线程应为:线程24
 * 线程24购买了第975张票-----下一个线程应为:线程25
 * 线程25购买了第974张票-----下一个线程应为:线程26
 * 线程26购买了第973张票-----下一个线程应为:线程27
 * 线程27购买了第972张票-----下一个线程应为:线程28
 * 线程28购买了第971张票-----下一个线程应为:线程29
 * 线程29购买了第970张票-----下一个线程应为:线程30
 * 线程30购买了第969张票-----下一个线程应为:线程31
 * 线程31购买了第968张票-----下一个线程应为:线程32
 * 线程32购买了第967张票-----下一个线程应为:线程33
 * 线程33购买了第966张票-----下一个线程应为:线程34
 * 线程34购买了第965张票-----下一个线程应为:线程35
 * 线程35购买了第964张票-----下一个线程应为:线程36
 * 线程36购买了第963张票-----下一个线程应为:线程37
 * 线程37购买了第962张票-----下一个线程应为:线程38
 * 线程38购买了第961张票-----下一个线程应为:线程39
 * 线程39购买了第960张票-----下一个线程应为:线程40
 * 线程40购买了第959张票-----下一个线程应为:线程41
 * 线程41购买了第958张票-----下一个线程应为:线程42
 * 线程42购买了第957张票-----下一个线程应为:线程43
 * 线程43购买了第956张票-----下一个线程应为:线程44
 * 线程44购买了第955张票-----下一个线程应为:线程45
 * 线程45购买了第954张票-----下一个线程应为:线程46
 * 线程46购买了第953张票-----下一个线程应为:线程47
 * 线程47购买了第952张票-----下一个线程应为:线程48
 * 线程48购买了第951张票-----下一个线程应为:线程49
 * 线程49购买了第950张票-----下一个线程应为:线程50
 * 线程50购买了第949张票-----下一个线程应为:线程51
 * 线程51购买了第948张票-----下一个线程应为:线程52
 * 线程52购买了第947张票-----下一个线程应为:线程53
 * 线程53购买了第946张票-----下一个线程应为:线程54
 * 线程206插队成功线程206购买了第945张票-----下一个线程应为:线程54
 * 线程54购买了第944张票-----下一个线程应为:线程55
 * 线程55购买了第943张票-----下一个线程应为:线程56
 * 线程217插队成功线程217购买了第942张票-----下一个线程应为:线程56
 * 线程56购买了第941张票-----下一个线程应为:线程57
 * 线程57购买了第940张票-----下一个线程应为:线程58
 * 线程228插队成功线程228购买了第939张票-----下一个线程应为:线程58
 * 线程58购买了第938张票-----下一个线程应为:线程59
 * 线程59购买了第937张票-----下一个线程应为:线程60
 * 线程60购买了第936张票-----下一个线程应为:线程61
 * 线程61购买了第935张票-----下一个线程应为:线程63
 * 线程63购买了第934张票-----下一个线程应为:线程64
 * ......
 */

将ReentrantLock改为公平锁以后将没有插队现象

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁可重入锁的一个优点是可一定程度避免死锁。在实际开发中,可重入锁常常应用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中。

public class Counter {

    private final ReentrantLock lock = new ReentrantLock();//默认非公平

    public void recursiveCall(int num){
        lock.lock();
        try {
            if(num==0) {
                return;
            }
            System.out.println("递归执行,num = "+num);
            recursiveCall(num-1);
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.recursiveCall(10);
    }
}

/**
 * 递归执行,num = 10
 * 递归执行,num = 9
 * 递归执行,num = 8
 * 递归执行,num = 7
 * 递归执行,num = 6
 * 递归执行,num = 5
 * 递归执行,num = 4
 * 递归执行,num = 3
 * 递归执行,num = 2
 * 递归执行,num = 1
 */

结合Condition实现生产者消费者模式

java.util.concurrent类库中提供Condition类来实现线程之间的协调。调用Condition.await() 方法使线程等待,其他线程调用Condition.signal() 或 Condition.signalAll() 方法唤醒等待的线程。
注意:调用Condition的await()和signal()方法,都必须在lock保护之内
案例:基于ReentrantLock和Condition实现一个简单队列

public class ConditionDemo {

    public static void main(String[] args) {
        Queue queue = new Queue(5);
        new Thread(new Producer(queue)).start();
        new Thread(new Customer(queue)).start();
    }

    static class Producer implements Runnable{

        private Queue queue;
        public Producer(Queue queue){this.queue = queue;}
        @Override
        public void run() {
            try {
                while(true){
                    Thread.sleep(new Random().nextInt(500,1000));
                    queue.put(new Random().nextInt(1000));
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    static class Customer implements Runnable{
        private Queue queue;
        public Customer(Queue queue){this.queue = queue;}
        @Override
        public void run() {
            try {
                while(true){
                    Thread.sleep(new Random().nextInt(500,1000));
                    System.out.println("customer消费:"+queue.take());
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    static class Queue{
        private Object[] items;

        int size = 0;
        int putIndex;
        int takeIndex;

        private final ReentrantLock lock = new ReentrantLock();
        private Condition notFull = lock.newCondition();
        private Condition notEmpty = lock.newCondition();

        public Queue(int capacity){
            this.items = new Object[capacity];

        }
        public void put(Object value) throws InterruptedException {
            lock.lock();
            try{
                while(size==items.length){
                    System.out.println("queue队列满了:"+ Arrays.asList(items));
                    notFull.await();
                }
                items[putIndex] = value;
                if(++putIndex == items.length){
                    putIndex = 0;
                }
                size++;
                notEmpty.signal();
            }finally {
                System.out.println("producer生产:" + value);
                lock.unlock();
            }
        }

        public Object take() throws InterruptedException {
            lock.lock();
            try {
                while(size==0){
                    System.out.println("queue队列为null:"+ Arrays.asList(items));
                    notEmpty.await();
                }
                Object value = items[takeIndex];
                items[takeIndex] = null;
                if(++takeIndex == items.length){
                    takeIndex = 0;
                }
                size--;
                notFull.signal();
                return value;
            }finally {
                lock.unlock();
            }

        }
    }
}

应用场景总结

ReentrantLock具体应用场景如下:

  1. 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次只有一个线程能够写入。
  2. 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。
  3. 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。

你可能感兴趣的:(并发编程,java,开发语言)