Java-并发编程-01并发基础概念

一、并发编程基础认识

1.了解多线程

是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。

2.并发和并行的区别

并行:在同一时刻,有多个指令在多个CPU上同时执行。

并发:在同一时刻,有多个指令在单个CPU上交替执行。

简单的效果图示:

3.进程和线程

进程:

是执行中一段程序,一个程序被载入到内存中并准备执行,它就是一个进程,是系统进行资源分配和调度的一个基本单位。
特点:
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行

线程:

是进程的一个实体,是cpu 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,有时被称为轻量级进程。线程是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序

二、多线程实现

1.继承Thread类

点击查看代码
package com.vayne.thread;

// 定义一个继承Thread类的类ThreadBean
class ThreadBean extends Thread{
    @Override
    public void run() {
        // 在run()方法中编写需要执行的任务逻辑
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
            try {
                // 由于程序执行过快,无法从结果看出并发效果,加入线程休眠
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        // 创建ThreadBean对象
        ThreadBean threadBean1 = new ThreadBean();
        // 启动线程
        threadBean1.start();
        
        // 创建另一个ThreadBean对象
        ThreadBean threadBean2 = new ThreadBean();
        // 启动另一个线程
        threadBean2.start();
    }
}

2.实现Runnable接口

点击查看代码
package com.vayne.thread;

// 定义一个实现Runnable接口的类RunnableBean
class RunnableBean implements Runnable{
    
    @Override
    public void run() {
        // 在run()方法中编写需要执行的任务逻辑
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
            try {
                // 由于程序执行过快,无法从结果看出并发效果,加入线程休眠
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadRunnable {
    public static void main(String[] args) {
        // 创建两个RunnableBean对象
        RunnableBean runnableBean1 = new RunnableBean();
        RunnableBean runnableBean2 = new RunnableBean();
        
        // 创建两个Thread对象,将RunnableBean对象作为构造方法的参数,并指定线程名称
        Thread thread1 = new Thread(runnableBean1, "线程一");
        Thread thread2 = new Thread(runnableBean2, "线程二");
        
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

3.实现Callable接口

点击查看代码
/**
 * 定义一个实现Callable接口的类,用于创建可调用的任务
 */
class CallableBean implements Callable<String>{
    @Override
    public String call() throws Exception {
        // 在call()方法中编写需要执行的任务逻辑
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
            Thread.sleep(100);
        }
	//获取当前线程名称=Thread.currentThread().getName()
        return Thread.currentThread().getName()+"线程执行完毕";
    }
}

public class ThreadCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建CallableBean实例
        CallableBean callableBean = new CallableBean();

        // 创建FutureTask,将callableBean作为参数传入
        FutureTask<String> stringFutureTask = new FutureTask<>(callableBean);

        // 创建线程,并将FutureTask作为任务传入
        Thread thread = new Thread(stringFutureTask);

        // 启动线程
        thread.start();

        // 获取任务执行结果
        String result = stringFutureTask.get();

        // 打印结果
        System.out.println(result);
    }
}

4.三种实现的区别

实现Runnable、Callable接口:

好处: 扩展性强,实现该接口的同时还可以继承其他的类
缺点: 编程相对复杂,不能直接使用Thread类中的方法

继承Thread类:

好处: 编程比较简单,可以直接使用Thread类中的方法
缺点: 可以扩展性较差,不能再继承其他的类

5.线程优先级

线程调度

两种调度方式:

分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些

Java使用的是抢占式调度模型

假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

优先级相关方法

final int getPriority()-->返回此线程的优先级

final void setPriority(int newPriority)-->更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10

点击查看代码
/**
 * 定义一个实现Callable接口的类,用于创建可调用的任务
 */
class CallableBean implements Callable<String>{
    @Override
    public String call() throws Exception {
        // 在call()方法中编写需要执行的任务逻辑
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"正在执行第"+i+"次");
        }
        return Thread.currentThread().getName()+"线程执行完毕";
    }
}

public class ThreadCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建CallableBean实例
        CallableBean callableBean = new CallableBean();
        CallableBean callableBean2 = new CallableBean();


        // 创建FutureTask,将callableBean作为参数传入
        FutureTask<String> stringFutureTask1 = new FutureTask<>(callableBean);
        FutureTask<String> stringFutureTask2 = new FutureTask<>(callableBean2);

        // 创建线程,并将FutureTask作为任务传入
        Thread thread1 = new Thread(stringFutureTask1);
        Thread thread2 = new Thread(stringFutureTask2);

        // 设置线程名称
        thread1.setName("线程一");
        thread2.setName("线程二");

