Java线程(二):生产者/消费者问题

2021/7/22 1:06:24

本文主要是介绍Java线程(二):生产者/消费者问题,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1、生产者/消费者

还需要线程与线程协作(通信),生产者/消费者问题是一个经典的线程同步以及通信的案例。

该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”和“消费者”在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者,通常采用线程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。

2、问题描述

        假设有这样一种情况,有一个盘子,盘子里只能放一个鸡蛋,A线程专门往盘子里放鸡蛋,如果盘子里有鸡蛋,则一直等到盘子里没鸡蛋,B线程专门从盘子里取鸡蛋,如果盘子里没鸡蛋,则一直等到盘子里有鸡蛋。这里盘子是一个互斥区,每次放鸡蛋是互斥的,每次取鸡蛋也是互斥的,A线程放鸡蛋,如果这时B线程要取鸡蛋,由于A没有释放锁,B线程处于等待状态,进入阻塞队列,放鸡蛋之后,要通知B线程取鸡蛋,B线程进入就绪队列,反过来,B线程取鸡蛋,如果A线程要放鸡蛋,由于B线程没有释放锁,A线程处于等待状态,进入阻塞队列,取鸡蛋之后,要通知A线程放鸡蛋,A线程进入就绪队列。我们希望当盘子里有鸡蛋时,A线程阻塞,B线程就绪,盘子里没鸡蛋时,A线程就绪,B线程阻塞,代码如下:

import java.util.ArrayList;
import java.util.List;
/** 定义一个盘子类,可以放鸡蛋和取鸡蛋 */
public class Plate {
    /** 装鸡蛋的盘子 */
    List<Object> eggs = new ArrayList<Object>();
    /** 取鸡蛋 */
    public synchronized Object getEgg() {
        while (eggs.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Object egg = eggs.get(0);
        eggs.clear();// 清空盘子
        notifyAll();// 唤醒阻塞队列的某线程到就绪队列
        System.out.println("拿到鸡蛋");
        return egg;
    }
    /** 放鸡蛋 */
    public synchronized void putEgg(Object egg) {
        while (eggs.size() > 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        eggs.add(egg);// 往盘子里放鸡蛋
        notifyAll();// 唤醒阻塞队列的某线程到就绪队列
        System.out.println("放入鸡蛋");
    }
    static class AddThread implements Runnable  {
        private Plate plate;
        private Object egg = new Object();
        public AddThread(Plate plate) {
            this.plate = plate;
        }
        public void run() {
            plate.putEgg(egg);
        }
    }
    static class GetThread implements Runnable  {
        private Plate plate;
        public GetThread(Plate plate) {
            this.plate = plate;
        }
        public void run() {
            plate.getEgg();
        }
    }
    public static void main(String args[]) {
        Plate plate = new Plate();
        for(int i = 0; i < 10; i++) {
            new Thread(new AddThread(plate)).start();
            new Thread(new GetThread(plate)).start();
        }
    }
}
        输出结果:

放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋

 程序开始,A线程判断盘子是否为空,放入一个鸡蛋,并且唤醒在阻塞队列的一个线程,阻塞队列为空;假设CPU又调度了一个A线程,盘子非空,执行等待,这个A线程进入阻塞队列;然后一个B线程执行,盘子非空,取走鸡蛋,并唤醒阻塞队列的A线程,A线程进入就绪队列,此时就绪队列就一个A线程,马上执行,放入鸡蛋;如果再来A线程重复第一步,在来B线程重复第二步,整个过程就是生产者(A线程)生产鸡蛋,消费者(B线程)消费鸡蛋。

3、面试

题目:子线程循环10次,主线程循环100次,如此循环100次

public class ThreadTest2 {
    public static void main(String[] args) {
        final Business business = new Business();
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadExecute(business, "sub");
            }
        }).start();
        threadExecute(business, "main");
    }    
    public static void threadExecute(Business business, String threadType) {
        for(int i = 0; i < 100; i++) {
            try {
                if("main".equals(threadType)) {
                    business.main(i);
                } else {
                    business.sub(i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Business {
    private boolean bool = true;
    public synchronized void main(int loop) throws InterruptedException {
        while(bool) {
            this.wait();
        }
        for(int i = 0; i < 100; i++) {
            System.out.println("main thread seq of " + i + ", loop of " + loop);
        }
        bool = true;
        this.notify();
    }    
    public synchronized void sub(int loop) throws InterruptedException {
        while(!bool) {
            this.wait();
        }
        for(int i = 0; i < 10; i++) {
            System.out.println("sub thread seq of " + i + ", loop of " + loop);
        }
        bool = false;
        this.notify();
    }
}

大家注意到没有,在调用wait方法时,都是用while判断条件的,而不是if,在wait方法说明中,也推荐使用while,因为在某些特定的情况下,线程有可能被假唤醒,使用while会循环检测更稳妥。wait和notify方法必须工作于synchronized内部,且这两个方法只能由锁对象来调用。

4、一个类中的两个方法都加了同步锁,多个线程能同时访问这个类的两个方法吗?

首先,同步锁有两种,JVM的synchronized和JDK的ReentrantLock;
然后,多个线程访问这个类的两个方法也有不同的形式,例如访问这个类的两个方法是通过同一个类的实例对象来访问还是通过不同的类的实例对象访问;
再者,一个类的两个方法加了同步锁,这两个被同步方法也没有说明是什么样的方法。他可能是类的普通实例方法,也可能是类中Runnable对象的run方法。

1.synchronized

1.多个线程同时访问 同一个类实例对象 的两个同步方法:

public class Example1 {
    private int num = 0 ;
    public synchronized void method1() {
        System.out.println("method1进入");
        for(int i = 0 ; i<10 ; i++) {
            System.out.print("同步方法1:"+num+"--");
            num++ ;
        }
        System.out.println("同步方法1结束");
    }
    
    public synchronized void method2() {
        System.out.println("method2进入:");
        for(int i = 0 ; i<10 ; i++) {
            System.out.print("method2:"+num+"--");
            num++ ;
        }
        System.out.println("method2结束");
    }
    
    public static void main(String[] args) {
        final Example1 example1 = new Example1() ;
        
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                example1.method1();
            }
        }) ;
        
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                example1.method2();
            }
        }) ;           
            thread1.start();
            thread2.start();     
    }
    
}

