目录
1.经典问题:三窗口售票,及同步代码块------synchronized(任意对象){多条语句操作共享数据代码}
一、了解(并发&并行)
并行:同一时刻,多个指令在(多个)cpu上----同时----执行(多个人同时处理多件事)
并发:同一时刻,多个指令在(单个)cpu上----交替----执行(一个人同时交替处理多件事)
二、了解(多线程&单线程)
多线程:指从软件或者硬件上实现多个线程并发执行的技术。具有多线程处理能力的计算机因有硬件的支持而能够在同一时间执行多个线程,提升性能
单线程:一次处理一个线程
三、了解(进程&线程)
进程:比如任务管理器上看见的正在运行的软件,具有【①独立性:进程是一个独立运行的基本单位,同时也是系统分配资源和调度的独立单位】、【②动态性:进程的实质为程序的一次执行过程,动态产生动态消亡】、【③并发性:任何进程可以和其它进程并发执行】
线程:属于进程,是进程中的单个顺序控制流,是一条执行路径,如果一个程序中只有一条执行路径,那么该程序为单线程程序。如果有多个,那就是多线程程序。(线程类:Thread)
四、多线程的实现方案
method1:继承Thread类的方式进行实现
/*
四个步骤:1.定义一个MyThread类去继承Thread类
2.在MyThread类中重写run()
3.创建MyThread类对象
4.启动线程
*/
public class MyThreadMethod1 extends Thread {
/*
---问题1:为什么要重写run()?
答1:因为run()是用来封装被线程执行的代码
---问题2:run()和start()的区别
答2:
run():封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程
start():启动线程,然后由JVM调用此线程的run()
*/
@Override
public void run(){
//这里面的代码就是在线程开启之后执行的代码
for (int i = 0; i <100 ; i++) {
System.out.println("第一种方式实现多线程:"+i);
}
}
}
----------------------------------------------------------
public class Demo1 {
public static void main(String[] args) {
//创建对象
MyThreadMethod1 mt1 = new MyThreadMethod1();
MyThreadMethod1 mt2 = new MyThreadMethod1();
/*
用创建的对象,调用start()
*/
//开启一条线程
mt1.start();
//开启第二条线程
mt2.start();
System.out.println("main方法的单线程");
/*
总结:main()相当于一个单线程,
所以我们这里最后的输出语句会打印在输出框的第一行!!!!
其余多线程执行的输出语句会无规则交替打印在展示台
*/
}
}
method2:实现Runnable接口
/*
步骤:
1.创建一个类,去实现Runnable接口
2.在实现类中重写run()
3.创建一个实现类对象
4.创建Thread类的对象,把实现类对象作为构造方法的参数传入
5.启动线程
*/
public class MyThreadMethod2 implements Runnable{
@Override
public void run() {
//也表示线程启动后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("第二种方式实现多线程:"+i);
}
}
}
----------------------------------------------------------
public class Demo2 {
public static void main(String[] args) {
//创建参数的对象
MyThreadMethod2 mt1 = new MyThreadMethod2();
//创建Thread对象(创建了个线程的对象),传参
Thread t1 = new Thread(mt1);
//在创建的线程对象中调用start()
t1.start();
MyThreadMethod2 mt2 = new MyThreadMethod2();
//创建线程对象,传参
Thread t2 = new Thread(mt2);
t2.start();
System.out.println("测试2");
/*
上述输出语句也是在打印台第一行,原理跟method1中的原理一致,
t1和t2的输出打印也会无规则交替打印
*/
}
}
method3:Callable和Future
/*
步骤:
1.定义一个类实现Callable接口
2.在该实现类中重写call()
3.创建实现类对象
4.创建Future的实现类FutureTask对象,把实现类对象作为构造方法的参数
5.创建Thread类对象,把FutureTask对象作为构造方法的参数
6.启动线程
*/
import java.util.concurrent.Callable;
public class MyThreadMethod3 implements Callable<String> {
/*
注意:这里的call()有返回值,默认为Object.
返回值表示线程运行完毕之后的结果,默认为return null;
*/
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女娃表白:"+i);
}
//这里面带有返回值,表示循环结束后,返回的东西
return "答应了";
}
}
---------------------------------------------------------------
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要使用里面的call()
MyThreadMethod3 mt = new MyThreadMethod3();
/*
Thread thread = new Thread(mt);
这里创建线程类对象会报错,现在实现的为Callable接口,里面没有继承Runnable
所以我们不能直接创建该对象
*/
//获得线程结束之后的结果,也可以作为参数传递给Thread对象
FutureTask<String> ft = new FutureTask<>(mt);
/*
注意FutureTask<>的泛型要和Callable的泛型保持一致
*/
//创建线程对象
Thread t1 = new Thread(ft);
/*
问题:为什么FuntureTask对象可以传入Thread?
因为API中,FuntureTask implements Runnable
*/
//开启线程
t1.start();
//获取【子线程】的执行结果,让FuntureTask<>对象调用get(),根据泛型进行接收
String s = ft.get();
System.out.println(s);
/*
总结:对比三种开启多线程的方式,
如果我们要求多线程结束之后会带有返回值,我们就需要method3
如果不需要,我们可以选择前面两种方式
*/
}
}
注意开启线程start(),和获取线程结束后返回值get(),这两个方法的顺序问题!!!!----要明白get()是获取的线程结束的返回值,如果get()在start()前面,那么就不会运行到start(),就不会开启线程,就更不会得到结果!!!
三种实现多线程方式的对比:
method1:继承Thread, 优点:编程简单,可以直接调用Thread中的方法
method2:实现Runnable接口 优点:扩展性强,可实现/继承其它类 缺点:编程复杂
method3:实现Callable接口 优点:扩展性强,可实现/继承其它类 缺点:编程复杂
五、Thread类(线程类)常用方法
1.获取和自定义线程名字
public class ThreadDemo1 extends Thread{
public ThreadDemo1() {
}
public ThreadDemo1(String name) {
super(name);
}
/*
注意:如果要使用带参创建自定义线程名的时候,
要在Thread的实现类中构造Thread的
Thread() 和 Thread(name:String)两个构造方法
*/
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//在此输出语句中用getName()打印线程名
System.out.println(getName()+"@"+i);
}
}
}
class TestDemo1{
public static void main(String[] args) {
ThreadDemo1 td1 = new ThreadDemo1();
ThreadDemo1 td2 = new ThreadDemo1();
ThreadDemo1 td3 = new ThreadDemo1("自定义线程名method2");
//未自定义线程名
td1.start();//Thread-0@0...
//2.自定义线程名,method1
td2.setName("自定义线程名:");
td2.start();//自定义线程名:@0...
//2.自定义线程名,method2
td3.start();//自定义线程名method2@0...
/*
1.Thread类是有默认的名字,格式为:"Thread-编号"...
2.如果我们要自定义名字怎么办?
答:自定义线程名有两种方法
method1:调用setName()
method2:使用Thread(String name),带参
*/
}
}
2.获取当前线程对象
public static Thread currentThread();返回对当前正在执行的线程对象的引用
情况1:未创建自己的线程,只使用main线程
public class ThreadDemo2 {
public static void main(String[] args) {
/*
在main方法中的currentThread(),是由main线程去调用的
main线程是由虚拟机在刚开始启动的时候创建的,所以我们可以
调用该方法获取当前线程对象,最后.getName()去获得线程对象名
*/
String name = Thread.currentThread().getName();
System.out.println(name);//main
}
}
情况2:创建了线程,实现Runnable接口
//当我们的类实现了Runnable接口的时候
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//我们在这里就不能getName()去获得线程对象名了
System.out.println(Thread.currentThread().getName()+i);
/*
1.得通过上面的Thread.currentThread()获取到当前线程对象
2.然后再.getName()去获取线程名
*/
}
}
}
3.线程休眠
public static void sleep(long time);让线程休眠指定时间,单位为:毫秒(ms)
情况1:当我们未创建自己的线程时
public class ThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
//当我们未创建自己的线程时
System.out.println("测试开始");
Thread.sleep(5000);
System.out.println("测试结束");//会延迟五秒输出
}
}
情况2:我们自己创建了线程,实现Runnable接口
public class ThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
RunnableImpl rb = new RunnableImpl();
Thread th = new Thread(rb);
th.start();
//输出结果没句会间隔500ms
}
}
//情况2:自己创建了个线程,实现Runnable接口
class RunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
/*
注意:当我们只写Thread.sleep(),会报错
原因:Runnable中关于run()中没有throws任何异常
解决方法:我们手动捕获该异常的程序语句
*/
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
4.线程调度
概念:计算机中的cpu,在任意时刻只能执行一条指令,是每个线程只有或的了cpu的使用权才能执行代码,各个线程轮流获得cpu的使用权,分别执行各自的任务。
我们根据线程获得cpu使用权的方式不同,分为:
A.分时调度模型:所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的时间片
B.抢占式调度模型:这里会对比优先级[1-10],优先让优先级高的线程使用,如果优先级相等,那么随机选择使用,优先级高的获得cpu的时间片会相对多一点,但是不保证每次都时第一个执行优先级高的线程。概率论去解释,当样本容量无限大,则会发现优先级高低的影响。
注意:JAVA采取的抢占式调度模型
public final void setPriority(int newPriority);设置线程优先级
public final int getPriority();获取线程的优先级
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadDemo4 {
public static void main(String[] args) {
//创建线程1
CallableImpl c1 = new CallableImpl();
FutureTask<String> ft1 = new FutureTask<>(c1);
Thread t1 = new Thread(ft1);
t1.setName("线程一");
/*
调用getPriority()去获取该线程优先级
我们发现该线程的优先级默认为5
*/
System.out.println("线程一的优先级:"+t1.getPriority());//5
//创建线程2
CallableImpl c2 = new CallableImpl();
FutureTask<String> ft2 = new FutureTask<>(c2);
Thread t2 = new Thread(ft2);
t2.setName("线程二");
/*
当我们对线程2调用setPrioity()设置优先级的时候
我们最后的出该线程优先级为我们设置的那个
注意:优先级的范围[1--10]
*/
t2.setPriority(10);
System.out.println("线程二的优先级:"+t2.getPriority());//10
}
}
//这里我们创建一个线程,实现Callable接口
class CallableImpl implements Callable<String>{
//注意,该类有返回值
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"----"+i);
}
return "线程执行完毕";
}
}
5.后台线程(守护线程)(备胎线程)
public final void setDaemon(boolean on);设置为守护线程
线程在运行中有两种形式:一种为普通线程,另一种为守护线程。举例:聊天软件中存在(聊天线程)(视频线程)(语音线程)这些普通线程,那么退出聊天程序就是这些普通线程的(守护线程)。
public class ThreadDemo5 {
public static void main(String[] args) {
//创建两个线程对象
MyThread51 mt1 = new MyThread51();
MyThread52 mt2 = new MyThread52();
//再给两个线程分别命名
mt1.setName("X");
mt2.setName("备胎");
/*
1.我们把mt2设置为守护线程,
*/
mt2.setDaemon(true);
/*
3.最后我们发现,当mt1线程运行结束之后,mt2线程不再运行
总结:当普通线程结束之后,守护线程没有继续执行的必要了
但是守护线程不会立马结束运行,因为守护线程还占有一部分的cpu使用权
所以守护线程还会再执行一小会!!!
*/
mt1.start();
mt2.start();
}
}
//创建一个类,继承Thread
class MyThread51 extends Thread{
@Override
public void run() {
/*
2.然后更改mt1中的线程体条件,比如循环到10结束
*/
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"---"+i);
}
}
}
//再创建一个类,也是继承Thread
class MyThread52 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"---"+i);
}
}
}
六、线程安全问题
1.经典问题:三窗口售票,及同步代码块------synchronized(任意对象){多条语句操作共享数据代码}
/*
经典售票问题
*/
public class TicketTest implements Runnable {
//创建一个成员属性,票数=100张
private static int ticket = 100;
/*
创建任意对象,好传入同步代码块
为什么还要static修饰,是因为,这个类不一定只有一个对象,
加入static是为了让本类所有对象共享
*/
private static Object obj = new Object();
@Override
public void run() {
//死循环
while (true) {
//使用同步代码块,里面可以是任意对象
synchronized (obj){
if (ticket == 0) {
//当票数还剩0张的时候退出死循环
break;
} else {
/*
问题:当我们加入sleep()的时候,会出现负数票和重复售票
解答:方式一:我们在if语句中加入<
方式二:我们可以加入同步代码块synchronized(){},完美解决重复和负票
方式三:我们可以将ticket--;提到sleep()之前
*/
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "在售票,还剩" + ticket + "张票");
}
}
}
}
}
//创建一个测试类
class Test{
public static void main(String[] args) {
//这里只用创建一个对象,因为只对一个对象中的属性进行操作
TicketTest tt = new TicketTest();
//创建线程类,需要创建三个
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);
//起名
t1.setName("窗口1:");
t2.setName("窗口2:");
t3.setName("窗口3:");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
三窗口售票出现的问题:当我们按照生活常理加入sleep()延迟相邻售票操作,那么会出现负数票和重复票,所以我们解决方案就是引入同步代码块synchronized(){}
同步代码块:默认为打开,只要有一个线程进入执行代码,那么锁就会关闭。优点:解决了多线程数据安全问题,缺点:耗费资源,降低程序运行效率。
还有关于同步代码块的问题:当一个类创建了多个对象,这个时候我们要记得在本来中将成员属性加入static修饰,当然同步代码块传入的对象,也要被该类所有对象共享,所以上述代码最后完善记得都要加入static修饰!!!以确保同步代码块的唯一性
2.同步方法:把synchronized关键字加载方法上
格式:修饰符 synchronized 返回值类型 方法名(方法参数){.....};
public static synchronized String method(){return null......};就表示一个静态的同步方法
注意:①如果一个方法为同步方法,那么该方法对象为this;
②如果一个方法为同步静态方法,那么该方法对象为 类名.class
/*
在刚刚售票代码的基础上我们把售票的代码提取出来,做成一个静态的同步方法
*/
public static synchronized boolean synchronMthod() {
if (ticket == 0) {
//当票数还剩0张的时候退出死循环
return true;
} else {
ticket--;
System.out.println(Thread.currentThread().getName() + "在售票,还剩" + ticket + "张票");
return false;
}
}
/*
同步代码块和同步方法的区别:
1.同步代码块可以锁定指定代码,同步方法是锁住方法中的所有代码
2.同步代码块可以指定锁定对象,同步方法不能指定锁定对象
注意:如果我们设置的是一个同步静态方法,那么如果要在该类书写第二个同步代码块
那么此时同步代码块传入的对象,就是(类名.class)
*/
3.Lock锁----是在JDK1.5之后提供的新的锁对象
Lock锁实现了提供比synchronized()更广泛的锁定操作
其中的方法有:void lock(); 获得锁 / void unlock();开锁
注意:Lock是接口,不能实例化,使用时候需要采用其实现类ReentranLock来实例化。ReentranLock的构造方法:ReentranLock();创建该实例
-----结合刚开始的售票问题,我们可以把synchronized(),换成Lock锁
public class TicketTest implements Runnable{
//创建一个成员属性,票数=100张
private static int ticket = 100;
//使用lock锁
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
//使用lock锁
while(true){
try {
lock.lock();
if (ticket <= 0) {
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在售票,还剩" + ticket + "张票");
}
}catch(InterruptedException e){
//这里捕捉的异常,为上面Thread.sleep()抛出的异常
e.printStackTrace();
}finally {
//必须得开锁
lock.unlock();
}
}
}
/*
注意:
我们要使用Lock锁的时候,记得创建Lock的实现类对象new ReentrantLock();
然后我们要确保unlock()释放锁的方法一定运行,我们用try/catch/finall{lock.unlock();}
*/
}
七、线程死锁
概念:由于多个线程互相持有对方需要的资源,导致这些线程始终处于等待时间,无法执行代码
public class LockDeath {
/*
线程死锁
*/
public static void main(String[] args) {
//创建两把锁
Object o1 = new Object();//锁1
Object o2 = new Object();//锁2
//lambda表示式
new Thread(()->{
while (true) {
synchronized (o1){
synchronized (o2){
System.out.println("测试线路一");
}
}
}
}).start();//会发现,开启线程之后,线路一一直存在,且没有结束
new Thread(()->{
while (true) {
synchronized (o2){
synchronized (o1){
System.out.println("测试线路二");
}
}
}
}).start();//压根没有线路二
}
}
八、生产者与消费者模式
1.生产/消费者模式代码实现
这个模式会用到一些线程唤醒和等待的方法
void wait();--让当前线程等待,直到另一个线程调用该对象的notify()或者notifyAll();
void notify();--唤醒正在等待对象监视器的单个线程
void notifyAll();--唤醒正在等待对象监视器的所有线程
public class Model {
/*
生产--消费模式
为了简化代码页数,我把这些写在一个java文件中
注意:1.最后一定要对公区域进行私有化,然后作为参数传入生产者和消费者对象
2.还有Desk类中的count一定要赋值,如果走默认值,那么输出台不会输出东西
*/
public static void main(String[] args) {
//我们注意,我们需要把Desk公共区域对象传入给生产者和消费者
Desk desk = new Desk();
//当我们三个类创建好之后,最后我们进行测试
people people = new people(desk);
producer producer = new producer(desk);
//开启线程
people.start();
producer.start();
}
}
//创建餐桌类,记住最后要进行private私有,确保数据安全
class Desk{
/*
要明白餐桌(线程通信区域)类需要进行什么操作
*/
//定义一个标记,用来记录该区域是否存在东西,默认无东西,就为false
private boolean flag = false;//false的话就让生产者执行生产,true相反
//定义交易的总数量
private int count = 3;
//创建一个生产者和消费者都使用的一个锁,而且不被允许修改,所以final修饰
private final Object lock = new Object();
//创建好后,可以传入消费者里的同步代码块和生产者里的
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public Object getLock() {
return lock;
}
public Desk(boolean flag, int count) {
this.flag = flag;
this.count = count;
}
public Desk() {
}
}
//创建消费者类,让消费者继承Thread
class people extends Thread{
//当我们把共享区域传入该类,我们需要创造一个people的构造方法,把desk传入
private Desk desk;
public people(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
//这里我们用while(true){}
while(true){
//由于生产者线程和消费者线程都要使用同一把锁,所以我们加入同步代码块
//再加上如果把锁定义在消费者类中,那么其余线程要调用就得创建消费者对象去调用,
//所以我们直接把同步代码块的对象创建到公共区域Desk类中
synchronized(desk.getLock()){
//开始加入判断语句,让消费者对交易总数量进行判断
if(desk.getCount() == 0){
break;
}else{
//然后对共享区域是否存在商品进行判断
if(desk.isFlag()){
//如果有东西,我们就开始代码
System.out.println("---消费者购买了");
//将共享区域Desk中的标志换位false,再进行唤醒生产者线程
desk.setFlag(false);
desk.getLock().notifyAll();//注意notify()是唤醒随机的一个
//Desk.count--;//总数--
desk.setCount(desk.getCount() -1);
}else{
//如果没有东西就等待,调用wait()
//但是我们要注意,wait()是得什么对象当作锁,就要用什么对象去调用
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
//创建生产者类,也让生产者继承Thread
class producer extends Thread{
private Desk desk;
public producer(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
while(true){
synchronized(desk.getLock()){
if(desk.getCount() == 0){
break;
}else{
if(!desk.isFlag()){
System.out.println("生产者生产");
//生产之后,让共享区与的标志为true,再进行唤醒消费者线程
desk.setFlag(true);
desk.getLock().notifyAll();
}else{
//表示共享区域存在东西,那么就让生产线程等待
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
2.阻塞队列
消费者 <---- take() -----{商品数num:1,2,3,4,5,6 } <---- put() -----生产者
阻塞队列,相当于给共享区域生成了一个容器,容器规定生产者最多放下多少个商品,if(生产者放入商品数num>6 || 消费者获取的商品数<1) {sout("都会造成阻塞");}
阻塞队列的继承结构:
(接口)Iterable-->(接口)Collection-->(接口)Queue-->(接口)BlockingQueue-->(实现类1)ArrayBlockingQueue /(实现类2)LinkedBlockingQueue
其中ArrayBlockingQueue:底层是数组,有界 LinkedBlockingQueue:底层是链表,无界(非真正无界,界限为int最大值)
import java.util.concurrent.ArrayBlockingQueue;
public class ArrayBlocking {
/*
阻塞队列ArrayBlockingQueue
*/
public static void main(String[] args) throws InterruptedException {
//1表示该阻塞队列的最大容量,我们设定为1
ArrayBlockingQueue<String> ab = new ArrayBlockingQueue<>(1);
//放入数据测试
ab.put("111");
System.out.println(ab.take());
System.out.println(ab.take());
//最后输出测试一下
System.out.println("main的输出测试");
/*
造成输出结果的原因:因为阻塞队列容量只有1,也只能获取一个
下面take()了两次,第二次取不出来所以造成阻塞
*/
}
}
3.阻塞队列实现等待唤醒机制
结合上述生产者与消费者模式,我们发现,所谓的共享区域Desk,其实可以用阻塞队列ArrayBlockingQueue去代替
出现问题:打印错误!!!(原因在代码中)
import java.util.concurrent.ArrayBlockingQueue;
public class ArrayBlockingDemo2 {
public static void main(String[] args) {
//创建阻塞队列对象,容量设为1,代表生产一个,共享区存在一个
ArrayBlockingQueue<String> list = new ArrayBlockingQueue<String>(1);
//创建消费者和生产者线程
people2 p2 = new people2(list);
producer2 d2 = new producer2(list);
//开启线程
p2.start();
d2.start();
/*
关于打印结果出现的原因:
首先我们明确一点,关于put()和take()底层,都是有lock锁的,
但是我们的打印和输出语句是在main线程中的,这个会发生一个cpu抢占概率的问题,
导致了控制台输出非程序真实运行的结果,所以阻塞队列的设置是没有问题的!!!!
这里只是打印的问题!!!
*/
}
}
//创造消费者类
class people2 extends Thread{
private ArrayBlockingQueue<String> list;
public people2(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
try {
String take = list.take();
System.out.println("消费者从阻塞队列拿取"+take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//创建生产者类
class producer2 extends Thread{
private ArrayBlockingQueue<String> list;
public producer2(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
try {
list.put("商品");
System.out.println("生产者生产商品放入了阻塞队列");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里没有像最开始设置count,所以会一直输出。