        //设置线程优先级
        thread1.setPriority(1);
        thread2.setPriority(9);


        // 启动线程
        thread1.start();
        thread2.start();

        // 获取任务执行结果
        String result1 = stringFutureTask1.get();
        String result2 = stringFutureTask2.get();

        // 打印结果
        System.out.println(result1);
        System.out.println(result2);
    }
}

6.守护线程

所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程。 比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。 因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。

void setDaemon(boolean on)-->将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

点击查看代码
package com.vayne.thread;

/**
 * 定义一个继承Thread类的线程类Thread1
 */
class Thread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 打印当前线程名称和计数值
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

/**
 * 定义一个继承Thread类的线程类Thread2
 */
class Thread2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 打印当前线程名称和计数值
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

public class DaemonDemo {
    public static void main(String[] args) {
        // 创建Thread1和Thread2的实例
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        // 设置线程名称
        thread1.setName("舔dog");
        thread2.setName("女god");

        // 将thread1设置为守护线程
        thread1.setDaemon(true);

        // 设置线程优先级
        thread1.setPriority(1);
        thread2.setPriority(9);
        
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

三、并发编程中的线程同步

当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),可能会导致数据相互之间产生冲突,因此需要使用线程同步来保证资源变量的唯一性和准确性。 总的来说,是为了多个线程访问同一数据时,会出现数据的安全性问题,保证数据安全,所以需要同步。

1.经典场景一之卖票

目前正在售卖Jay Chou演唱会门票,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该演唱会门票

点击查看代码
package com.vayne.thread;

class TicketSell  implements Runnable{
    private  int ticket = 100;

