生产者与消费者案例-虚假唤醒

本文通过一个具体的案例,探讨了在多线程环境下生产者和消费者模型的实现方式及存在的问题。特别是针对虚假唤醒的问题,详细解释了如何通过将wait方法置于while循环中来解决这一问题。

以下是一个案例,有一个店员,负责进货和卖货。进货生产,卖货消费。

当商品超过10件,生产等待,消费继续,当少于0件,消费等待,消费继续。

正常代码如下:

package com.atguigu.juc;

/*
 * 生产者和消费者案例
 */
public class TestProductorAndConsumer {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        
        Productor pro = new Productor(clerk);
        Consumer cus = new Consumer(clerk);
        
        new Thread(pro, "生产者 A").start();
        new Thread(cus, "消费者 B").start();
    }
}
//店员
class Clerk {
    private int product = 0;
    //进货
    public synchronized void get(){//循环次数:0
        if(product >= 10){
            System.out.println("产品已满!");
    
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();
    }
    //卖货
    public synchronized void sale(){//product = 0; 循环次数:0
        if(product <= 0){
            System.out.println("缺货!");
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }
}

//生产者
class Productor implements Runnable{
    private Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) 
        {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
            }
            clerk.get();
        }
    }
}

//消费者
class Consumer implements Runnable{
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.sale();
        }
    }
}

运行结果:

很和谐没问题!,生产者每次生产完就等待一下,导致消费者抢到资源,这样导致:0,1轮替。

但是,如果此时再假如一个生产者和消费者:

public class TestProductorAndConsumer {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        
        Productor pro = new Productor(clerk);
        Consumer cus = new Consumer(clerk);
        
        new Thread(pro, "生产者 A").start();
        new Thread(cus, "消费者 B").start();

        new Thread(pro, "生产者 C").start();
       new Thread(cus, "消费者 D").start();
    }
}

此时运行结果:

 

可以看到,非常离谱!生产者数量为负数,并且一直没有停止的样子。

分析:

假如最开始是缺货状态,消费者B和D进入都是进入等待的,此时一个生产者抢到资源,进行生产,完事生产了一件,

两个消费者同时唤醒,唤醒了之后,每个消费者都继续下面代码,也就是wait下面的--product,导致数量为负数。

这个时候两个消费者再次进入当然还是等待,一个生产者再次进入,当然效果和上面一样,再次数量在-1的基础上,-1,-2。

 

这种现象叫做虚假唤醒。

 

为了解决这个,其实JDK中已经说明了,对于wait方法的使用,必须始终放在while循环中。

每次线程被唤醒之后都得重新进入循环,而不是直接执行下面的--或者++操作。

修改如下:

把上面的if换成while即可:

package com.atguigu.juc;

/*
 * 生产者和消费者案例
 */
public class TestProductorAndConsumer {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        
        Productor pro = new Productor(clerk);
        Consumer cus = new Consumer(clerk);
        
        new Thread(pro, "生产者 A").start();
        new Thread(cus, "消费者 B").start();

        new Thread(pro, "生产者 C").start();
        new Thread(cus, "消费者 D").start();
    }
}
//店员
class Clerk {
    private int product = 0;
    //进货
    public synchronized void get(){//循环次数:0
        while(product >= 10){//为了避免虚假唤醒问题,应该总是使用在循环中
            System.out.println("产品已满!");
    
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + ++product);
        this.notifyAll();
    }
    //卖货
    public synchronized void sale(){//product = 0; 循环次数:0
        while(product <= 0){
            System.out.println("缺货!");
            try {
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        System.out.println(Thread.currentThread().getName() + " : " + --product);
        this.notifyAll();
    }
}

//生产者
class Productor implements Runnable{
    private Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) 
        {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
            }
            clerk.get();
        }
    }
}

//消费者
class Consumer implements Runnable{
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            clerk.sale();
        }
    }
}

 

 

转载于:https://www.cnblogs.com/alsf/p/9131424.html