method1进入
同步方法1:0--同步方法1:1--同步方法1:2--同步方法1:3--同步方法1:4--同步方法1:5--同步方法1:6--同步方法1:7--同步方法1:8--同步方法1:9--method1结束
method2进入:
method2:10--method2:11--method2:12--method2:13--method2:14--method2:15--method2:16--method2:17--method2:18--method2:19--method2结束

显然此时多个线程是不能访问同个类(的一个实例对象)的两个同步方法的。因为获取的是同一个实例对象锁,同一时刻只有一个线程可以访问该对象内的一个同步方法。

2.多个线程同时访问 同一个类的不同实例对象的 两个同步方法

将上面的代码稍作修改,主函数中多new一个该类实例

final Example1 example2 = new Example1() ;
再修改thread2的run方法调用的类实例为example2

Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                example2.method2();
            }
        }) ;


method1进入
method2进入:
method2:0--method2:1--同步方法1:0--同步方法1:1--同步方法1:2--method2:2--同步方法1:3--method2:3--同步方法1:4--method2:4--同步方法1:5--method2:5--同步方法1:6--同步方法1:7--method2:6--同步方法1:8--同步方法1:9--method2:7--同步方法1结束
method2:8--method2:9--method2结束


这时候显然,多个线程是能访问同个类(的不同实例对象)的两个同步方法的。

小结:这是因为synchronized是对象锁,即线程获得的锁是施加在一个实例对象上的,如果不同的线程访问的是同一对象上的不同的同步方法,那么显然不能同时进行。
如果是不同对象上的不同的同步方法,那么就是可以同时进行的。

3.多个线程同时访问 同一个类实例对象的两个Runnable对象 的run方法:

