实验记录
首先分析生产者与消费者问题需要解决的点:
1.在缓冲区为空时,消费者不能再进行消费
2.在缓冲区为满时,生产者不能再进行生产
3.在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作,即保持线程间的同步
4.注意条件变量与互斥锁的顺序
由于笔者通过java线程的方式实现,java库封装了许多可以便捷解决该问题的类:
解决同步问题的有:
1.Synchronized代码块,通过monitor监视器实现
2.Lock类,通过系统级CAS(Compare And Swap)原子性操作以及锁自旋实现(乐观锁)。
解决缓冲区读写问题的有:
1.阻塞队列(BlockingQueue),在队列满时阻塞添加操作,队列空时阻塞出队操作。
由于java编写该问题相对于C++而言简单了许多,因此笔者将用不同的方式实现该问题并且附上java相对应的实现源码与方式。
1.使用数组作为缓冲区,Synchronized同步操作。
2.使用数组作为缓冲区,Lock同步操作。
3.使用阻塞队列作为缓冲区。
使用数组作为缓冲区
public class Test {
//数组缓冲区,长度为10
int[] buf = new int[10];
//size标记缓冲数组中的数据数
int size = 0;
void prodeceRun(){
Thread producer = new Thread(){
@Override
public void run(){
while(true) {
synchronized (buf) {
if (size == buf.length) {
try {
System.out.println("缓冲区已满,生产者线程阻塞...");
buf.wait();//生产者等待,wait方法会释放该锁
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//生产者生产物品
System.out.println("生产者即将生产物品,当前缓冲区中共有" + size + "个物品");
try {
Thread.sleep(2000);//模拟生产者生产过程延时2秒
buf[size] = 1;
size++;
System.out.println("生产者生产完毕,用时2000ms,当前缓冲区中有" + size + "个物品" );
buf.notify();//唤醒消费者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
producer.start();
}
void customerRun(){
Thread customer = new Thread(){
@Override
public void run(){
while(true) {
synchronized (buf) {
if (size == 0) {
try {
System.out.println("缓冲区为空,消费者线程阻塞...");
buf.wait();//缓冲区中没有物品,wait等待...
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//生产者生产物品
System.out.println("消费者即将消费物品,当前缓冲区中共有" + size + "个物品");
try {
Thread.sleep(500);//模拟生产者生产过程延时500毫秒
buf[size-1] = 0;
size--;
System.out.println("消费者消费完毕,耗时500ms,当前缓冲区中有" + size + "个物品");
buf.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
customer.start();
}
public static void main(String[] args) {
Test test = new Test();
test.customerRun();
test.prodeceRun();
test.prodeceRun();
test.prodeceRun();
}
}
运行结果及Monitor同步方法导致的不公平竞争锁
运行结果:
可以看出,解决了生产者消费者的问题,但是没有完美解决,由于锁竞争的问题,总是导致生产者连续生产或是消费者连续消费,因此需要引入公平锁的概念:
公平锁:表示线程获取锁的顺序是按照加锁的顺序来分配的,及先来先得,先进先出的顺序。
非公平锁:表示获取锁的抢占机制,是随机获取锁的,和公平锁不一样的就是先来的不一定能拿到锁,
由于Synchronized无法解决公平锁问题,将在下面的Lock实现中解决…
Lock解决生产者消费者问题
简介Lock实现方式
先大致简介一下java中的Lock类,以下部分为笔者通过观看JDK源码后的个人描述,因此可能有所出入。
以下是jdk9.0.4中Lock类源码的注释部分
* {@code Lock} implementations provide more extensive locking
* operations than can be obtained using {@code synchronized} methods
* and statements. They allow more flexible structuring, may have
* quite different properties, and may support multiple associated
* {@link Condition} objects.
*
* <p>A lock is a tool for controlling access to a shared resource by
* multiple threads. Commonly, a lock provides exclusive access to a
* shared resource: only one thread at a time can acquire the lock and
* all access to the shared resource requires that the lock be
* acquired first. However, some locks may allow concurrent access to
* a shared resource, such as the read lock of a {@link ReadWriteLock}.
*
* <p>The use of {@code synchronized} methods or statements provides
* access to the implicit monitor lock associated with every object, but
* forces all lock acquisition and release to occur in a block-structured way:
* when multiple locks are acquired they must be released in the opposite
* order, and all locks must be released in the same lexical scope in which
* they were acquired.
*
* <p>While the scoping mechanism for {@code synchronized} methods
* and statements makes it much easier to program with monitor locks,
* and helps avoid many common programming errors involving locks,
* there are occasions where you need to work with locks in a more
* flexible way. For example, some algorithms for traversing
* concurrently accessed data structures require the use of
* "hand-over-hand" or "chain locking": you
* acquire the lock of node A, then node B, then release A and acquire
* C, then release B and acquire D and so on. Implementations of the
* {@code Lock} interface enable the use of such techniques by
* allowing a lock to be acquired and released in different scopes,
* and allowing multiple locks to be acquired and released in any
* order.
简单的意思就是:
Lock实现类中应当维护一个内部类AbstractQueueSynchronizer(抽象队列同步器以下简称为AQS)作为同步手段,AQS内部通过维护一个State变化量以及一个等待同步队列,并通过CAS原子操作配合线程自旋来获取锁。
大致过程如下:
线程通过Lock.lock()方法,调用AQS的acquire方法竞争Lock对象的锁,此时会检测AQS中的state变化量是否已经被占用(该检测使用CAS操作同步,因此不用担心两个线程若同步对state变化量进行竞争会同时拿到锁),若没有被占用,则直接在AQS中标明state变化量由本线程占用,若已经被占用,则说明本线程需要等待加锁。
以下是公平锁加锁源码
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
Lock通过CAS操作向AQS中维护的同步队列申请加入(由于是原子性,因此不用担心两个线程同时申请加入队列导致另一个线程丢失的线程安全问题),如果CAS操作失败,则说明有线程在并发进行入队操作,则Lock通过CAS+自旋的操作来进入队列,以下是该过程的源码:
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(Node node) {
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return oldTail;
}
} else {
initializeSyncQueue();
}
}
}
线程加入同步等待队列后,通过自旋不断检测本线程是否在队列头部,若在头部则检测能否对state变化量占用,若不能占用则不断循环自旋。
以下是该过程源码:
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
以上就是Lock的全部执行过程,那么公平锁是如何实现的呢?首先可以分析易知,公平锁的实质是锁已经被占用时,其他线程在同步队列中的顺序应当是线程竞争的顺序,也就是对入队方法进行扩展即可。
Lock介绍完毕,下面将是实际代码
Lock实现生产者消费者模式
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test2 {
//数组缓冲区,长度为10
int[] buf = new int[10];
//size标记缓冲数组中的数据数
int size = 0;
void prodeceRun(Lock lock, Condition condition){
Thread producer = new Thread(){
@Override
public void run(){
while(true) {
lock.lock();
if (size == buf.length) {
try {
System.out.println("缓冲区已满,生产者线程阻塞...");
condition.await();//生产者等待,wait方法会释放该锁
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//生产者生产物品
System.out.println("生产者即将生产物品,当前缓冲区中共有" + size + "个物品");
try {
Thread.sleep(2000);//模拟生产者生产过程延时2秒
buf[size] = 1;
size++;
System.out.println("生产者生产完毕,用时2000ms,当前缓冲区中有" + size + "个物品" );
condition.signal();//唤醒消费者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
}
}
};
producer.start();
}
void customerRun(Lock lock,Condition condition){
Thread customer = new Thread(){
@Override
public void run(){
while(true) {
lock.lock();
if (size == 0) {
try {
System.out.println("缓冲区为空,消费者线程阻塞...");
condition.await();//缓冲区中没有物品,wait等待...
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//生产者生产物品
System.out.println("消费者即将消费物品,当前缓冲区中共有" + size + "个物品");
try {
Thread.sleep(500);//模拟生产者生产过程延时500毫秒
buf[size-1] = 0;
size--;
System.out.println("消费者消费完毕,耗时500ms,当前缓冲区中有" + size + "个物品");
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
}
}
};
customer.start();
}
public static void main(String[] args) {
Test2 test = new Test2();
Lock lock = new ReentrantLock(true);
Condition condition = lock.newCondition();
test.customerRun(lock,condition);
test.prodeceRun(lock,condition);
}
}
代码运行结果及分析
可以看出,解决了2.1中的公平性问题,生产者和消费者是交替进行的(因为生产过程中消费者竞争锁,生产结束后生产者也开始竞争锁,由于消费者竞争的时间顺序在生产者前,因此锁总是被消费者占用)。但是由于设定的生产用时2000ms,消费用时500ms,该实例并不能很好的体现生产者消费者问题,于是笔者将生产者的数量提升到5个,观察代码运行结果:
阻塞队列实现
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class Test3 {
BlockingQueue buf = new ArrayBlockingQueue(10,true);//缓冲区长度为10,设为公平锁
public void producerRun(){
Thread producer = new Thread(){
@Override
public void run(){
while(true) {
try {
buf.put("联想小新Pro 16 2021款");
System.out.println("生产者生产物品...生产后缓冲区中有" + buf.size() + "件物品");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
}
public void customerRun(){
Thread customer = new Thread(){
@Override
public void run(){
while(true){
try {
buf.take();
System.out.println("消费者消费物品,消费后缓冲区中还有" + buf.size() + "件物品");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
customer.start();
}
public static void main(String[] args) {
Test3 test = new Test3();
test.customerRun();
test.producerRun();
test.producerRun();
test.producerRun();
test.producerRun();
test.producerRun();
}
}