并发编程原理与实战(二十)ReentrantLock与Condition实战应用解析

上一篇讲解了Condition接口各个API的功能,本文继续对ReentrantLock+Condition的具体使用进行深入分析。

实现2个线程依次输出123456…

在系列文章的第二篇中,分析了采用synchronized与wait+notify实现一个经典的题目:如何控制2个线程依次输出1 2 3 4 5 6…?既然ReentrantLock可以代替synchronized,Condition可以代替wait+notify,可以实现更精细的并发协同控制,那么我们就来用ReentrantLock+Condition来实现这一个经典的题目。

ReentrantLock+Condition方案

我们回顾下这个题目的思路:

步骤1:创建2个线程t1,t2。

步骤2:t1运行输出1,t1停止输出(等待t2输出完,进入等待状态)。

步骤3:t2运行输出2,t2停止输出(等待t1输出完,进入等待状态)。

步骤4:循环执行步骤2和步骤3,即t1和t2依次执行”运行<->等待“动作。

这其中有两个等待,一个是“t1等待t2”,一个是“t2等待t1”,结合前面所学的Condition可以创建多个等待条件变量,可以创建两个Condition条件用来表示“t1等待t2”和“t2等待t1”。Condition提供了让线程进入等待状态的await()方法和唤醒其他线程的signal()方法,可以用来代替Object来的wait()方法和notify()方法。基于上面分析,我们对之前的代码改造如下:

public class ReentrantLockCollaboration {
    //输出数字
    private static int num = 1;
    //公共变量,线程执行标识
    private static boolean thread1Run = true;
    //锁对象
    private static final ReentrantLock lockObject = new ReentrantLock();
    // 线程1运行条件
    private static final Condition thread1RunCondition = lockObject.newCondition();
    // 线程2运行条件
    private static final Condition thread2RunCondition = lockObject.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (num < 10) {
                    //获取锁对象
                    lockObject.lock();
                    try {
                        //判断是否轮到自己执行
                        while (!thread1Run) {
                            try {
                                //没到就进入等待
                                thread1RunCondition.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " output " + num++);
                        thread1Run = false;
                        //执行完输出,通知t2执行
                        thread2RunCondition.signal();
                    } finally {
                        lockObject.unlock();
                    }
                }
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (num < 10) {
                    //获取锁对象
                    lockObject.lock();
                    try {
                        //判断是否轮到自己执行
                        while (thread1Run) {
                            //没到就进入等待
                            try {
                                thread2RunCondition.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " output " + num++);
                        thread1Run = true;
                        //执行完输出,通知t2执行
                        thread1RunCondition.signal();
                    } finally {
                        lockObject.unlock();
                    }
                }
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

运行结果:

t1 output 1
t2 output 2
t1 output 3
t2 output 4
t1 output 5
t2 output 6
t1 output 7
t2 output 8
t1 output 9
t2 output 10

结果符合我们的预期。

实现3个线程依次输出123456…

上面的例子中只有2个线程,一个线程输出完成后就唤醒另外的一个线程,现在我们对题目升级下:用3个线程依次输出1 2 3 4 5 6…那该如何实现?

按之前的方法我们对题目进行步骤拆解分析:

步骤1:创建3个线程t1,t2,t3。

步骤2:t1运行输出1,t1停止输出,进入等待状态(等待t2,t3输出完成)。

步骤3:t2运行输出2,t2停止输出,进入等待状态(等待t3,t1输出完成)。

步骤4:t3运行输出3,t3停止输出,进入等待状态(等待t1,t2输出完成)。

步骤5:循环执行步骤2、步骤3、步骤4,整个过程只有一个线程处于输出状态,另外两个线程处于等待状态。

先思考下,如果用synchronized与wait+notify该怎么实现呢?在只有2个线程的情况下,通过公共的变量用来标识是否轮到自己执行,2个线程内部通过判断公共变量的值是否轮到自己执行,不是t1执行就是t2执行。但是现在变成3个线程了,用一个公共变量标识是否轮到自己执行明显不可行了,因为t1执行修改公共变量后,t2和t3都读取到公共变量轮到自己执行,无法控制是t2先执行还是t3先执行。

那么通过什么方式标识是否轮到自己执行呢?也就是用什么来做无限循环条件判断?找出无限循环判断条件是解决这个题目的核心点。这个题目还有哪些特征?根据对题目的步骤分析,我们推导出程序的运行结果:

t1 output 1
t2 output 2
t3 output 3
t1 output 4
t2 output 5
t3 output 6
t1 output 7
t2 output 8
t3 output 9

观察下上面的预期结果,我们发现线程的序号和输出数字直接存在一定的关系:输出数字除以3的余数和线程序号除以3的余数相等。

1除以3的余数是1;

2除以3的余数是2;

3除以3的余数是0;

4除以3的余数是1;

5除以3的余数是2;

6除以3的余数是0;

所以我们可以通过这个关系来判断是否轮到当前线程执行,输出数字除以3的余数和线程序号除以3的余数相等说明轮到当前线程执行。

synchronized与wait+notify方案

基于上述分析,我们先用synchronized与wait+notify的方式实现,代码如下:

public class ConcurrentCollaboration {
    //输出数字
    private static int num = 1;
    //锁对象
    private static final Object lockObject = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(num < 10) {
                    //获取锁对象
                    synchronized (lockObject) {
                        //判断是否轮到自己执行
                        while (num%3 != 1%3) {
                            //没到就进入等待
                            try {
                                lockObject.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " output " + num++);
                        //执行完输出,通知t2,t3执行
                        lockObject.notifyAll();
                    }
                }
            }
        },"t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(num < 10) {
                    //获取锁对象
                    synchronized (lockObject) {
                        //判断是否轮到自己执行
                        while (num%3 != 2%3) {
                            //没到就进入等待
                            try {
                                lockObject.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " output " + num++);
                        //执行完输出,通知t1,t3执行
                        lockObject.notifyAll();
                    }
                }
            }
        },"t2");

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(num < 10) {
                    //获取锁对象
                    synchronized (lockObject) {
                        //判断是否轮到自己执行
                        while (num%3 != 3%3) {
                            //没到就进入等待
                            try {
                                lockObject.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " output " + num++);
                        //执行完输出,通知t1,t2执行
                        lockObject.notifyAll();
                    }
                }
            }
        },"t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