public class Example2 {
    private int num ;
    public Runnable runnable1 = new Runnable() {
        @Override
        public void run() {
            //同步锁
            synchronized (this) {
                System.out.println("线程1进入");
                for(int i = 0 ; i < 10 ; i ++) {
                    System.out.print("线程1:"+num+"--");
                }
                System.out.println("线程1结束");
            }
        }
    };
    
    public Runnable runnable2 = new Runnable() {
        @Override
        public void run() {
            //同步锁
            synchronized (this) {
                System.out.println("thread2进入");
                for(int i = 0 ; i < 10 ; i ++) {
                    System.out.print("thread2:"+num+"--");
                }
                System.out.println("thread2结束");
            }
        }
    };
    
    public static void main(String[] args) {
        Example2 example = new Example2() ; //创建一个对象
        new Thread(example.runnable1).start(); //同步方法1
        new Thread(example.runnable2).start(); //同步方法2
    }
}
thread2进入
线程1进入
thread2:0--线程1:0--线程1:0--thread2:0--线程1:0--线程1:0--线程1:0--thread2:0--线程1:0--thread2:0--thread2:0--线程1:0--thread2:0--线程1:0--thread2:0--thread2:0--线程1:0--thread2:0--线程1:0--thread2:0--线程1结束
thread2结束

可见此时多个线程是能同时访问同个类的两个同步方法的。这是因为
synchronized(this){ //... }中锁住的不是代码块,即这个锁在run方法中,但是并不是同步了这个run方法,而是括号中的对象this,也就是说,多个线程会拿到各自的锁,就能够同时执行run方法。(在run方法前声明synchronized也是同样的效果)

new Thread(example.runnable1).start(); //同步方法1
new Thread(example.runnable2).start(); //同步方法2

打印出这个this对象,是两个不同的类实例对象:
synchronizedTest.Example2$1@65db6dfa
synchronizedTest.Example2$2@471fab


也说明了不同线程的实例对象不同,都是各自对象的锁,不可以认为是类似于例子1中的同一实例对象,而应该类似与例子2的不同类的实例对象

总结:分析synchronized同步锁的核心在于他是个对象锁,找清楚锁的对象

5、ReentrantLock锁

1.多个线程同时访问同一个类实例对象的两个同步方法:

public class LockExample {
    private int num;
    private Lock lock = new ReentrantLock();
    public void method1() {
            lock.lock();
            System.out.println("同步方法1进入");
            for(int i = 0 ; i<10 ; i++) {
                System.out.print("同步方法1:"+num+"--");
                num++ ;
            }
            System.out.println("同步方法1结束");
            lock.unlock();
    }

    public void method2() {
        lock.lock();
        System.out.println("method2进入:");
        for (int i = 0; i < 10; i++) {
            System.out.print("method2:" + num + "--");
            num++;
        }
        System.out.println("method2结束");
        lock.unlock();
    }

    public static void main(String[] args) {
        final LockExample example = new LockExample() ;
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.method1();
            }
        }) ;
        
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                example.method2();
            }
        }) ;
            thread1.start();
            thread2.start();

    }
}

同步方法1进入
同步方法1:0--同步方法1:1--同步方法1:2--同步方法1:3--同步方法1:4--同步方法1:5--同步方法1:6--同步方法1:7--同步方法1:8--同步方法1:9--同步方法1结束
method2进入:
method2:10--method2:11--method2:12--method2:13--method2:14--method2:15--method2:16--method2:17--method2:18--method2:19--method2结束

可见此时多个线程是不能访问同个类(的一个实例对象)的两个同步方法的

2.多个线程同时访问同一个类的不同实例对象的两个同步方法:
修改main函数的即可:

public static void main(String[] args) {
        final LockExample example1 = new LockExample() ;//两个实例
        final LockExample example2 = new LockExample() ;
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                example1.method1(); //实例1的同步方法1
            }
        }) ;
        
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                example2.method2();//实例2的同步方法2
            }
        }) ;

            thread1.start();
            thread2.start();
    }

同步方法1进入
method2进入:
同步方法1:0--method2:0--method2:1--同步方法1:1--method2:2--同步方法1:2--同步方法1:3--method2:3--同步方法1:4--method2:4--同步方法1:5--同步方法1:6--method2:5--同步方法1:7--method2:6--同步方法1:8--同步方法1:9--同步方法1结束
method2:7--method2:8--method2:9--method2结束