<think>好的,我现在需要帮助用户补全Consumer函数的代码。根据问题描述,消费者需要从链表中取出数据并打印,遇到-1时停止所有消费者,并且不删除该数据。首先,我需要理解消费者线程的工作流程。消费者应该循环等待条件变量,当有数据可用时被唤醒。使用互斥锁保护共享数据结构,这里是指链表beginData。 步骤一:在进入循环时,先获取互斥锁,确保对链表的访问是线程安全的。然后检查链表是否为空,如果为空,则等待条件变量cond。这里需要使用while循环而不是if,因为可能存在虚假唤醒的情况。 步骤二:当链表不为空时,取出第一个节点的数据。检查数据是否为-1。如果是,则不需要打印,直接释放该节点的内存,并设置beginData为NULL。然后需要通知其他消费者线程,可能通过条件变量再次触发,确保其他线程也能检测到-1的情况。最后break退出循环,并释放锁。 如果数据不是-1,打印数据,将头指针移动到下一个节点,并释放原头节点的内存。然后释放锁,以便其他线程可以继续操作。 需要注意,当遇到-1时,必须让其他消费者也退出。因此,在消费到-1后,可能需要再次触发条件变量,让其他线程有机会检查到-1的存在。或者,可以设置一个全局的退出标志,但题目中没有提到,所以可能需要通过链表中的-1数据来实现。但问题中提示,当消费到-1时,不删除该数据,这样其他线程也会处理到该数据。但用户提供的示例中,当遇到-1时,需要停止消费并退出,且不删除该数据。然而,原题中的提示可能有矛盾,因为用户代码在遇到-1时应该不删除数据,但示例中的代码却释放了节点。需要再仔细看问题描述。原问题提示说:“当遇到消费数据为-1时,退出线程,并且不删除该数据,那么其它消费线程也会消费到该条数据,并且也会退出”。但示例中的代码在遇到-1时,确实删除了该节点,这提示矛盾。可能用户给出的示例代码有误,或者问题描述需要重新理解。 这时候可能需要重新分析问题。正确的逻辑应该是:当消费者取出数据是-1时,不删除该节点,这样其他消费者线程也能取到同一个-1的数据,从而各自退出。但这样的话,链表中必须保留该节点。但示例代码中的处理是:如果数据是-1,则释放节点,并设置beginData为NULL。这会导致后续线程无法获取到该数据。这可能存在矛盾。 或者,可能问题中的示例代码是用户提供的,而实际正确的做法应该是在遇到-1时不删除节点,这样其他线程可以处理。但根据用户的问题描述,示例中的代码可能有问题。需要按照问题描述来补全代码,而不是示例。 根据用户的问题描述,正确的处理是:当消费者遇到-1时,停止消费,不删除该数据,这样其他消费者线程也会处理到该数据,从而全部退出。但示例代码中,当遇到-1时,释放了该节点,导致其他线程无法处理。这可能存在问题。 因此,正确的做法应该是:当数据是-1时,不删除节点,直接break,并通知其他线程。但这样的话,需要如何操作? 可能这里需要更仔细的处理。比如,当某个消费者取出数据是-1时,应该将节点放回链表头部,或者保持链表不变,这样其他消费者可以继续处理该节点。但这样会导致无限循环,因为每个消费者都会取出同一个-1的节点,然后退出。但根据问题提示,当遇到-1时,停止消费,并且不删除该数据,其他消费者也会消费到该数据,从而退出。 所以正确的逻辑应该是: 1. 消费者线程获取锁,检查链表是否为空,如果为空则等待条件变量。 2. 当链表不为空时,取出第一个节点的数据。 3.如果数据是-1: a. 不删除该节点(即不移动头指针,也不释放内存)。 b.释放锁,并退出线程。这样,其他消费者线程在获取锁后,同样会取出同一个节点,发现是-1,同样退出。但这样会导致该节点一直存在于链表中,无法被释放,造成内存泄漏。 但根据问题中的提示,当遇到-1时,不需要删除该数据,因此必须保留该节点。所以正确的处理步骤是: 当检测到数据为-1时,break循环,不移动头指针,也不释放该节点。这样,其他线程在下次获取锁后,同样会处理该节点,从而退出。因此,在代码中,当发现数据是-1时,应该:- 打印该数据吗?问题中说“-1数据不需要打印出来”,所以直接判断后break。 但这样的话,如何让其他消费者线程也退出?因为每个线程都需要处理到-1才能退出。而如果链表中有多个节点,当生产者在生产-1之后,可能链表中有多个数据,比如之前的数据和-1。消费者可能处理前面的数据,直到遇到-1。但根据问题描述,生产者可能在某个时刻生产-1,然后消费者需要处理到该节点时,停止所有消费者。 可能的实现方式是,当某个消费者线程遇到-1时,在退出前,需要将链表中的该节点保留,并通知其他线程继续处理。这样其他线程也会处理到该节点,从而退出。 因此,补全Consumer函数的步骤: 1.加锁。2. 使用while循环检查链表是否为空,如果为空则等待条件变量。 3.取出头节点的数据。 4.如果数据是-1:a. 不打印。 b. 解锁。c. break循环,退出线程。 5.否则: a. 打印数据。 b.移动头指针到下一个节点。c.释放原头节点。 d.解锁。 但问题在于,这种情况下,如果某个消费者处理了-1,但该节点未被删除,那么其他消费者可能无法处理到该节点,因为当该消费者处理完前面的节点后,可能-1节点已经被处理了? 或者,生产者可能只生产一个-1节点,此时所有消费者线程都会竞争处理该节点,但根据锁的机制,只有一个线程能获取到该节点,处理之后,其他线程会发现链表为空,继续等待。这样无法实现所有线程退出。 这说明原来的问题可能存在逻辑矛盾。根据问题提示,正确的处理方式应该是:当某个消费者线程遇到-1时,通知所有其他线程退出,可能通过设置一个全局变量,并在条件变量上广播。但问题中提示不允许修改全局变量,只能通过数据中的-1来触发。 或者,问题中的链表可能有多个节点,包括一个-1节点,所有消费者线程在处理完前面的节点后,最终会处理到该-1节点,每个处理到该节点的线程都会退出。但这样需要确保所有线程都能处理到该节点。 例如,假设链表中有三个数据:5,3, -1。三个消费者线程可能分别处理5、3、-1。处理-1的线程退出,其他两个处理完5和3的线程在后续循环中可能再次进入,发现链表为空,等待生产者的数据。此时如果生产者不再生产数据,这些线程将一直等待,无法退出。 因此,这可能意味着问题中的逻辑需要更复杂的处理,比如当某个线程遇到-1时,需要通知所有其他线程退出,可能通过设置一个全局标志,并使用条件变量广播。 但问题中的提示说明不允许使用额外的全局变量,只能通过数据中的-1。所以可能需要在处理-1时,将该节点保留在链表中,并广播条件变量,使得其他线程被唤醒,处理该节点。 但根据用户提供的代码框架,没有全局退出标志,所以必须通过链表中的-1节点来触发所有消费者线程的退出。 因此,正确的处理步骤应该是: 当消费者线程取出一个节点,发现数据是-1时:- 不打印,也不删除该节点(即不修改链表,保持头指针不变)。- 解锁,并退出循环。这样,其他线程在获取锁后,同样会处理同一个节点,发现-1,同样退出。但这里有一个问题:当多个线程同时等待条件变量时,一旦有一个线程处理了该节点(虽然不删除),其他线程在唤醒后会发现链表不为空,再次处理该节点,导致重复处理。 但根据问题描述,当遇到-1时,线程退出,并且不删除该数据,其他线程也会处理到该数据,所以每个线程在处理到-1时都会退出。 但这样的设计会导致该节点永远不会被删除,从而内存泄漏。但可能问题中的示例允许这种情况,或者在生产者线程中会在适当的时候释放该节点。 回到用户的问题,需要补全的代码: 在Consumer函数中,当遇到-1时,不打印,退出线程,并且不删除该数据(即不移动头指针,不释放节点)。 但示例中的代码可能不同,需要按照用户提供的代码框架来补全。 根据用户提供的代码框架,补全代码如下: 在循环中: 1.加锁。 2. 等待条件变量,直到链表不为空。 3. 获取头节点的数据。 4. 如果数据是-1:a.解锁。 b. break循环,退出线程。 5. 否则:a.打印数据。b. 移动头指针到下一个节点。 c. 释放原头节点。d. 解锁。 但这样处理的话,当某个线程处理到-1时,会解锁并退出,但该节点仍然在链表中。其他线程在获取锁后,同样会处理该节点,导致所有线程最终都会处理到该节点并退出。 然而,如果链表中有多个节点,例如头节点是正常数据,后面跟着-1,那么消费者线程可能会先处理前面的节点,直到处理到-1。但问题中的提示提到,当遇到-1时,停止消费,并且不删除该数据,其他消费者线程也会处理到该数据。因此,代码中的处理应该是:当发现数据是-1时,不移动头指针,不释放该节点,直接退出。 因此,在代码中:当检测到Data->number为-1时,执行以下步骤: -不要打印。- 不要修改beginData(即不执行beginData= beginData->next)。 -释放互斥锁。 - break循环,退出线程。其他情况下: -打印数据。- 移动beginData到下一个节点。 - 释放原头节点。因此,补全后的代码可能如下: ```cvoid *Consumer(void*arg) {while(1) {pthread_mutex_lock(&mutex);while (beginData == NULL){ pthread_cond_wait(&cond, &mutex); }struct Data *current = beginData; if (current->number ==-1) {pthread_mutex_unlock(&mutex); break; }printf("%d\n", current->number); beginData= current->next; pthread_mutex_unlock(&mutex);free(current);} pthread_exit(NULL); }``` 但这里有一个问题:当current->number是-1时,线程解锁后退出,但beginData仍然指向该节点。其他线程在获取锁后,同样会处理该节点,并退出。这样,每个消费者线程都会处理该节点一次,导致多次触发退出条件,但因为节点未被删除,所有线程最终都会退出。但是,当多个线程同时等待条件变量时,假设有一个线程处理了该节点(虽然不删除),那么其他线程被唤醒后,会再次处理同一个节点,导致每个线程都检测到-1,并退出。这正是问题所要求的行为。 因此,这样的代码是正确的。但是,在代码中,当current->number是-1时,线程解锁后退出,没有释放current节点,这会导致内存泄漏。但根据问题描述,遇到-1时不需要删除该数据,所以允许内存泄漏吗?或者可能生产者在其他地方释放? 这可能是一个问题,但根据题目要求,必须按照提示处理,即不删除该数据。因此,即使有内存泄漏,也需要按照题目要求处理。 综上,补全的Consumer函数应该如上述代码所示。</think>以下是补全后的消费者线程实现: ```c void *Consumer(void *arg) { while(1) { pthread_mutex_lock(&mutex); // 等待直到有数据可消费 while (beginData == NULL) { pthread_cond_wait(&cond, &mutex); } // 检查退出条件 if (beginData->number == -1) { pthread_mutex_unlock(&mutex); break; } // 消费数据 printf("%d\n", beginData->number); // 删除已消费节点 struct Data *temp = beginData; beginData = beginData->next; pthread_mutex_unlock(&mutex); free(temp); } // 唤醒其他消费者线程检查退出条件 pthread_cond_broadcast(&cond); pthread_exit(NULL); } ``` 代码解释: 1. 使用`pthread_mutex_lock`获取互斥锁保护共享数据 2. 通过`while (beginData == NULL)`处理虚假唤醒 3. 遇到-1时直接解锁并跳出循环 4. 正常数据执行打印和删除操作 5. 退出前广播条件变量唤醒所有等待线程 6. 使用`pthread_cond_broadcast`确保所有消费者都能检测到退出条件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值