我们对创建线程任务代码的方式改造下,用更符合面向方式改造代码:

public class ConcurrentCollaboration {
    //输出数字
    private static int num = 1;
    //锁对象
    private static final Object lockObject = new Object();
    //输出数字最大值
    private static final int max = 10;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Printer(1));
        Thread t2 = new Thread(new Printer(2));
        Thread t3 = new Thread(new Printer(3));

        t1.start();
        t2.start();
        t3.start();
    }

    static class Printer implements Runnable {
        private final int threadId;

        public Printer(int threadId) {
            this.threadId = threadId;
        }

        @Override
        public void run() {
            while (num < max) {
                synchronized (lockObject) {
                    //判断是否轮到自己执行
                    while (num % 3 != threadId % 3) {
                        try {
                            //没到就进入等待
                            lockObject.wait();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    if (num <= max) {
                        System.out.println("t" + threadId + " output " + num);
                        num++;
                    }
                    //执行完输出,通知其他线程执行
                    lockObject.notifyAll();
                }
            }
        }
    }
}

运行程序结果符合我们的预期。

ReentrantLock+Condition方案

synchronized与wait+notify的方案中,无法为指定的线程设置单独的等待条件,也无法指定唤醒某个线程。我们可以使用Condition给3个线程设置不同的条件变量,通过条件变量指定某个线程进入等待或者唤醒状态。实现如下:

public class ConcurrentCollaboration {
    //锁对象
    private static final ReentrantLock lockObject = new ReentrantLock();
    // 线程1运行条件
    private static final Condition condition1 = lockObject.newCondition();
    // 线程2运行条件
    private static final Condition condition2 = lockObject.newCondition();
    // 线程3运行条件
    private static final Condition condition3 = lockObject.newCondition();
    //输出数字
    private static int num = 1;
    //输出数字最大值
    private static final int max = 10;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Printer(1));
        Thread t2 = new Thread(new Printer(2));
        Thread t3 = new Thread(new Printer(3));

        t1.start();
        t2.start();
        t3.start();
    }

    static class Printer implements Runnable {
        private final int threadId;

        public Printer(int threadId) {
            this.threadId = threadId;
        }

        @Override
        public void run() {
            while (num < max) {
                //获取锁
                lockObject.lock();
                try {
                    //判断是否轮到自己执行
                    while (num % 3 != threadId % 3) {
                        try {
                            //没到就进入等待
                            if (threadId == 1) {
                                condition1.await();
                            } else if (threadId == 2) {
                                condition2.await();
                            } else if (threadId == 3) {
                                condition3.await();
                            }
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    if (num <= max) {
                        System.out.println("t" + threadId + " output " + num);
                        num++;
                    }

                    //执行完输出,通知其他线程执行
                    if (threadId == 1) {
                        condition2.signal();
                    } else if (threadId == 2) {
                        condition3.signal();
                    } else if (threadId == 3) {
                        condition1.signal();
                    }
                } finally {
                    //释放锁
                    lockObject.unlock();
                }
            }
        }
    }
}

运行程序结果同样符合我们的预期。通过上面的例子可以看出,通过Condition条件变量可以进行具体到某个线程更细粒度的等待和唤醒控制。

ReentrantLock在jdk中的具体应用

ReentrantLock的典型应用在生产者-消费者协模型中,如jdk中的ArrayBlockingQueue。ArrayBlockingQueue是Java并发包中基于数组实现的有界阻塞队列,其核心设计围绕线程安全和生产者-消费者协作展开。下面我们就来分析ReentrantLock在ArrayBlockingQueue中是如何保证线程安全的。

锁与条件变量的定义

public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, Serializable {
    private static final long serialVersionUID = -817911632652898426L;
    //存储数据的数组
    final Object[] items;
    //指向下一个待出队元素的数组下标
    int takeIndex;
    //指向下一个待入队元素的数组下标
    int putIndex;
    //实际元素数量
    int count;
    //锁
    final ReentrantLock lock;
    //非空的条件变量
    private final Condition notEmpty;
    //非满的条件变量
    private final Condition notFull;
    transient ArrayBlockingQueue<E>.Itrs itrs;
    ......
        
    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and the specified access policy.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        //创建数组对象
        this.items = new Object[capacity];
        //创建锁
        lock = new ReentrantLock(fair);
        //创建条件变量
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }        
}

