欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

day20Java-常用对象Thread-多线程中生产者和消费者

程序员文章站 2022-05-09 13:25:18
...

多线程生产者和消费者

前面已经写过电影票程序,不是特别的符合实际情况。

多线程生产者和消费者:
就是不同种类的线程对同一个资源的操作。

自己对Java等待唤醒机制总结:
之前买电影票的线程只有一个卖票线程,创建多个线程对象去执行它。出现了线程安全问题可以使用同步来解决。

但是现在是不同种类的线程对同一个资源的操作。也可以使用同步来解决线程安全问题,线程安全问题解决了,但是打印的结果不是很符合实际情况,如果让不同种类的线程更符合实际情况,可以使用Java的等待唤醒机制。

线程通信问题:
		不同种类的线程间针对同一个资源的操作。

day20Java-常用对象Thread-多线程中生产者和消费者

多线程生产者消费者代码版本1

代码演示
生产者消费者测试

public class StudnetThreadDemo {
    public static void main(String[] args) {
        //创建一个生产者消费者共享的对象
        Student s = new Student();
        
        //创建生产者线程对象
        SetThread st = new SetThread(s);
        //创建消费者线程对象
        GetThread gt = new GetThread(s);

        //启动线程
        st.start();
        gt.start();
    }
}

生产者

public class SetThread extends Thread {
    private Student s;

    public SetThread(Student s) {
        this.s = s;
    }
    @Override
    public void run() {
        int count = 0;
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count % 2 == 0) {
                //Student s = new Student();
                s.name = "格雷福斯";
                s.age = 60;
            } else {
                s.name = "至高之拳";
                s.age = 30;
            }
            count++;
        }
    }
}

消费者

