Java 多线程 买票问题和生产者消费者问题

3 篇文章 1 订阅
1 篇文章 0 订阅

在共享资源的情况下,多个线程同时运行一段程序,若不加一些外部的约束,就会出现线程安全问题

以前在上操作系统这门课程时,就有讲过很多多线程相关的问题,这里我只提两个。

1买票 - 变量共享问题

比如一个简单的卖票程序,使用匿名函数实现Runnable接口从而实现资源共享

public class Ticket {
    int ticketCount = 10;
    public static void main(String[] args) {
        Runnable sellTicket = new Runnable(){
            
            @Override
            public void run(){
                while(ticketCount > 0){
                    try {
//让线程在--ticketCount操作前睡眠,才能更准确的体现ticketCount操作在多个线程并发访问的状态
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    --ticketCount;
                    System.out.println(Thread.currentThread().getName()+"卖了一张票,余票:"+ticketCount);
                }
                System.out.println("已售完");
            }
        };

        new Thread(sellTicket,"1号窗口").start();//开启一个售票窗口
        new Thread(sellTicket,"2号窗口").start();//开启一个售票窗口
        new Thread(sellTicket,"3号窗口").start();//开启一个售票窗口
    }
}

输出结果就可能是如下情况(注意只是可能,不是必然)

问题有两个

1.程序出现了重复

2.程序出现了不存在的-1(甚至-2)

现在我们挨个来分析原因

1.程序出现了重复。要分析这个原因需要先了解多线程的三大特征:原子性、可见性、有序性。这里程序出现ticket变量的重复的根本原因就在原子性。首先ticket是一个全局变量,我们通过局部赋值的方式实现的减一操作,而“--ticket”操作本身就是一个线程不安全的,参考下面引用的补充知识点。

补充知识点

当i为全局变量时,++i和i++都不是线程安全的,也就是不具有原子性,因为这个有两个步骤(i++时先赋值,再自加,++i是先自家再赋值),对于线程而言,同一进程的不同线程都可以访问到该变量,这样就会造成脏读。

当i为局部变量时(在方法中定义的),则是线程安全的,因为局部变量是线程私有的,一个线程访问时,另一个线程访问不到。

所以,我们就拿上图运行结果的第一个和第二个重复了9作为研究对象,我们把"--ticket"拆分为两部,也就是ticket-1(10-1)和ticket=9两部分,此时假设1号窗口拿到线程优先级,执行到ticket-1操作后,2号窗口也可以访问到ticket变量(所以全局变量线程不安全,因为多个线程可以同时访问),此时ticket只是执行了ticket-1,还未执行ticket=9,所以2号窗口访问到的就是全局变量ticket=10,然后1号窗口继续执行ticket=9,再打印ticket余票为9,而2号窗口执行到“--ticket”时(2号窗口此时获取的ticket是10),又会执行ticket-1(10-1)和ticket=9两个步骤,然后3号窗口并没有在ticket=9之前拿到全局ticket的值,所以2号窗口继续执行,最终打印ticket余票为9。

 

2.程序出现了不存在的-1、-2。这是因为我们为了降低线程安全问题的出现而添加了线程睡眠Thread.sleep(10)。看上图运行结果的最后三个线程的运行,当票只有1张时,3号窗口拿到了优先级,当3号窗口执行到“--ticket”之前,会执行Thread.sleep(10),进入睡眠,然后2号窗口拿到优先级,同样再执行到“--ticket”之前进入睡眠,然后1号窗口同样拿到优先级,进入睡眠,然后3号线程苏醒(苏醒也是随机的,也有可能3号窗口已经苏醒了2号窗口才拿到cpu资源-取决于睡眠时间,但由于都已经读取了全局变量ticket,所以并不影响后面问题的出现),执行--ticket,最后打印为0(1-1=0),然后2号窗口苏醒,执行--ticket,最后打印-1(0-1=-1),然后1号窗口苏醒,执行--ticket,最后打印-2(-1-1=-2)

 

知道了原因,再来讨论解决方案,其实有很多方案可以选择,比入给方法加上synchronized锁、给--操作的代码块加上synchronized锁、给代码块Lock锁等待,下面就举例用Lock锁来解决。

public class Ticket {
    static int ticketCount = 10;
    public static void main(String[] args) {
        Runnable sellTicket = new Runnable(){
            Lock lock = new ReentrantLock();//创建一个锁对象
            @Override
            public void run(){
                try{
                    lock.lock();//调用lock()方法开始将后面的代码锁住
                    while(ticketCount > 0){
                        --ticketCount;
                        System.out.println(Thread.currentThread().getName()+"卖了一张票,余票:"+ticketCount);
                    }
                    System.out.println("已售完"); 
                }finally {
                    lock.unlock();//unlock()释放锁
                }
                
            }
        };

        new Thread(sellTicket,"1号窗口").start();//开启一个售票窗口
        new Thread(sellTicket,"2号窗口").start();//开启一个售票窗口
        new Thread(sellTicket,"3号窗口").start();//开启一个售票窗口
    }
}

输出结果如下图

其实真实的业务场景种,这里最好只对--ticketCount部分加锁即可,我是为了保证输出信息的顺序才对整个代码块加锁。

 

2生产者-消费者问题

问题描述:生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

--引用自百度百科

其实这和上面的买票问题有些类似,都涉及到了读取共享变量,但区别在于买票问题要解决的是如何让变量被访问时能够保证线程安全,这个问题要解决的是如何分配线程的访问才能保证缓冲区不会出现异常。常用的解决方案就是使用Object类的wait()方法和notify()方法来实现线程的通信。

比如缓冲区在没有数据时,就让消费者wait()进入等待状态,当生产者在缓冲区创建了一个数据后,就通过唤醒消费者的方式告知消费者可以消费了。

代码实现如下:

public class ProducerConsumer {
    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();
        //生产者和消费者各模拟10个
        for (int i = 0; i < 10; i++) {
            new Thread(i+"号生产者"){
                @Override
                public void run(){
                    pc.produce(getName());
                }
            }.start();

            new Thread(i+"号消费者"){
                @Override
                public void run(){
                    pc.consume(getName());
                }
            }.start();
        }
    }