ArrayBlockingQueue定义了ReentrantLock类型的lock对象,一个Condition类型队列非空的条件变notEmpty,一个Condition类型队列非满的条件变量notFull,这三个对象保证队列数据入队和出队是的线程安全操作。这三个对象在ArrayBlockingQueue的构造函数值进行初始化。下面摘取关键的入队(如put)和出队(如take)操作来分析线程安全控制方法。

put操作

public void put(E e) throws InterruptedException {
    Objects.requireNonNull(e);
    //获得锁对象
    ReentrantLock lock = this.lock;
    //上锁,可中断
    lock.lockInterruptibly();

    try {
        //无限循环判断队列实际元素数量是否等于数组长度
        while(this.count == this.items.length) {
            //非满条件阻塞生产者,停止插入元素
            this.notFull.await();
        }
		//插入元素
        this.enqueue(e);
    } finally {
        //释放锁
        lock.unlock();
    }
}

private void enqueue(E e) {
    Object[] items = this.items;
    //通过待入队元素的数组下标定位插入位置
    items[this.putIndex] = e;
    if (++this.putIndex == items.length) {
        this.putIndex = 0;
    }
	//队列实际长度加1
    ++this.count;
    //非空条件唤醒消费者线程取数据
    this.notEmpty.signal();
}

如代码所示,put方法内部在插入元素前先去获取一把锁,获取过程可以被中断,然后通过无限循环判断队列实际元素数量是否等于数组长度,等于则通过非满条件阻塞生产者,停止插入元素。否则执行插入元素逻辑,通过待入队元素的数组下标定位插入位置,队列实际长度加1,然后通过非空条件唤醒消费者线程。

take操作

public E take() throws InterruptedException {
    //获得锁对象
    ReentrantLock lock = this.lock;
    //上锁,可中断
    lock.lockInterruptibly();

    Object var2;
    try {
         //无限循环判断队列实际元素数量是否等于0
        while(this.count == 0) {
            //非空条件阻塞消费者,停止取数据
            this.notEmpty.await();
        }
		//取数据
        var2 = this.dequeue();
    } finally {
        //解锁
        lock.unlock();
    }

    return var2;
}

private E dequeue() {
    Object[] items = this.items;
    //通过待出队元素的数组下标
    E e = items[this.takeIndex];
    items[this.takeIndex] = null;
    if (++this.takeIndex == items.length) {
        this.takeIndex = 0;
    }
	//队列长度减1
    --this.count;
    if (this.itrs != null) {
        this.itrs.elementDequeued();
    }
	//非满条件唤醒生产者线程放入数据
    this.notFull.signal();
    return e;
}

如代码所示,take方法内部在获取元素前先去获取一把锁,获取过程可以被中断,然后通过无限循环判断队列实际元素数量是否等于0,等于则通过非空条件阻塞消费者,停止取数据。否则执行取出元素逻辑,通过待出队元素的数组下标定位取出元素位置,队列长度减1,然后通非满条件唤醒生产者线程放入数据。

线程安全分析

通过上面的ArrayBlockingQueue的部分关键代码分析,ArrayBlockingQueue内部存在共享资源竞争,底层的数组(items)、计数器(count)和入队出队位置索引(putIndex/takeIndex)是多个线程共享的变量。对这些共享变量操作均需要在加锁后才能操作,确保多线程环境下对队列的各种操作行为的正确性。

其次,一个入队和出队操作均包含多个步骤,比如入队时需要先判断数组长度、再写入数据到指定为止、指向下一个入队元素的数组下标加1,队列实际长度加1。完成这一组步骤必须作为一个原子单元执行,保证要么执行全部完成,要么执行失败,不允许出现一部分执行成功,一部分执行失败的情况,保证了数据的完整性和正确性。

最后,通过锁与多条件变量的控制,队列满了则生产者调用await()方法阻塞,非满则插入数据并调用signal()唤醒消费者取数据。消费者取数据时,队列为空则调用signal()唤醒生产者存入数据。通过这一系列的阻塞-唤醒操作实现多线程之间的高效协作。

总结

本文以多线程依次输出123456…的经典例子来讲解ReentrantLock+Condition的使用,并与采用synchronized与wait+notify实现方案进行对比,体现了ReentrantLock+Condition更精细的并发协同控制优势。最后通过分析ReentrantLock在ArrayBlockingQueue的具体应用,进一步加深了对ReentrantLock+Condition使用的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值