一、基础
什么是多线程
- 进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
- 线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
- 什么是多线程:只要你想让多个事情同时运行就需要用到多线程
二、概念
1.并发和并行
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
2.线程的状态
- 状态说明
- 新建状态(New)
- 当使用new关键字创建一个线程对象时,线程处于新建状态。
- 在这个状态下,线程对象已经被分配内存,但尚未调用其start()方法启动线程。
- 示例代码:Thread thread = new Thread();
2.就绪状态(Runnable)
- 当线程对象调用start()方法后,线程进入就绪状态。
- 在就绪状态下,线程处于可运行状态,等待操作系统的线程调度器调度执行。
- 处于就绪状态的线程位于JVM的可运行池中,等待CPU的使用权。
- 线程从新建状态转换到就绪状态的条件是调用start()方法。
3.运行状态(Running)
- 当就绪状态的线程获得CPU的使用权时,线程进入运行状态。
- 在运行状态下,线程占用CPU并执行程序代码。
- 如果计算机有多个CPU,同一时刻可以有几个线程同时处于运行状态,分别占用不同的CPU。
- 线程从就绪状态转换到运行状态的条件是获得CPU的使用权。
4.阻塞状态(Blocked)
- 线程在运行过程中,可能因为某些原因放弃CPU并暂时停止运行,进入阻塞状态。
- 阻塞状态的原因可能包括:
- 同步阻塞:当线程试图获取某个对象的同步锁,但该锁已被其他线程占用时。
- 其他阻塞:如线程执行了sleep()方法,或发出I/O请求等待资源等。
- 线程从运行状态或就绪状态转换到阻塞状态的条件是遇到阻塞操作。
5.等待状态(Waiting)和计时等待状态(Timed Waiting)
- 线程调用wait()方法或join()方法,或执行了带有超时参数的sleep(long)方法时,会进入等待状态或计时等待状态。
- 在这两种状态下,线程会放弃CPU并暂停执行,直到满足一定条件(如其他线程调用notify()或notifyAll()方法,或超时时间到达)后才会被唤醒。
- 线程从运行状态或就绪状态转换到等待或计时等待状态的条件是调用相应的等待方法。
6.终止状态(Terminated)
- 当线程执行完run()方法中的代码,或遇到未捕获的异常时,会退出run()方法并进入终止状态。
- 在终止状态下,线程结束其生命周期,不再执行任何代码。
- 线程从运行状态、阻塞状态、等待状态或计时等待状态转换到终止状态的条件是执行完run()方法或遇到未捕获的异常。
3.线程是如何获取CPU的使用权的
线程获取CPU执行权的过程可以被认为是基于一种抢占(或称为竞争)的机制,但这个过程实际上是由操作系统的线程调度器来管理和控制的。
以抢座休息为例:cpu的执行权就是椅子,而线程就是围椅子转要抢椅子的人,当只有一个人的时候,无需抢椅子,但当多个人的时候就需要抢椅子(多个线程需要抢占某个cpu的执行权,即就绪状态),抢到椅子的人就可以休息一会(某个线程线程抢到执行权开始执行代码,即运行状态,其他线程依然还是就绪状态),直到坐椅子的人站起来(线程放弃cpu执行权,即阻塞或者等待状态,还有就是执行完了的终止状态),如果这个人没有休息足够,就会和其他线程接着围绕椅子转进行抢椅子(线程从运行状态回到阻塞状体),知道这个人休息够了(即线程的代码执行完毕,即终止状态)。
三、实现方式
1.继承Thread类的方式进行实现
jdk说明
线程是程序中执行的线程。Java虚拟机允许应用程序同时执行多个执行线程。
每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。 每个线程可能也可能不会被标记为守护程序。 当在某个线程中运行的代码创建一个新的
Thread
对象时,新线程的优先级最初设置为等于创建线程的优先级,并且当且仅当创建线程是守护进程时才是守护线程。当Java虚拟机启动时,通常有一个非守护进程线程(通常调用某些指定类的名为
main
的方法)。 Java虚拟机将继续执行线程,直到发生以下任一情况:
- 已经调用了
Runtime
类的exit
方法,并且安全管理器已经允许进行退出操作。- 所有不是守护进程线程的线程都已经死亡,无论是从调用返回到
run
方法还是抛出超出run
方法的run
。创建一个新的执行线程有两种方法。 一个是将一个类声明为
Thread
的子类。 这个子类应该重写run
类的方法Thread
。 然后可以分配并启动子类的实例。 例如,计算大于规定值的素数的线程可以写成如下:class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } }
然后,以下代码将创建一个线程并启动它运行:
PrimeThread p = new PrimeThread(143); p.start();
实例演示
-
MyThread类(多线程类)
//多线程的类需要继承Thread类 public class MyThread extends Thread{ //重写Thread类的run方法 @Override public void run() { //书写线程要执行的代码 //这里以执行100次的输出各个 线程名+字符 为例子 for (int i = 0; i < 100; i++) { //getName()可以获取当前线程的线程名 System.out.println(getName()+"hello word"); } } }
-
demo类
public class demo01 {
public static void main(String[] args) {
/**
* 多线程的第一种启动方式:
* 1.自己定义一个类继承Thread
* 2.重写run方法
* 3.创建子类的对象,并启动线程
*/
MyThread t1= new MyThread();
MyThread t2= new MyThread();
//给线程取名,用于区分线程一二
t1.setName("线程1");
t2.setName("线程2");
//用start启动线程,并不是调用run方法
t1.start();
t2.start();
}
}
运行结果:线程一二同时进行
线程1hello word
线程1hello word
线程1hello word
线程1hello word
线程2hello word
线程2hello word
线程2hello word
线程2hello word
线程1hello word
线程1hello word
线程2hello word…
2.实现Runnable接口的方式进行实现
另一种方法来创建一个线程是声明实现类
Runnable
接口。 那个类然后实现了run
方法。 然后可以分配类的实例,在创建Thread
时作为参数传递,并启动。 这种其他风格的同一个例子如下所示:class PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } }
- 然后,以下代码将创建一个线程并启动它运行:
PrimeRun p = new PrimeRun(143); new Thread(p).start();
每个线程都有一个用于识别目的的名称。 多个线程可能具有相同的名称。 如果在创建线程时未指定名称,则会为其生成一个新名称。
除非另有说明,否则将
null
参数传递给null
中的构造函数或方法将导致抛出NullPointerException
。
实例演示
-
MyThread类
//多线程的类需要实现Runnable接口中的run方法 public class MyThread02 implements Runnable{ //重写run方法 @Override public void run() { //书写线程要执行的代码 for (int i = 0; i < 100; i++) { //获取到当前线程的对象,因为Runnable无法直接获取线程对象的属性 Thread t=Thread.currentThread(); System.out.println(t.getName()+"hello word"); } } }
-
demo类
public class demo02 {
public static void main(String[] args) {
/**
* 多线程的第二种启动方式:
* 1.自己定义一个类实现Runnable接口
* 2.重写里面的run方法
* 3.创建自己的类的对象
* 4.创建一个Thread类的对象,并开启线程
*/
//创建线程对相
//表示多线程要执行的任务
MyThread02 mr =new MyThread02();
//创建线程对象
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
运行结果:线程一二同时进行
线程1hello word
线程1hello word
线程1hello word
线程1hello word
线程2hello word
线程2hello word
线程2hello word
线程2hello word
线程1hello word
线程1hello word
线程2hello word…
- Runnable也可以通过 匿名内部类创建Runnable子类对象
Thread thread3=new Thread(new Runnable() {
//重写run方法
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 100; i++) {
//获取到当前线程的对象,因为Runnable无法直接获取线程对象的属性
Thread t = Thread.currentThread();
System.out.println(t.getName() + "hello word");
}
}
});
- 也可以通过lambda 的形式创建Runnable子类对象
// 使⽤ lambda 表达式创建 Runnable ⼦类对象
Thread thread4= new Thread(() -> {
//书写线程要执行的代码
for (int i = 0; i < 100; i++) {
//获取到当前线程的对象,因为Runnable无法直接获取线程对象的属性
Thread t = Thread.currentThread();
System.out.println(t.getName() + "hello word");
}
});
3.利用Callable接口和Future接口方式实现
Callable可以获取多线程结果
实例演示
-
MyThread类
import java.util.concurrent.Callable; //Callable可以对值进返回,通过实现Callable接口并说明要返回属性值的类型 public class MyThread03 implements Callable<Integer> { //Callable重写的是call方法,并且可以将异常进行返回 @Override public Integer call() throws Exception { int sum=0; for (int i = 0; i < 101; i++) { sum+=i; } return sum; } }
-
demo类
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 多线程的第三种实现方式:
* 特点:可以获取到多线程运行的结果
* 1.创建一个类MyCallable实现callable接口
* 2.重写ca11 (是有返回值的,表示多线程运行的结果)
* 3.创建MyCallable的对象(表示多线程要执行的任务)
* 4.创建FutureTask的对象(作用管理多线程运行的结果)
* 5.创建Thread类的对象,并启动(表示线程)
*/
//创建线程对相
//表示多线程要执行的任务
MyThread03 mr =new MyThread03();
//创建FutureTask的对象(作用管理多线程运行的结果)
FutureTask<Integer> integerFutureTask = new FutureTask<>(mr);
//创建线程对象
Thread t1=new Thread(integerFutureTask);
//开启线程
t1.start();
//获取多线程结果
Integer i = integerFutureTask.get();
System.out.println(i);
}
}
运行结果:线程一运行结果
5050
三种线程的对比
优点 | 缺点 | |
---|---|---|
Thread | 编成比较简单,可以直接使用thread类中的方法 | 可扩展性较差,不能再继承其他类 |
Runnable | 1.扩展性强,实现该接口的同时还可以继承其他的类,2.线程和任务分离,提高了程序健壮性 3.线程池接受Runnable类型任务,不接受Thread类型线程 | 编成相对复杂,不能直接使用Thread类中的方法 |
Callable | 1.扩展性强,实现该接口的同时还可以继承其他的类,线程和任务分离,提高了程序健壮性 2.线程和任务分离,提高了程序健壮性3.call方法具有返回值 | 编成相对复杂,不能直接使用Thread类中的方法 |
四、常见的成员方法
注意:
1、如果我们没有给线程设置名字,线程也是有默认的名字的
* 格式:Thread-x(X序号,从0开始的)
2、如果我们要给线程设置名字,可以用setName方法进行设置,也可以构造方法设置
3.细节:static Thread currentThread()获取当前线程的对象
当JVM虚拟机启动之后,会自动的启动多条线程
其中有一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码
在以前,我们写的所有的代码,其实都是运行在main线程当中
4.static void sleep(long time) 让线程休眠指定的时间,学位为毫秒
细节:
1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
2、方法的参数:就表示睡眠的时间,单位毫秒
1秒=1000毫秒
3、当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
1.线程优先级
java采取抢占式调度方式:多个进程在抢夺cpu的执行权,Java中优先级越高抢夺cpu控制权的概率越大
还有一种是非抢占式调度方式:所有线程轮流执行
- 继承Thread的类
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"@"+i);
}
}
}
- demo : 获取线程的优先级
/**final int getPriority() 获取线程的优先级 */
public class demo01 {
public static void main(String[] args) {
/**
* setPriority(int newPriority) 设置线程的优先级
* final int getPriority() 获取线程的优先级
*/
MyThread t1= new MyThread();
MyThread t2= new MyThread();
t1.setName("飞机");
t2.setName("坦克");
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
}
}
注意:java如果没有设置优先级的话,通过源码可知默认优先级是5,最小是1,最大是10
- demo :设置线程的优先级
public class demo01 {
public static void main(String[] args) {
/**
* setPriority(int newPriority) 设置线程的优先级
*/
MyThread t1= new MyThread();
MyThread t2= new MyThread();
t1.setName("飞机");
t2.setName("坦克");
t1.setPriority(1);
t1.setPriority(10);
t1.start();
t2.start();
}
}
注意:优先级只是获取cpu执行权的概率变高,并不是一定先执行完
2.守护线程
- 什么是守护线程
- final void setDaemon(boolean on) 设置为守护线程
当所有用户线程(也称为前台线程或非守护线程)结束时,无论守护线程是否执行完毕,程序都会退出,同时守护线程会随之立即终止。
以女神和备胎举例:只要女神存在,那么备胎也有存在的必要,但是某一天女神嫁人了(非守护线程结束),那么备胎的意义就没了,全都不需要了(守护线程终止)。
- 实现:
demo
public class demo {
public static void main(String[] args) {
/**
* final void setDaemon(boolean on) 设置为守护线程
* 细节:
* 当其他的非守护线程执行完毕之后,守护线程会陆续结束
* 通俗易懂:
* 当女神线程结束了,那么备胎也没有存在的必要了,也会结束
*/
nvsheng t1= new nvsheng();
beitai t2= new beitai();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设置为守护线程(备胎线程)
t2.setDaemon(true);
t1.start();
t2.start();
}
}
thread类
女神类
public class nvsheng extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"@"+i);
}
}
}
备胎类(守护线程)
public class beitai extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"@"+i);
}
}
}
- 应用场景
qq的聊天对话界面
当聊天框开启时,我们可以边聊天边下载,也可以等待下载
当聊天框关闭时,下载也就没有必要了,这时将下载变成守护线程,线程一结束后,下载即使没有下载完也结束了。
3.出让插入线程
- 出让线程
出让线程就是将当前线程cpu的执行权交出,并与其他线程重新抢夺cpu执行权
thread类代码
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"@"+i);
//表示出让当前cpu的执行权给别人
Thread.yield();
}
}
}
注意:出让后,进程会重新抢夺,并不是直接让给
-
插入线程:将插入的线程在下面的线程之前执行
public class demo { public static void main(String[] args) { /** * public final void join() 插入线程/插队线程 */ MyThread t1= new MyThread(); t1.setName("土豆"); //表示把t这个线程,插入到当前线程之前。 //t:土豆 //当前线程:main线程 t1.start(); //执行在main线程当中的 for (int i = 0; i < 10; i++) { System.out.println("main线程"+i); } } }
表示 把 t1 这个线程,插入到当前线程之前
t1 表示线程 1
当前线程 : 在 join 下的第一个线程 : main
五、线程安全
- 什么是线程安全
线程安全是指在多线程环境下,多个线程同时访问同一资源时,不会产生意外结果或导致数据出错的状态。一个线程安全的程序能够正确地处理并发请求,不论线程执行的顺序如何。
在实际开发中,线程安全非常重要,因为多线程经常会同时访问共享数据或资源,如果没有进行适当的保护措施,就会导致数据的不一致性、错误或丢失等问题。
常用的线程安全方法有:
- 加锁:通过加锁机制控制对共享资源的访问,使得每次只有一个线程能够访问共享资源。Java中提供了synchronized关键字和Lock接口来实现加锁功能。
- 使用原子变量:Java提供了一些原子类,例如AtomicInteger和AtomicReference等,它们可以保证在并发情况下对变量的操作是原子性的,从而避免数据错误或不一致的问题。
- 使用线程安全的集合类:Java中提供了一些线程安全的集合类,例如ConcurrentHashMap和ConcurrentLinkedQueue等,这些类本身就具备线程安全性,能够在多线程环境下正确地处理并发请求。
需求: 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口票,请设计一个程序模拟该电影院卖票
- demo
public class demo {
public static void main(String[] args) {
/**
* 需求:
* 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口票,请设计一个程序模拟该电影院卖票
*/
thread01 t1=new thread01();
thread01 t2=new thread01();
thread01 t3=new thread01();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- thread
public class thread01 extends Thread{
static int ticket=0;
@Override
public void run() {
while (true){
if (ticket<100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName()+"正在卖第"+ticket+"张票!!!");
}else break;
}
}
}
输出结果
窗口3正在卖第96张票!!!
窗口1正在卖第97张票!!!
窗口3正在卖第97张票!!!
窗口2正在卖第98张票!!!
窗口3正在卖第99张票!!!
窗口1正在卖第99张票!!!
窗口2正在卖第100张票!!!
窗口1正在卖第101张票!!!
窗口3正在卖第101张票!!!
问题:当多个线程同时操作数据时,相同的数据出现多次,出现超出范围的数据。这时候该如何解决?
可以通过对共享数据进行加锁,来解决改问题。
这个博客是对java锁的一个详细介绍:
java中的各种锁详细介绍 - JYRoy - 博客园 (cnblogs.com)
1.synchronized锁
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或
方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量
级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开
销更低),而且其使用也更容易出错。
可以通过下面的博客了解一下:
volatile关键字
(1).同步代码块
解决:把操作共享数据的代码锁起来
格式:
synchronized(锁){
操作共享数据代码块
}
锁的特点:
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
-
加锁后的thread
public class thread01 extends Thread{ //表示这个类所有的对象,都共享ticket数据 static int ticket=0; //锁对象,一定要是唯一的 static Object obj=new Object(); @Override public void run() { while (true){ //同步代码块 //obj可以是 类名.class , 不可以是this,因为this并不是唯一的,类名.class在文件夹中是唯一的 synchronized (obj){ if (ticket<100) { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } ticket++; System.out.println(getName()+"正在卖第"+ticket+"张票!!!"); }else break; } } } }
注意:锁对象,一定要是唯一的
(2)同步方法
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){
操作共享数据代码块
}
同步方法特点:
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定
同步方法使用Runnable进行多线程操作,因为Runnable是唯一的。
- demo代码
package 线程.tongbu;
public class demo {
public static void main(String[] args) {
/**
* 需求:
* 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口票,请设计一个程序模拟该电影院卖票
*/
thread01 thread01=new thread01();
Thread t1=new Thread(thread01);
Thread t2=new Thread(thread01);
Thread t3=new Thread(thread01);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- Runnable
public class thread01 implements Runnable{
//表示这个类所有的对象,都共享ticket数据
int ticket=0;
@Override
public void run() {
//1.循环
while (true){
//2.同步代码块(同步方法)
if (extracted()) break;
}
}
//同步方法
private synchronized boolean extracted() {
//3.判断共享数据是否到了末尾,如果到了末尾
if (ticket==100){
return true;
}else {
//4.判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName()+"在卖第"+ticket+"张票");
}
return false;
}
}
2.Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题
但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象LockLock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
- thread
public class thread01 extends Thread{
//表示这个类所有的对象,都共享ticket数据
static int ticket=0;
static Lock lock=new ReentrantLock();
@Override
public void run() {
while (true){
//同步代码块
//原先加synchronized锁的地方改成加Lock锁
// synchronized (thread01.class){
lock.lock();//加锁
try {
if (ticket<100) {
Thread.sleep(10);
ticket++;
System.out.println(getName()+"正在卖第"+ticket+"张票!!!");
}else break;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//通过finally的一个特性,可以使每次共享代码块执行完成后或者报错都会执行释放锁的操作
lock.unlock();//释放锁
}
// }
}
}
}
六、死锁
有两把锁,A锁嵌套B锁,B锁嵌套A锁,当B需要A资源则需要A锁释放资源,同时A也在等B锁释放资源。
**注意:**以后写代码不要将两个锁嵌套起来
七、生产者和消费者(等待唤醒机制)
生产者消费者模式是一个十分经典的多线程协作的模式
需求:完成生产者和消费者(等待唤醒机制)的代码
实现线程轮流交替执行的效果
- 控制层(desk)
public class Desk {
/**
* 作用:控制生产者和消费者的执行
*/
//是否有面条 0:没有面条 1:有面条
public static int footFlag=0;
//总个数
public static int count=10;
//锁对象
public static Object lock=new Object();
}
- 消费者
public class Foodie extends Thread{
@Override
public void run() {
/**
*1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(到了末尾)
* 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
*/
while (true){
synchronized (Desk.lock){
if (Desk.count==0){
break;
}else {
//先判断桌子上是否有面条
if (Desk.footFlag==0){
//如果没有,就等待
try {
Desk.lock.wait();//让当前线程跟锁进行绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//把吃的总数-1
Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃"+Desk.count + "碗!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();//唤醒跟这把锁绑定的所有线程
//修改桌子的状态
Desk.footFlag=0;
}
}
}
}
}
}
- 生产者
public class Cook extends Thread{
@Override
public void run() {
/**
*
*/
while (true){
synchronized (Desk.lock){
if (Desk.count==0){
break;
}else {
if (Desk.footFlag>0){
//判断桌子上是否有食物
//如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.footFlag=1;
//侥幸等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
- demo
public class demo {
public static void main(String[] args) {
/**
* 需求:完成生产者和消费者(等待唤醒机制)的代码
* 实现线程轮流交替执行的效果
*/
Cook c=new Cook();
Foodie f =new Foodie();
c.setName("厨师");
f.setName("吃货");
c.start();
f.start();
}
}
运行结果:
厨师做了一碗面条
吃货在吃面条,还能再吃9碗!
厨师做了一碗面条
吃货在吃面条,还能再吃8碗!
厨师做了一碗面条
吃货在吃面条,还能再吃7碗!
厨师做了一碗面条
吃货在吃面条,还能再吃6碗!
厨师做了一碗面条
吃货在吃面条,还能再吃5碗!
厨师做了一碗面条
吃货在吃面条,还能再吃4碗!
厨师做了一碗面条
吃货在吃面条,还能再吃3碗!
厨师做了一碗面条
吃货在吃面条,还能再吃2碗!
厨师做了一碗面条
吃货在吃面条,还能再吃1碗!
厨师做了一碗面条
吃货在吃面条,还能再吃0碗!
2.阻塞队列方式实现
什么是阻塞队列?
阻塞队列是一种特殊类型的队列,具有阻塞特性。当队列为空时,消费者线程试图从队列中获取元素时会被阻塞,直到队列中有新的元素加入;当队列已满时,生产者线程试图向队列中添加元素时会被阻塞,直到队列中有空位。
阻塞队列的实现方式
阻塞队列可以通过不同的实现方式来实现,常见的实现方式包括基于数组的实现(ArrayBlockingQueue)和基于链表的实现(LinkedBlockingQueue)。我们将使用基于数组的方式来实现一个简单的阻塞队列。
- 消费者
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
//将生产者生产出来阻塞队列传递过来
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
//不断从阻塞队列中获取面条
System.out.println(queue.take());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
- 生产者
public class Cook extends Thread{
ArrayBlockingQueue<String> queue;//自带方法锁
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
//不断的把面条放到阻塞队列当中
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
- demo
public class demo {
public static void main(String[] args) {
/**
* 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
* 细节:
* 生产者和消费者必须使用同一个阻塞队列
*/
//1.创建阻塞队列的对象
ArrayBlockingQueue<String> queue =new ArrayBlockingQueue<>(1);
//2.创建线程的对象,并把阻塞队列传递过去
Cook c=new Cook(queue);
Foodie f =new Foodie(queue);
c.setName("厨师");
f.setName("吃货");
//3.开启线程
c.start();
f.start();
}
}
- 打印结果
了一碗面条
厨师放了一碗面条
面条
面条
厨师放了一碗面条
厨师放了一碗面条
厨师放了一碗面条
面条
面条
面条
厨师放了一碗面条
厨师放了一碗面条
厨师放了一碗面条面条
面条
面条
为什么会重复: 应为打印语句在锁的外面,在打印前其他线程会抢占控制权,导致打印错乱,但数据确是正常运行
八、线程池
对线程池的一个抽象化理解
- 背景是一家餐厅(线程池),餐厅有两种员工,分别是正式员工(核心线程数),临时员工(临时线程数),并且员工与顾客只能一对一服务。(所有员工的总数量就是最大线程数)
- 假设该餐厅的正式员工数量为3,临时员工数量为2,门口等待的椅子数量为3(任务队列).此时开始营业
- 营业后来了第一个顾客,此时员工一快速迎上去,进行一对一的服务,服务中又来了第二个顾客,此时员工二也就迎上去,直到3个员工都在工作。
- 此时三个员工都在工作,再来顾客也就只能在门外椅子处等待前面顾客出来。(线程进任务队列等待)
- 当椅子都坐满后,又来顾客,此时餐厅经理赶快让临时员工工作,让他们招待后面来没有椅子的顾客(注意:这里临时线程创建后并不是先处理等待队列中的线程,而是先处理溢出的线程)
- 当正式和临时员工也被占满了,等待的板凳也坐满了时,再来顾客也只能根据条件让顾客不要等待了(任务的拒绝策略 )
- 当过了用餐高峰期,临时员工一定时间没有工作了,就将临时员工辞退(空闲线程最大存活时间)
public class 线程池 {
public static void main(String[] args) {
/**
* ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
* (核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
* 参数一:核心线程数量 不能小于0
* 参数二:最大线程数 不能小于0,最大数量>=核心线程数量
* 参数三:空闲线程最大存活时间 不能小于0
* 参数四:时间单位 用TimeUnit指定
* 参数五:任务队列 不能为null
* 参数六:创建线程工厂 不能为null
* 参数七:任务的拒绝策略 不能为null
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3,//核心线程数量,不能小于0
6,//最大线程数,不能小于0,最大数量>=核心线程数量
60, //空闲线程最大存活时间,不能小于0
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//任务队列,指定队伍长度
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
//添加线程
threadPoolExecutor.submit(线程 );
}
}