    @Override
    public void run() {
        while (true){
            if (ticket<=0){
                break;
            }else {
                try {
                    //模拟售票业务
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket--;
            }
            System.out.println(Thread.currentThread().getName()+"剩下"+ticket+"张票");
        }
    }
}
public class TicketDemo{
    public static void main(String[] args) {
        TicketSell ticketSell = new TicketSell();

        Thread thread1 = new Thread(ticketSell,"窗口一");
        Thread thread2 = new Thread(ticketSell,"窗口二");
        Thread thread3 = new Thread(ticketSell,"窗口三");

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

查看打印结果:
image
以及
image

问题:
1.相同的票出现了多次售卖情况
2.票数出现负数
原因:
线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题

2.同步代码块解决数据安全问题

安全问题出现的条件
1.多线程环境
2.存在共享数据
3.有多条语句操作共享数据

基本思想
让一条业务执行时处于没有安全问题的环境

如何实现?
把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可,对此Java提供了同步代码块的方式来解决

格式:
synchronized(任意对象) { 多条语句操作共享数据的代码 }

同步的好处和弊端:
好处:解决了多线程的数据安全问题
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

点击查看代码
package com.vayne.thread;

import java.sql.PseudoColumnUsage;

/**
 * 定义一个实现Runnable接口的售票窗口类TicketWindow
 */
class TicketSell implements Runnable {
    private  int ticket = 100;
    private  Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        // 模拟售票业务,线程休眠一段时间
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        // 创建一个TicketWindow实例
        TicketSell ticketSell= new TicketSell();


        // 创建三个线程,将TicketWindow实例作为参数传递给Thread的构造方法
        Thread thread1 = new Thread(ticketSell, "窗口一");
        Thread thread2 = new Thread(ticketSell, "窗口二");
        Thread thread3 = new Thread(ticketSell, "窗口三");

        // 启动三个线程,开始售票
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

synchronized关键字的注意事项:

1.一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;

2.每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁;

3.synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁

对象锁
代码块形式:手动指定锁定对象,可以是this,也可以是自定义的锁

方法锁形式:synchronized修饰普通方法,锁对象默认为this。例如:public synchronized void method(){xxx}

类锁
synchronize修饰静态方法。例如:public static synchronized void method()

synchronized指定锁对象为Class对象

synchronized的缺陷:

1.效率低:

锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时

2.不够灵活:

加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活

3.无法知道是否成功获得锁:

相对而言,Lock可以拿到状态,如果成功获取锁,....,如果获取失败,.....

synchronized加锁只与一个条件(是否获取锁)相关联,不灵活,后来condition与lock的结合解决了这个问题:
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。

Synchronized原理分析

深入JVM看字节码指令,创建代码:

点击查看代码
public class SynchronizedDemo2 {

    Object object = new Object();
    public void method1() {
        synchronized (object) {

        }
        method2();
    }

    private static void method2() {
    }
}

使用javac命令进行编译生成.class文件-->javac SynchronizedDemo2.java
使用javap命令反编译查看.class文件的信息-->javap -verbose SynchronizedDemo2.class

反编译结果如下:
image

Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

1.monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待

2.如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加

3.这把锁已经被别的线程获取了,等待锁释放

monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
image

该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

可重入原理:加锁次数计数器

什么是可重入?可重入锁?

可重入
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

可重入锁
又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

观察如下代码:

点击查看代码
public class SynchronizedDemo {

    public static void main(String[] args) {
        SynchronizedDemo demo =  new SynchronizedDemo();
        demo.method1();
    }

    private synchronized void method1() {
        System.out.println(Thread.currentThread().getId() + ": method1()");
        method2();
    }

    private synchronized void method2() {
        System.out.println(Thread.currentThread().getId()+ ": method2()");
        method3();
    }

    private synchronized void method3() {
        System.out.println(Thread.currentThread().getId()+ ": method3()");
    }
}
结合前文中加锁和释放锁的原理,不难理解:

执行monitorenter获取锁 :
(monitor计数器=0,可获取锁)
执行method1()方法,monitor计数器+1 -> 1(获取到锁)
执行method2()方法,monitor计数器+1 -> 2
执行method3()方法,monitor计数器+1 -> 3

执行monitorexit命令:
method3()方法执行完,monitor计数器-1 -> 2
method2()方法执行完,monitor计数器-1 -> 1
method2()方法执行完,monitor计数器-1 -> 0 (释放了锁)
(monitor计数器=0,锁被释放了)

这就是Synchronized的重入性,即在同一锁程中,每个对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会加一,释放锁后就会将monitor计数器减一,线程不需要再次获取同一把锁。

保证可见性的原理:内存模型和happens-before规则

Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。来看代码:

点击查看代码
public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}
该代码的happens-before关系如图所示:

image

什么是happens-before?
Happens-Before(先行发生)原则是对 Java 内存模型(JMM)中所规定的可见性的更高级的语言层面的描述。用这个原则解决并发环境下两个操作之间的可见性问题,而不需要陷入 Java 内存模型苦涩难懂的定义中。关于 Java 内存模型中所规定的可见性定义本文不再叙述,感兴趣的读者可参考的书籍有《深入理解 Java 虚拟机》和《Java 并发编程的艺术》。
可以参考:https://xie.infoq.cn/article/d0f4d9e812ee03b6a32265686 解读JMM中的Happens-Before原则

在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么?

根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。

3.Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
void lock()-->获取锁
void unlock()-->释放锁

点击查看代码
class TicketSell implements Runnable {
    private int ticket = 100;
    private Object obj = new Object();
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
//            synchronized (obj) {
                lock.lock();
                if (ticket > 0) {
                    // 模拟售票业务,线程休眠一段时间
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
                    ticket--;
                } else {
                    break;
                }
                //}
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        // 创建一个TicketWindow实例
        TicketSell ticketSell = new TicketSell();


        // 创建三个线程,将TicketWindow实例作为参数传递给Thread的构造方法
        Thread thread1 = new Thread(ticketSell, "窗口一");
        Thread thread2 = new Thread(ticketSell, "窗口二");
        Thread thread3 = new Thread(ticketSell, "窗口三");

        // 启动三个线程,开始售票
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

4.死锁

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
产生死锁的情况:

1.资源有限

2.同步嵌套

点击查看代码
public class TicketDemo {
    public static void main(String[] args) {Object objA = new Object();
        Object objB = new Object();

        new Thread(() -> {
            while (true) {
                synchronized (objA) {
                    //线程一
                    synchronized (objB) {
                        System.out.println("aaa");
                    }
                }
            }
        }).start();

        new Thread(() -> {
            while (true) {
                synchronized (objB) {
                    //线程二
                    synchronized (objA) {
                        System.out.println("bbb");
                    }
                }
            }
        }).start();

    }
}

会发现执行过程中程序像卡死了一样,一动不动,通过执行过程中的线程信息看到当前两个线程都被阻塞了,循环嵌套等待锁释放:
image

四、生产者和消费者模式

生产者和消费者模式概述

生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

所谓生产者消费者问题,实际上主要是包含了两类线程:

一类是生产者线程用于生产数据

一类是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

Object类的等待和唤醒方法:

void wait()-->导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify()-->唤醒正在等待对象监视器的单个线程
void notifyAll()-->唤醒正在等待对象监视器的所有线程

认识阻塞队列

队列(Queue)是一种经常使用的集合。Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:
把元素添加到队列末尾;
从队列头部取出元素。

阻塞队列是一个在队列基础上又支持了两个附加操作的队列。
2个附加操作:
支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空

常见的BlockingQueue:

  1. ArrayBlockingQueue 数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

  2. LinkedBlockingQueue一个由链表结构组成的有界阻塞队列,最大为int的最大值,此队列按照先出先进的原则对元素进行排序

  3. PriorityBlockingQueue 支持优先级的无界阻塞队列

  4. DelayQueue 支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素

  5. SynchronousQueue不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。并且他支持公平访问队列。

  6. LinkedTransferQueue由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法

BlockingQueue的核心方法
put(anObject)-->将参数放入队列,如果放不进去会阻塞
take()-->取出第一个数据,取不到会阻塞

简易场景之消息队列

举个栗子,你的程序中运行着两个线程,分别是生产者线程和消费者线程。
假设有一个固定容量的消息队列:当队列中有可消费的消息时,生产者线程会通知消费者线程进行消息的消费;
同样地,当队列中有额外的空间时,消费者线程会通知生产者线程进行消息的生产。
即:当消息队列满了,生产者线程应该停止生产并进入等待状态;当消息队列为空的时候,消费者线程应该停止消费并进入等待状态。

点击查看Produce代码
package com.vayne.thread.producerandconsumer;

import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;

import java.util.Queue;

public class Producer implements Runnable {
    private final static Logger LOGGER = LoggerFactory.getLogger(Producer.class);
    private QueueDemo queueDemo;

    public Producer(QueueDemo queueDemo) {
        this.queueDemo = queueDemo;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (queueDemo) {

                // 如果消息队列满了,等待
                while (queueDemo.getMessageCount() == queueDemo.getMAX_SIZE()) {
                    try {
                        queueDemo.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //设置ID
                queueDemo.setMessageId(queueDemo.getMessageId()+1);
                //队列长度+1
                queueDemo.setMessageCount(queueDemo.getMessageCount() + 1);
                queueDemo.getQueue().add(queueDemo.getMessageId());
                System.out.println("生产者生产了消息"+queueDemo.getMessageId()+",此时消费队列中的消息个数"+queueDemo.getMessageCount());
                queueDemo.notifyAll();
            }
        }
    }
}
点击查看Queue代码
package com.vayne.thread.producerandconsumer;


import com.sun.javafx.font.PrismFontFactory;

import java.util.LinkedList;
import java.util.Queue;

/**
 * @author vayne
 * @date 2023-10-31
 */
public class QueueDemo {
    private final Queue<Integer> queue;
    private final Integer MAX_SIZE;

    private  Integer MessageId;

    public void setMessageId(Integer messageId) {
        MessageId = messageId;
    }

    public Integer getMessageId() {
        return MessageId;
    }

    public QueueDemo(){
        this(new LinkedList<>(),1,0,0);
    }

    public QueueDemo(Queue<Integer> queue, Integer MAX_SIZE, Integer messageId, int messageCount) {
        this.queue = queue;
        this.MAX_SIZE = MAX_SIZE;
        MessageId = messageId;
        MessageCount = messageCount;
    }

    private int MessageCount;

    public Queue<Integer> getQueue() {
        return queue;
    }

    public Integer getMAX_SIZE() {
        return MAX_SIZE;
    }

    public int getMessageCount() {
        return MessageCount;
    }

    public void setMessageCount(int messageCount) {
        MessageCount = messageCount;
    }
}

点击查看Consumer代码
package com.vayne.thread.producerandconsumer;

import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;

/**
 * @author vayne
 * @date 2023-10-31
 */
public class Consumer implements Runnable {
    private QueueDemo queueDemo;

    public Consumer(QueueDemo queueDemo) {
        this.queueDemo = queueDemo;
    }
    private final static Logger LOGGER = LoggerFactory.getLogger(Producer.class);

    @Override
    public void run() {
        while (true){
            synchronized (queueDemo){

                while (queueDemo.getMessageCount()==0){
                    try {
                        queueDemo.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                Integer remove = queueDemo.getQueue().remove();
                queueDemo.setMessageCount(queueDemo.getMessageCount()-1);
                System.out.println("消费者消费了消息"+remove+",此时消费队列中的消息个数"+queueDemo.getMessageCount());
                queueDemo.notifyAll();
            }
        }
    }
}

点击查看Launcher代码
package com.vayne.thread.producerandconsumer;

/**
 * @author vayne
 * @date 2023-10-31
 */
public class Launcher {
    public static void main(String[] args) {

        QueueDemo queueDemo = new QueueDemo();

        Producer producer = new Producer(queueDemo);
        Consumer consumer = new Consumer(queueDemo);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

posted @ 2023-10-31 02:27  VayneBeSelf  阅读(164)  评论(1编辑  收藏  举报