可见,多个线程是能访问同个类(的不同实例对象)的两个同步方法的。

总结:ReentrantLock和synchronized的前两个例子结论都相同

3.多个线程同时访问同一个类实例对象的两个Runnable对象的run方法:

public class Lockexample2 {

    private int num;
    private Lock lock = new ReentrantLock();
    
    public Runnable runnable1 = new Runnable() {
        @Override
        public void run() {
            lock.lock();//上锁
            System.out.println("线程1进入");
            for(int i = 0 ; i < 10 ; i ++) {
                System.out.print("线程1:"+num+"--");                
            }
            System.out.println("线程1结束");
            lock.unlock();
        }
    };
    
    public Runnable runnable2 = new Runnable() {
        @Override
        public void run() {
            lock.lock();//上锁
            System.out.println("thread2进入");
            for(int i = 0 ; i < 10 ; i ++) {
                System.out.print("thread2:"+num+"--");
            }
            System.out.println("thread2结束");
            lock.unlock();
        }
    };
    
    public static void main(String[] args) {
        Lockexample2 example = new Lockexample2();
        new Thread(example.runnable1).start(); 
        new Thread(example.runnable2).start(); 
        
    }
}

线程1进入
线程1:0--线程1:0--线程1:0--线程1:0--线程1:0--线程1:0--线程1:0--线程1:0--线程1:0--线程1:0--线程1结束
thread2进入
thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2结束

这里可以看到,与synchronized的第三个例子出现了不同的结果。在这个地方,ReentrantLock不允许多线程同时访问一个类的不同同步方法。
这里要注意的是ReentrantLock与synchronized不同,ReentrantLock的实现方式是要先创建ReentrantLock对象,然后用这个对象的方法来上锁。而一个类的实例中只有一个ReentrantLock对象:

private Lock lock = new ReentrantLock();
而本例中,线程的创建是建立在同一个类实例上的:

Lockexample2 example = new Lockexample2();
new Thread(example.runnable1).start(); 
new Thread(example.runnable2).start(); 
因此,ReentrantLock对象lock是同一个,因此第一个线程进入同步方法1后就获取了锁,第二个线程无法获取这个锁,只能等待。

如果换成是两个实例对象:

public static void main(String[] args) {
        Lockexample2 example = new Lockexample2();
        Lockexample2 example2 = new Lockexample2();
        new Thread(example.runnable1).start(); 
        new Thread(example2.runnable2).start(); 
        
    }

线程1进入
thread2进入
线程1:0--线程1:0--线程1:0--线程1:0--thread2:0--线程1:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--thread2:0--线程1:0--thread2:0--线程1:0--线程1:0--thread2:0--线程1:0--thread2结束
线程1:0--线程1结束


可见不同的实例对象中是不同的ReentrantLock对象,因此可以同时访问

小结:ReentrantLock锁的核心在与ReentrantLock对象是不是同一个

6、结论

一个类中的两个方法都加了同步锁,多个线程能同时访问这个类的两个方法吗?

分成synchronized和ReentrantLock两个情况:

一.对于synchronized
1.一个类中的两个方法都加了同步锁,多个线程不能同时访问这个类的同一实例对象的两个方法
2.一个类中的两个方法都加了同步锁,多个线程能同时访问这个类的不同实例对象的两个方法
3.一个类中的两个方法**(Runnable的run方法)都加了同步锁,多个线程能**同时访问这个类的两个方法(不论是不是同一实例对象)

二.对于ReentrantLock
1.一个类中的两个方法都加了同步锁,多个线程不能同时访问这个类的同一实例对象的两个方法(不论同步加在实例方法中或是run方法中)
2.一个类中的两个方法都加了同步锁,多个线程能同时访问这个类的不同实例对象的两个方法(不论同步加在实例方法中或是run方法中)

参考:https://blog.csdn.net/ghsau/article/details/7433673

https://blog.csdn.net/weixin_40616523/article/details/87883267



这篇关于Java线程(二):生产者/消费者问题的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程