java多线程(3):Lock接口和Condition监视器接口使用详解

时间:2022-01-21 17:58:54

前言

前面使用的synchronized关键字来保证同步时,每个锁对象都有自己的一套监视器方法(wait,notify,notifyAll),这些方法是继承自Object类的。也就是说,锁对象被实例化后,只有一套监视器方法,一个线程要唤醒其他线程只能使用notifyAll方法,这也是多线程中的效率问题的根源。

为此,jdk1.5引入了java.util.concurrent.locks包,并提供了Lock和Condition接口及实现类。下面来看这两个接口的jdk文档。

  • Lock 实现提供了比使用 synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
  • Condition 将 Object 监视器方法(wait、notify 和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待set(wait-set)。其中,Lock 替代了synchronized 方法和语句的使用,Condition 替代了 Object监视器方法的使用。

这两个接口将“锁对象”和“监视器对象”分开,这样,一个“锁”就可以关联多个“监视器对象”。也就能实现生产者线程固定唤醒消费者线程,消费者线程固定唤醒生产者线程。

正文

一,Lock和Condition接口的组合使用

我们来修改上一篇中的多生产者和多消费者代码。

package com.jimmy.ThreadCommunication;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Resource3{ // 资源函数

private String productName; // 共享资源不变
private int count = 1;

private Lock lock = new ReentrantLock(); // 定义一个锁对象
private Condition conProducer = lock.newCondition(); // 获得lock锁的"生产者"线程监视器对象
private Condition conConsumer = lock.newCondition(); // 获得lock锁的"消费者"线程监视器对象

private boolean flag = false; // 资源类增加一个标志位,默认false,也就是没有资源

public void produce(String name){

lock.lock(); // 获取锁

try { // 业务代码要写在try块中
while (flag == true) { // flag判断不变,即如果flag为true,也就是有资源了,生产者线程就去等待。
try {
conProducer.await(); // "生产者"线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}

this.productName = name + count;
count ++;
System.out.println(Thread.currentThread().getName()+"....生产者.."+this.productName);

flag = true; // 生产完了就将flag修改为true
conConsumer.signal(); // 随机唤醒"消费者"线程中被await的一个线程
}
finally
{
lock.unlock(); // 无论如何都要释放锁
}
}

public void consume() {
lock.lock();

try {
while (flag == false) {
try {
conConsumer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...消费者.."+this.productName);

flag = false; // 消费完了就把标志改为false
conProducer.signal(); // 随机唤醒"生产者"线程中被await的一个线程

} finally {
lock.unlock(); // 无论如何,最后要释放锁
}
}
}

class Producer3 implements Runnable{ // 生产者类不变

private Resource3 res;

//生产者初始化就要分配资源
public Producer3(Resource3 res) {
this.res = res;
}

@Override
public void run() {
for (int i = 0; i < 5; i++) {

res.produce("bread");
}
}

}

class Comsumer3 implements Runnable{ // 消费者类不变

private Resource3 res;

//同理,消费者一初始化也要分配资源
public Comsumer3(Resource3 res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {

res.consume();
}
}

}

public class ProducerAndConsumer3 {
public static void main(String[] args) { // 测试程序不变

Resource3 resource = new Resource3(); // 实例化资源

Producer3 producer = new Producer3(resource); // 实例化生产者,并传入资源对象
Comsumer3 comsumer = new Comsumer3(resource); // 实例化消费者,并传入相同的资源对象

Thread threadProducer1 = new Thread(producer); // 创建2个生产者线程
Thread threadProducer2 = new Thread(producer);

Thread threadComsumer1 = new Thread(comsumer); // 创建2个消费者线程
Thread threadComsumer2 = new Thread(comsumer);

threadProducer1.start();
threadProducer2.start();
threadComsumer1.start();
threadComsumer2.start();
}
}

代码与上一篇博客中相比,Lock.lock()和Lock.unlock()代替了“synchronized”关键字,而且Lock的使用更加灵活,也更加“面向对象”。Condition.await()/Condition.signal()/Condition.signalAll()方法替代了以前的this.await()/this.signal()/this.signalAll()方法,更重要的是,Lock和Condition分开,而且一个Lock可以绑定多个Condition,这就为唤醒对方线程打下了基础,从而提高了多线程的执行效率。

二,多资源的情况

前面写的代码都是多个线程对单一资源的操作,实际中资源会放在一个集合中,生产者不停的往里面放,消费者不停的从里面取,下面就是将资源放进数组中循环存取的示例代码。

package com.jimmy.ThreadCommunication;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BoundedBuffer {

final Lock lock = new ReentrantLock(); // 获得"锁"对象
final Condition notFull = lock.newCondition(); // 得到锁对象的监视器对象
final Condition notEmpty = lock.newCondition();

final Object[] items = new Object[50]; // 资源放进数组中循环存取
int putptr; // 生产者脚标
int takeptr; // 消费者脚标
int count; // 资源总数

public void put(Object x) throws InterruptedException {
lock.lock(); // 一进来就锁上
try {
while (count == items.length) // 生产者判断组数满了就等待
notFull.await();
items[putptr] = x;
if (++putptr == items.length) // 脚标到头就又从头开始
putptr = 0;
++count;
System.out.println(Thread.currentThread().getName()+"...生产..No "+putptr+"..剩余 "+count);
notEmpty.signal(); // 唤醒消费者线程
} finally {
lock.unlock(); // 最终一定要释放锁
}
}

public Object take() throws InterruptedException { // 注释跟上面函数的差不多
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
System.out.println(Thread.currentThread().getName()+"..消费..No "+takeptr+"..剩余 "+count);
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}

class Producer4 implements Runnable{

private BoundedBuffer buffer;
public Producer4(BoundedBuffer buffer) {
this.buffer = buffer;
}

@Override
public void run() {
int i = 0;
while(i < 200) {
try {
buffer.put("bread");
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Consumer4 implements Runnable{
private BoundedBuffer buffer;
public Consumer4(BoundedBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
int i = 0;
while(i < 200) {
try {
buffer.take();
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProducerAndConsumer4 {
public static void main(String[] args) {
BoundedBuffer boundedBuffer = new BoundedBuffer();

Producer4 producer4 = new Producer4(boundedBuffer);
Consumer4 consumer4 = new Consumer4(boundedBuffer);

Thread produceThread1 = new Thread(producer4);
Thread produceThread2 = new Thread(producer4);

Thread consumeThread1 = new Thread(consumer4);
Thread consumeThread2 = new Thread(consumer4);

produceThread1.start();
produceThread2.start();

consumeThread1.start();
consumeThread2.start();
}
}

来看一下输出的结果,只截取了其中一部分:

Thread-1...生产..No 1..剩余 1
Thread-1...生产..No 2..剩余 2
Thread-1...生产..No 3..剩余 3
Thread-1...生产..No 4..剩余 4
Thread-1...生产..No 5..剩余 5
Thread-1...生产..No 6..剩余 6
Thread-1...生产..No 7..剩余 7
Thread-1...生产..No 8..剩余 8
Thread-1...生产..No 9..剩余 9
Thread-1...生产..No 10..剩余 10
Thread-1...生产..No 11..剩余 11
Thread-1...生产..No 12..剩余 12
Thread-1...生产..No 13..剩余 13
Thread-1...生产..No 14..剩余 14
Thread-1...生产..No 15..剩余 15
Thread-1...生产..No 16..剩余 16
Thread-2..消费..No 1..剩余 15
Thread-2..消费..No 2..剩余 14
Thread-2..消费..No 3..剩余 13
Thread-2..消费..No 4..剩余 12
Thread-2..消费..No 5..剩余 11
Thread-2..消费..No 6..剩余 10
Thread-2..消费..No 7..剩余 9
Thread-2..消费..No 8..剩余 8
Thread-2..消费..No 9..剩余 7
Thread-2..消费..No 10..剩余 6
Thread-2..消费..No 11..剩余 5
Thread-2..消费..No 12..剩余 4
Thread-2..消费..No 13..剩余 3
Thread-2..消费..No 14..剩余 2
Thread-2..消费..No 15..剩余 1
Thread-2..消费..No 16..剩余 0
Thread-0...生产..No 17..剩余 1
Thread-0...生产..No 18..剩余 2
Thread-0...生产..No 19..剩余 3
Thread-0...生产..No 0..剩余 4
Thread-0...生产..No 1..剩余 5
Thread-0...生产..No 2..剩余 6
Thread-0...生产..No 3..剩余 7
Thread-0...生产..No 4..剩余 8
Thread-0...生产..No 5..剩余 9
Thread-0...生产..No 6..剩余 10
Thread-0...生产..No 7..剩余 11
Thread-0...生产..No 8..剩余 12
Thread-0...生产..No 9..剩余 13
Thread-0...生产..No 10..剩余 14
Thread-0...生产..No 11..剩余 15
Thread-0...生产..No 12..剩余 16
Thread-0...生产..No 13..剩余 17
Thread-0...生产..No 14..剩余 18
Thread-0...生产..No 15..剩余 19
Thread-0...生产..No 16..剩余 20

我们来看,生产者线程Thread-1首先得到执行权,然后往能容纳20个资源对象的数组中存数据,存到第16个后,被消费者线程Thraed-2获得执行权,它从第一个资源开始取,一直取到第16个,此时数组中的资源已经空了,就通知生产者线程继续生产,生产者会接着上次停留的地方(也就是脚标17)继续生产,到头了就又从头开始生产。循环往复。

总结

Lock和Condition的组合使用,使“锁对象”和锁的“监视器对象”分离,这样既操作灵活,又可以直接唤醒对方线程,提高多线程的效率。