public class GetThread extends Thread {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        //这里不能这样创建对象,那样就是各使用各的。
        //Student s = new Student();
        while (true) {
            /*try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            System.out.println(s.name + "-" + s.age);
        }
    }
}

实体类

public class Student {
    public String name;
    public int age;
}

结果:我只列出来了有问题的

斯-60
格雷福斯-3
至高之拳-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60

多线程生产者消费者代码版本2-同步解决问题

分析:
		资源类:Student	
		设置学生数据:SetThread(生产者)
		获取学生数据:GetThread(消费者)
		测试类:StudnetThreadDemo 

问题1:按照思路写代码,发现数据每次都是:null---0
原因:我们在每个线程中都创建了新的资源,而我们要求的时候设置和获取线程的资源应该是同一个
如何实现呢?
		在外界把这个数据创建出来,通过构造方法传递给其他的类。

问题2:为了效果更好一些,我加入的循环和判断,给出不同的值,但是出现了新的问题。
	A:相同的数据出现了多次
			原因:CPU一点时间片执行权,足够线程执行很多次了。
	B:姓名和年龄不匹配
			原因:因为多线程运行的随机性导致的

线程安全问题?
			A:是否是多线程环境?		是
			B:是否有共享数据? 	是 	Student
			C:是否有多条语句操作共享数据? 是	

解决方案:
 		加锁。

注意:
  			A:不同种类的线程都要加锁。
  				如果只有生产者一种类型线程加锁,还是会出现上面一样的问题。
  			B:不同种类的线程加的锁必须是同一把。
  				如果不同类型线程锁不同,各自锁各自的,也会出现上面一样的问题,那毫无意义。
加锁后:
	实现生产者在生产的时候,消费者不能消费不能消费(生产者那边已经把锁锁住了),消费者在消费的时候,生产者那边不能生产(消费者那边已经把锁锁住了)
	加锁后也就是把年龄和姓名不匹配的问题解决了,但是没有解决同一个数据出现多次。
	数据安全了。

代码演示
生产者消费者测试

public class StudnetThreadDemo {
    public static void main(String[] args) {
        //创建一个生产者消费者共享的对象
        Student s = new Student();

        //创建生产者线程对象
        SetThread st = new SetThread(s);
        //创建消费者线程对象
        GetThread gt = new GetThread(s);

        //启动线程
        st.start();
        gt.start();
    }
}

生产者

public class SetThread extends Thread {
    private Student s;

    public SetThread(Student s) {
        this.s = s;
    }
	int count = 0;
    @Override
    public void run() {
       
        while (true) {
            synchronized (s) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (count % 2 == 0) {
                    //Student s = new Student();
                    s.name = "格雷福斯"; //不加同步刚执行到这里,就被其他线程抢到执行权了,上面一次内存中的值可能是name=至高之
                    //拳,age=30,如果这时候输出值也就name="格雷福斯",age=30。 					 		    
                    s.age = 60;
                } else {
                    s.name = "至高之拳";//不加同步刚执行到这里,就被其他线程抢到执行权了,上一次内存中的值可能是name=格雷福斯,age=60
                    //如果这时候输出值也就是name=至高之拳,age=60;
                    s.age = 30;
                }
                count++;
            }
        }
    }
}

消费者

public class GetThread extends Thread {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        //这里不能这样创建对象,那样就是各使用各的。
        //Student s = new Student();
        while (true) {
            synchronized (s) {
            /*try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
                System.out.println(s.name + "-" + s.age);
            }
        }
    }
}

实体类

public class Student {
    public String name;
    public int age;
}

结果:

格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
格雷福斯-60
至高之拳-30
至高之拳-30
至高之拳-30
至高之拳-30
至高之拳-30
至高之拳-30

多线程生产者消费者代码版本3-等待唤醒机制解决问题

上面其实已经解决数据安全问题了,但是呢?数据输出相同的数据都是打印一大片。觉得不是很好看,想依次打印出来。
同步能解决线程安全问题。但是解决不了那种很理性的输出数据。

如何实现呢?
为了满足这样需求Java就提供了等待唤醒机制解决。
等待和唤醒:
		Object中提供了3个方法:	
					wait():等待。
					notify:唤醒单个线程。
					notify:唤醒所有线程。

为什么这些方法不定义在Thread类中呢?
 			这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。
  			所以,这些方法必须定义在Object类中。

等待唤醒机制思路图解
day20Java-常用对象Thread-多线程中生产者和消费者

代码演示
生产者消费者测试

public class StudnetThreadDemo {
    public static void main(String[] args) {
        //创建一个生产者消费者共享的对象
        Student s = new Student();

        //创建生产者线程对象
        SetThread st = new SetThread(s);
        //创建消费者线程对象
        GetThread gt = new GetThread(s);

        //启动线程
        st.start();
        gt.start();
    }
}

生产者

public class SetThread extends Thread {
    private Student s;

    public SetThread(Student s) {
        this.s = s;
    }
	int count = 0;
    @Override
    @SuppressWarnings("all")
    //生产者
    public void run() {   
        while (true) {
            synchronized (s) {
                //如果有数据就等待,没有数据就创建
                if(s.flag){
                    try {
                        s.wait(); //满足判断条件st等待,立即释放锁。st等待的情况下,只能gt抢到执行权。
                        //注意:醒来是从这里醒来的。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (count % 2 == 0) {
                    //Student s = new Student();
                    s.name = "格雷福斯";
                    s.age = 60;
                } else {
                    s.name = "至高之拳";
                    s.age = 30;
                }
                count++;
                //到这里说明数据已经生产好了,修改标记
                s.flag =true;
                //唤醒线程
                s.notify(); //唤醒线程,唤醒线程后不会立马执行。在抢CPU执行权。

                //st抢到或者gt抢到
                //情况1:如果是st抢到cpu执行权,刚才已经生产好数据了,那就要等待了。

                //情况2:gt抢到cpu执行权那就从消费者的wait()中醒来,继续向下执行。
            }
        }
    }
}

消费者

public class GetThread extends Thread {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }
    //消费者
    @Override
    public void run() {
        //这里不能这样创建对象,那样就是各使用各的。
        //Student s = new Student();
        while (true) {
            synchronized (s) {
                //如果有数据,就消费,没有就等待。
                if(!s.flag){
                    try {
                        s.wait();//满足判断条件gt等待,并立即释放锁。gt等待的情况下,只能st抢到执行权。
                        //注意:醒来是从这里醒来的。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(s.name + "-" + s.age);
                //到这里说明已经消费完,修改标记
                s.flag = false;
                //唤醒线程
                s.notify();//唤醒线程,唤醒线程后不会立马执行。在抢CPU执行权。

                //st抢到或者gt抢到
                //情况1:如果gt抢到了cpu的执行权,数据已经消费完了,那就要等待了。

                //情况2:st抢到了cpu执行权那就从生产者wait()中醒来,继续向下执行。
            }
        }
    }
}

实体类

public class Student {
    public String name;
    public int age;
    public boolean flag; //定义一个标记,是否有学生,默认是false.没有数据
}

结果:

格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30

线程状态转换图
day20Java-常用对象Thread-多线程中生产者和消费者

多线程生产者消费者代码版本4-代码优化

代码演示
生产者消费者测试

public class StudentDemo {
    public static void main(String[] args) {
        //创建学生对象
        Student s =  new Student();
        //创建线程对象
        SetThread st = new SetThread(s);
        GetThread gt=  new GetThread(s);

        //启动线程
        st.start();
        gt.start();
    }
}

生产者

public class SetThread extends Thread {
    private Student s;

    public SetThread(Student s) {
        this.s = s;
    }

    int count = 0;

    @Override
    public void run() {
        while (true) {
            if (count % 2 == 0) {
                s.setNameAge("格雷福斯", 60);
            } else {
                s.setNameAge("至高之拳", 30);
            }
            count++;
        }
    }
}

消费者

public class GetThread extends Thread {
    private Student s;

    public GetThread(Student s){
        this.s =s;
    }

    @Override
    public void run() {
        while (true) {
            s.getNameAge();
        }
    }
}

实体类

public class Student {
    //实际开发中成员变量肯定是不能被外界访问的
    private String name;
    private int age;
    private boolean flag;

    public synchronized void setNameAge(String name, int age) {
        //如果数据存在就等待
        if (this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name = name;
        this.age = age;
        //修改标记
        this.flag = true;
        //唤醒线程
        this.notify();
    }

    public synchronized void getNameAge() {
        //如果没有数据就等待
        if (!this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this.name + "-" + this.age);
        //修改标记
        this.flag = false;
        //唤醒线程
        this.notify();
    }
}

结果:

格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
至高之拳-30
格雷福斯-60
相关标签: Java相关知识