2020/5/5
介绍
生产者与消费者就是程序中有两大类线程
生产者
,消费者
这两大类,生产者生产的资源数据供消费者消费。一般情况下,有一块共享内存,生产者向这块内存中生产,消费者从中取出数据进行消费;
- 关键要协调生产线程与消费线程,一个时刻只能有一个线程访问资源(这就要确保各个线程所有线程互斥)
- 此外,
生产者线程与消费者线程之间要进行通信,采取什么样的通信机制呢?何时唤醒何时阻塞
在Java中
BlockingQueue
阻塞队列中使用到了生产者与消费者模式,我们先自己实现一个生产者与消费者模式,之后再分析BlocingQueue
源码我们举的例子为,多个线程对一个数进行加与减操作,确保这个数的值为0或1之间变换,可以采用原生的
synchronized+notify
和reebtranLock+signall+await
两种方式,其对应的代码如下:
Synchronized版本生产者消费者
package Multithread.Test;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/5/5 0005 14:22
* 生产者 消费者模式
* 要保证两点:
* 1、各个线程之间要同步----》即一个时刻只能有一个线程访问共享资源(因此要加锁)
* 2、生产者线程与消费者线程要能实现通信
* 两个线程交替执行
* A--B
*/
public class Consumer01 {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{while (true)data.add();},"A").start();
new Thread(()->{while (true)data.decry();},"B").start();
new Thread(()->{while (true)data.decry();},"C").start();
new Thread(()->{while (true)data.decry();},"D").start();
}
}
//共享资源
//在多线程并发中首先要找到 共享资源
class Data{
//多个线程对这个数据进行操作
//+1 -1的操作
private int num = 0;
//加1的操作
public synchronized void add(){
//假如该值为1则需要进行等待
//将if改成while避免虚假唤醒
while (num==1){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//否则,将值+1
System.out.println(Thread.currentThread().getName()+"进行加操作职位"+(num++));
// Wakes up all threads that are waiting on this object's monitor.
//唤起所有在等待这个监视器的线程(这里就是等待data的监视器)
notifyAll();
}
public synchronized void decry(){
while (num==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"进行减法操作"+(num--));
//唤起所有等待这个监视器的线程
notifyAll();
}
}
reentranLock版本生产者与消费者
package Multithread.Test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/5/5 0005 14:50
*
*/
public class Consumer02 {
public static void main(String[] args) {
Data2 data2 = new Data2();
new Thread(()->{while (true) data2.add();},"A").start();
new Thread(()->{while (true) data2.decry();},"b").start();
}
}
class Data2{
private int number = 0;
//定义可重入锁
private ReentrantLock lock= new ReentrantLock();
//监视器
private Condition condition = lock.newCondition();
public void add(){
lock.lock();
try {
//等待
while (number==1){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//
System.out.println(Thread.currentThread().getName()+"进行了加"+(++number));
//唤醒等待在这个监事器上的线程
condition.signalAll();
}finally {
lock.unlock();
}
}
public void decry(){
lock.lock();
try{
while (number==0){//一定要加while防止虚假唤醒
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"的找为"+(--number));
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
}
注意在进行条件等待的时候,必须使用
while
条件进行判断,这个是在jdk
源码中说明了的,防止虚假唤醒
ArrayBlockingQueue源码分析
在
ArrayBlockingQueue
中使用到了生产者与消费者模式
,比如说底层的数组
满了的话,若数组已经满了,则向数组中添加数据的线程则不能添加数据,直到数组中有空的位置;若数组中没有数据了,则不能从数组中取数据,直到数组中有了数据,才可以通知取数据线程从数组中取出数据。底层用的是ReetranLock
注意:在ArrayBlockingQueue
底层有多组API,有的可以阻塞队列,有的不会阻塞队列,在这里我们分析的是put与take
- 底层用的是一把
锁
注意与Arrayblicking的区别
测试
package Multithread.Test;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/5/9 0009 22:43
*/
public class TestArrayBlockingQueue {
public static void main(String[] args) throws InterruptedException {
//阻塞队列的容量为3
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.put(1);
blockingQueue.put(2);
blockingQueue.put(3);
//从阻塞队列中取出数据,取5个数据,最后肯定会一直阻塞,因为我们只向阻塞队列中添加了4个数据
new Thread(()->{for(int i=0;i<5;i++) {
try {
Thread.sleep(1000);
System.out.println(blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//此时阻塞队列已经满了,进程会阻塞,
blockingQueue.put(4);
}
}
底层数据
/** The queued items */
//共享内存
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
生产者put()方法分析
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//尝试加锁
lock.lockInterruptibly();
try {
//若数组已经满了,在等待不满通知.这里采用的就是while循环 避免虚假唤醒
while (count == items.length)
notFull.await();
//调用一个封装的方法
enqueue(e);
} finally {
lock.unlock();
}
}
//
/**
* Inserts element at current put position, advances, and signals.
* Call only when holding lock.
*/
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//对数组添加元素
items[putIndex] = x;
//循环数组,当数组满了
if (++putIndex == items.length)
putIndex = 0;
//计算数组中的元素怒
count++;
//通知 数组中有数据了,可以进行取值了
notEmpty.signal();
}
消费者take()
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加锁
lock.lockInterruptibly();
try {
//数组为空,在等待不是空通知的瞎弄
while (count == 0)
notEmpty.await();
//取数据
return dequeue();
} finally {
lock.unlock();
}
}
/**
* Extracts element at current take position, advances, and signals.
* Call only when holding lock.
*/
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//取数据了
E x = (E) items[takeIndex];
items[takeIndex] = null;
//循环
if (++takeIndex == items.length)
takeIndex = 0;
count--;
//
if (itrs != null)
itrs.elementDequeued();
//通知可以声场数据
notFull.signal();
return x;
}
LinkedBlockingQueue源码
- 这里底层用的是两把锁
ReentrantLock takeLock
和ReentrantLock putLock
主以与ArrayBlocingQueue的区别(只有一把锁)
- 底层是链表,若没有指明初始值则大小为
Integer.MaxValue
,- 因为是两把锁所以锁的粒度比较小,但是可能引起线程不安全的问题,所以需要对
队列中的数据有一个准确的把握,因此使用一个原子类AtomicInteger count
底层数据结构
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
put源码
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//加锁
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
//LinkedBloking默认的长度Integer.mAXvALUE若没有指定会非常大则不会阻塞
//若达到设置的阈值,则假数据线程阻塞,也都是while
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
//通过cas进行加1
c = count.getAndIncrement();
//
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
/**
* Signals a waiting take. Called only from put/offer (which do not
* otherwise ordinarily lock takeLock.)
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
take源码
public E take() throws InterruptedException {
E x;
int c = -1;
//原子引用(为什么原子运用呢?()
final AtomicInteger count = this.count;
//取数据的锁
final ReentrantLock takeLock = this.takeLock;
//加锁
takeLock.lockInterruptibly();
try {
//这里获取的数据就是 主内存的数据 (voilitale修饰的数据避免了线程不安全)
while (count.get() == 0) {
notEmpty.await();
}
//返回链表头部数据
x = dequeue();
//线程安全,对链表长度加1
c = count.getAndDecrement();
//加入链表长度大于1
if (c > 1)
notEmpty.signal();
} finally {
//解锁
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
/**
* Removes a node from head of queue.
*
* @return the node删除链表头部节点并返回
*/
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
小结:ArrayBlockingQueue与LinkedBlocking的区别
- 底层结构:一个数组一个链表
- 加锁的方式,
ArrayBlocking所有线程一把锁,无论生产线程还是消费线程
;BlockingQueue生产者一把锁,消费者一把锁,生产者强putLock,消费者抢takeLock这吧锁
- LinkedBlockong的长度可以非常长,默认为
Integer.Max_vALUE