    public int buffer = 0;//缓冲区容量初始值
    public int MAX = 2;//缓冲区最大容量,设置小一点,冲突会更明显

    //生产
    synchronized void produce(String name){
        System.out.print(name+":");
        try {
            if(buffer < MAX){
                buffer++;//模拟向缓冲区生产数据
                System.out.print("生产产品1个 \t");
                this.notifyAll();//生产一个就唤醒所有等待的,让其重新竞争
                System.out.println("当前产品总数为:"+buffer);
            }else{
                //缓冲区已满,生产者就不能生产了
                System.out.println("缓冲区已满,无法生产产品,进入等待状态。。。 ");
                this.wait();//让当前的线程进入等待,释放锁
                //当消费者消费一个后,生产者被唤醒,然后再次尝试进行生产
                produce(name);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //消费
    synchronized void consume(String name) {
        System.out.print(name+":");
        try{
            if(buffer > 0){
                buffer--;//模拟从缓冲区消费数据
                System.out.print("消费产品1个 \t");
                this.notifyAll();//同上
                System.out.println("当前产品总数为:"+buffer);
            }else{
                //缓冲区为空,消费者就不能消费了
                System.out.println("产品为空,暂时无法消费,进入等待状态。。。");
                this.wait();//让当前的线程进入等待,释放锁
                //当生产者生产一个后,消费者被唤醒,然后再次递归尝试进行消费
                consume(name);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

我运行了好几次,才找到一个比较好的例子,程序运行结果截图如下:

 

 注意观察,7、8号消费者尝试消费未果,当7号生产者生产了1个产品后,唤醒所有wait的线程,然后7号消费者抢到了CPU资源,7号消费后,8号消费者也抢到了CPU资源,但很不幸,产品又没了,所以8号继续wait。当9号和3号分别生产力1个产品后,8号消费者这次才抢到CPU资源,然后进行消费。

 

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以使用Java中的BlockingQueue来实现消费者生产者问题。BlockingQueue是一个线程安全的队列,它提供了put()和take()方法,分别用于生产和消费。当队列为空时,take()方法会阻塞线程,直到队列中有元素;当队列满时,put()方法会阻塞线程,直到队列中有空闲位置。 以下是一个简单的示例代码: import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class ProducerConsumerExample { private static final int QUEUE_CAPACITY = 10; private static final int NUM_PRODUCERS = 2; private static final int NUM_CONSUMERS = 3; public static void main(String[] args) { BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY); for (int i = 0; i < NUM_PRODUCERS; i++) { new Thread(new Producer(queue)).start(); } for (int i = 0; i < NUM_CONSUMERS; i++) { new Thread(new Consumer(queue)).start(); } } static class Producer implements Runnable { private final BlockingQueue<Integer> queue; public Producer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { try { for (int i = 0; i < 10; i++) { queue.put(i); System.out.println("Produced: " + i); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } } static class Consumer implements Runnable { private final BlockingQueue<Integer> queue; public Consumer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { try { while (true) { Integer value = queue.take(); System.out.println("Consumed: " + value); Thread.sleep(2000); } } catch (InterruptedException e) { e.printStackTrace(); } } } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值