线程 进程 多线程
线程:独立的执行路径,一个进程可以有多个线程,比如视频中同时听声音,看图像,看弹幕等。线程是CPU调度和执行的基本单位。
多线程:多条执行路径,主线程与子线程并行交替执行(普通方法只有主线程一条路径)
程序:指令和数据的有序集合,本身没有任何运行的含义,是一个静态的概念
进程:在操作系统中运行的程序就是进程,即执行程序的一次执行过程,是一个动态的概念
概述
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,GC线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行是由调度器(cpu)安排调度的,调度器是与操作系统紧密相- 关的,先后顺序是不能人为干预的
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
- 很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
线程创建
继承Thread类(重点)
- 自定义线程类继承
Thread类 - 重写其
run()方法,编写线程执行体 - 创建线程对象,调用
start()方法启动线程
语法格式
//线程不一定立即执行,由CPU安排调度
public class TestThread extends Thread{ //定义一个继承Thread类的TestThread类
@Override
public void run() {
...//run方法线程体,包含线程执行的代码
}
public static void main(String[] args) {
TestThread [线程对象名] = new TestThread(); //创建一个线程对象
[线程对象名].start(); //start()开启线程
...//可以创建多个线程对象,并一一开启线程
...//也可以直接写一段代码,作为main主线程代码执行
}
//运行结果并发执行
}
示例(图片下载)
package com.kuang.demo01; //声明了代码所在的包名为com.mine.demo01
import org.apache.commons.io.FileUtils;
// ↑导入org.apache.commons.io包下的FileUtils工具类,这里被用来从URL下载文件并保存到本地
import java.io.File;
// ↑导入Java标准库中的File类。用于文件和目录的创建、查找等操作
import java.io.IOException;
// ↑导入Java标准库中的IOException异常类。表示输入/输出操作中可能发生的异常。这里被用于捕获和处理下载文件时可能出现的IO异常
import java.net.URL;
// ↑导入Java标准库中代表一个统一资源定位符的URL类。表示一个特定的网络地址,如文件路径、Web页面地址等。这里被用于创建一个指向网络图片地址的URL对象
//定义一个类TestThread,该类继承了Thread类,表示这是一个线程
public class TestThread extends Thread{
private String url;//用于存储网络图片地址
private String name;//用于存储保存的文件名
//定义一个构造函数,接受两个参数:网络图片地址,保存的文件名
public TestThread(String url,String name){
this.url = url; //将传入的url赋值给类的url变量
this.name = name; //将传入的name赋值给类的name变量
}
@Override //标记下面的方法重写了Thread类中的run方法
public void run(){ //下载图片线程的执行体
//创建了一个新的WebDownloader对象,用于下载图片
WebDownloader webDownloader = new WebDownloader();
//调用WebDownloader对象的downloader方法,传入url和name参数,下载图片
webDownloader.downLoader(url,name);
System.out.println("下载了文件名为:"+name); //打印已下载的图片文件名
}
public static void main(String[] args) { //主函数开始,程序的入口点
//创建三个TestThread线程对象,并分别传入不同的url和name参数
TestThread thread1 = new TestThread("https://img-blog.csdnimg.cn/img_convert/d8885c9a178b2fcaea732190717b516d.png", "1.jpg");
TestThread thread2 = new TestThread("https://img-blog.csdnimg.cn/img_convert/d8885c9a178b2fcaea732190717b516d.png", "2.jpg");
TestThread thread3 = new TestThread("https://img-blog.csdnimg.cn/img_convert/d8885c9a178b2fcaea732190717b516d.png", "3.jpg");
thread1.start(); //启动第一个线程
thread2.start(); //启动第二个线程
thread3.start(); //启动第三个线程
//结果并没有按顺序执行,且每次运行结果不一样,这是因为线程的执行顺序是不确定的
}
}
class WebDownloader{
//定义一个下载方法,用于下载指定url的图片到指定文件中
public void downLoader(String url,String name){
try {
//使用Apache Commons IO库的FileUtils类的copyURLToFile方法来下载图片
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) { //如果发生IOException,则打印堆栈跟踪并输出错误消息
e.printStackTrace();
System.out.println("IO 异常,Downloader方法出现问题");
}
}
}
//可能的输出结果包括:
1. 图片按任意顺序下载完成
2. 如果在下载过程中出现异常(例如网络中断或磁盘空间不足),则相应的异常信息会被打印出来
实现Runnable 接口(重点)
- 自定义线程类实现
Runnable接口 - 实现
run()方法,编写线程执行体 - 创建线程对象,调用
start()方法启动对象
优先使用:与继承Thread类实现多线程相比,使用Runnable接口更加灵活,因为一个类可以实现多个接口,而只能继承一个父类。此外,使用Runnable接口也更符合Java的“组合优于继承”的设计原则。
语法格式
第一种
Thread [线程对象名] = new Thread(new 类名); //创建一个新的线程对象,传入实现了Runnable接口的类的实例
[线程对象名].start(); // 启动线程,间接调用了run()方法
第二种
Thread [线程对象名] = new Thread(new Runnable() { //创建线程对象[线程对象名]
@Override
public void run() { //定义一个实现了Runnable接口的匿名内部类,并重写了run()方法
//线程执行的代码
}
});
[线程对象名].start(); // 启动线程
第三种
//仅适用于创建只需要执行一次并且不需要后续操作的线程
[类名] [类对象名] = new [类名]() //创建实现了Runnable接口的类的对象
new Thread(类对象名).start(); // 使用该对象创建一个新的线程,并开始
第四种
[类名] [类对象名] = new [类名]()
new Thread([类对象名],"[新线程的名字]").start();
//如果你需要存储线程引用以供后续操作(例如,停止线程,检查线程状态等),那么通常会创建一个实现了
//Runnable接口的类,并传递其实例给Thread构造函数。这样,你可以将Thread对象存储在某个变量中,
//以便以后使用————第一二种
示例一(买车票)
//声明一个实现Runnable接口的类TestThread4,表示这个类可以作为线程来运行
public class TestThread4 implements Runnable{
private int ticketNums = 10; //票数
@Override
public void run() {
while (true){
if(ticketNums<=0) break; //表示线程会不断尝试购买火车票,直到票数为0
try { //模拟延时
Thread.sleep(200); //sleep是Thread类中的一个静态方法,用于使当前线程休眠
//200是传递给sleep方法的参数,表示休眠的时间,单位是毫秒
} catch (InterruptedException e) {
e.printStackTrace(); //printStackTrace()方法用于输出关于异常的完整跟踪信息
}
System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"张票");
//Thread.currentThread()是Java中Thread类的一个静态方法,返回表示当前线程的Thread对象
//.getName()是Thread类的一个方法,返回表示该线程的名称的字符串
//ticketNums--是一个后减操作,意味着先返回ticketNums的值,然后再将其减1。
}
}
public static void main(String[] args) {
TestThread4 ticket = new TestThread4(); //创建了TestThread4类的一个实例
new Thread(ticket,"小明").start();
//创建了一个新的线程,该线程的名字是小明,并使用ticket实例作为运行对象。然后调用start()方法来启动这个线程
new Thread(ticket,"老师").start();
new Thread(ticket,"黄牛党").start();
}
}
详解new Thread(ticket,"小明").start();
new Thread(...): 这是Java的Thread类的一个构造方法,用于创建一个新的线程对象ticket,"小明": 这是传递给Thread构造方法的参数ticket: 这是传递给线程的第一个参数,通常是一个实现了Runnable接口的对象。在这个场景中,ticket是一个TestThread4类的实例,该类实现了Runnable接口,所以它可以作为线程运行的代码主体"小明": 这是传递给线程的第二个参数,通常是一个字符串,用于给线程命名。这里,我们给线程命名为“小明”
.start(): 这是启动新线程的方法。当调用这个方法时,线程会从它的run方法开始执行。注意,你不能直接调用run()方法,因为这会像普通方法一样执行,而不是在新的线程中执行
程序存在问题:
多个线程操作同一个资源,没有使用同步机制来确保对共享资源(票数)的访问是安全的,线程不安全,数据紊乱
示例二(龟兔赛跑)
public class Race implements Runnable{
private static String winner; //胜利者
@Override
public void run() {
for (int i = 1; i <= 100; i++) { //循环从1到100,模拟比赛的每一步
boolean flag=gameOver(i);//调用gameOver方法判断比赛是否结束
if(flag) break;
System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
//模拟兔子休息
if(Thread.currentThread().getName().equals("兔子")&& i%10==0){
//.equals("兔子") 是String类的一个方法,用于比较两个字符串是否相等。这里检查当前线程的名字是否是“兔子”
//i%10==0 检查i是否是10的倍数
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//判断是否完成比赛
private boolean gameOver(int steps){
if(winner!=null){ //如果已经有胜利者(即winner不为null),则返回true,表示比赛已经结束
return true;
}{
if (steps==100){
//如果当前步骤数为100,则设置当前线程的名字为胜利者,并打印胜利者的名字,然后返回true
winner=Thread.currentThread().getName();
System.out.println("winner is"+winner);
return true;
}
}
return false; //上面的条件都不满足,则返回false,表示比赛还没有结束
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"乌龟").start();
}
}
实现Callable 接口(了解)
实现Callable接口,需要返回值类型
重写call方法,需要抛出异常
创建目标对象
创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
提交执行:Future result1 = ser.submit(11);
获取结果:boolean r1 = result1.get()
关闭服务:ser.shutdownNow();
好处
- 可以定义返回值
- 可以抛出异常
示例(图片下载)
import java.util.concurrent.*; // 导入Java的并发工具包,提供了线程池等并发相关的工具
public class TestCallable implements Callable<Boolean> { //声明一个实现了Callable接口的TestCallable公共类,并指定返回值的类型为Boolean
private String url;//网络图片地址
private String name;//报错扥文件名
//有参构造
public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}
//下载图片线程的执行体
public Boolean call() throws Exception {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable c = new TestCallable("https://img-home.csdnimg.cn/images/20201124032511.png", "1.png");
TestCallable c1 = new TestCallable("https://img-home.csdnimg.cn/images/20201124032511.png", "2.png");
TestCallable c2 = new TestCallable("https://img-home.csdnimg.cn/images/20201124032511.png", "3.png");
//创建执行服务,使用Executors工具类的newFixedThreadPool方法创建一个固定大小的线程池,大小为3
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> r = ser.submit(c); //将TestCallable对象c提交到线程池执行,返回一个Future对象r,用于获取执行结果
Future<Boolean> r1 = ser.submit(c1);
Future<Boolean> r2 = ser.submit(c2);
//获取结果
boolean res = r.get();
//使用Future对象r的get方法获取执行结果,并存储在变量res中。这里会阻塞直到结果可用
boolean res1 = r1.get();
boolean res2 = r2.get();
System.out.println(res);
System.out.println(res1);
System.out.println(res2);
//关闭服务,ExecutorService 接口中的方法,用于尝试停止所有正在执行的活动任务,并返回等待执行的任务列表
ser.shutdownNow();
}
}
//class WebDownloader在前面下载图片已经定义了,这里就不用再次写,直接使用就好
静态代理模式
概述:
真实对象和代理对象都要实现同一个接口
代理对象要代理真实角色
好处:
代理对象可以做很多对象做不了的事情
真实对象专注做自己的事情
代理模式: 代理模式是一种设计模式,其中一个类代表另一个类的功能。代理模式可以用于控制对对象的访问,例如远程代理(代表远程对象)、虚拟代理(代表一个大型对象或耗时操作)等。
当将多线程与代理模式结合时,可以创建一种代理,该代理可以并发地处理多个请求。每个请求可以由一个单独的线程处理,这可以提高并发处理能力。
如何实现Java多线程静态代理:
- 定义接口: 首先,定义一个接口,该接口表示你要代理的服务。
public interface MyService {
void doSomething();
}
- 创建被代理类: 实现上面定义的接口,并提供实现。
public class MyServiceImpl implements MyService {
@Override
public void doSomething() {
// 实现业务逻辑
}
}
- 创建代理类: 这个类将实现与被代理类相同的接口,并使用多线程来处理请求。
import java.util.concurrent.*;
public class MyServiceProxy implements MyService {
private final ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
private final MyService myService;
public MyServiceProxy(MyService myService) {
this.myService = myService;
}
@Override
public void doSomething() {
executor.submit(() -> { // 使用线程池执行任务
myService.doSomething(); // 调用被代理类的业务逻辑
});
}
}
- 使用:
现在,你可以创建
MyServiceProxy的实例并使用它,它将使用多线程来调用MyServiceImpl。
注意:这里使用静态代理是因为代理类和被代理类有相同的接口。如果你使用动态代理(通过Proxy类),那么代码会有所不同。
- 注意事项: 使用多线程和代理模式时,需要考虑线程安全和异常处理。例如,确保共享资源在使用前被正确地初始化,以及在出现异常时能够正确地关闭线程池等。
示例
public class StaticProxy {
public static void main(String[] args) {
/*You you = new You();
you.HappyMarry();*/ //原本方式,下面交给代理
//线程类代理,实际调用了Runnable接口中的run方法
new Thread(()-> System.out.println("我爱你")).start();
//创建了一个WeddingCompany对象,并传入一个You对象作为参数。然后调用其HappyMarry方法
WeddingCompany company = new WeddingCompany(new You());
company.HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
class You implements Marry{ //真实角色
@Override
public void HappyMarry() {
System.out.println("我今天要结婚了!");
}
}
class WeddingCompany implements Marry{ //代理角色
//定义了一个私有成员变量target,类型为Marry(即真实对象的接口)
private Marry target; //target:真实对象。定义一个私有成员变量target,其类型为Marry
public WeddingCompany(Marry target) { //接受一个Marry类型的参数并将其赋值给私有成员变量
this.target = target;
}
@Override
public void HappyMarry() { //实现代理
before();
this.target.HappyMarry(); //调用真实对象的方法
after();
}
private void after() {
System.out.println("善后工作!");
}
private void before() {
System.out.println("婚前布置!");
}
}
总结:程序通过使用代理模式为婚礼添加了前后的额外操作(布置和善后)。原本的“我要结婚了!”消息被包装在代理的“婚前布置!”和“善后工作!”之间。
分析new Thread(()-> System.out.println("我爱你")).start();
这行代码创建了一个新的线程,并在这个线程中执行一个Lambda表达式打印出“我爱你”这个字符串。
具体来说,这行代码做了以下几件事:
new Thread(): 创建一个新的Thread对象,这是Java中创建线程的标准方式()-> System.out.println("我爱你"): 这是一个Lambda表达式,用于定义线程的执行任务。在这个例子中,Lambda表达式只做了一件事,就是打印字符串“我爱你”到控制台。.start(): 调用Thread对象的start方法,这会启动新创建的线程,并开始执行上面定义的Lambda表达式中的任务 代理类中实现代理的接口方法:
before():在真实对象的方法调用之前执行某些操作。这里它打印“婚前布置!”
this.target.HappyMarry():调用真实对象的HappyMarry方法。这意味着我们在这里将调用真实对象的方法。这是代理的主要功能之一,即在调用真实对象的方法之前或之后添加额外的操作或逻辑。在这个例子中,我们在调用真实对象的方法之前和之后都打印了一些信息
after():在真实对象的方法调用之后执行某些操作。这里它打印“善后工作!”
线程状态
线程五大状态
线程方法
| 方法名 | 功能 | 说明 |
|---|---|---|
| start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException |
| run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 |
| void join() | 等待该线程运行结束 | |
| join(long n) | 等待线程运行结束,最多等待 n 毫秒 | |
| getId() | 获取线程长整型的 id | id 唯一 |
| getName() | 获取线程名 | |
| setName(String) | 修改线程名 | |
| getPriority() | 获取线程优先级 | |
| setPriority(int newPriority) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 |
| getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
| isInterrupted() | 判断是否被打断, | 不会清除 打断标记 |
| boolead isAlive() | 测试线程是否处于活动状态(未运行完毕) | |
| void interrupt() | 中断线程,不推荐使用这个方式 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记 |
| interrupted() | static,判断当前线程是否被打断 | 会清除 打断标记 |
| Thread.currentThread() | static,获取当前正在执行的线程的引用 | |
| static void sleep(long n) | static,让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | |
| static void yield() | 暂停当前正在执行的线程对象,并执行其它线程 | 主要是为了测试和调试 |
线程停止
- 建议线程正常停止—>设置次数限制,不建议死循环
- 建议使用标志位(flag)来控制程序的执行流程。通过设置一个布尔类型的变量(通常命名为
flag或shouldContinue),在程序中的某个点检查该变量的值,并根据需要决定是否继续执行后续的代码 - 不要使用stop和destroy 等过时或者jdk不建议使用的方法
示例
public class Demo15_StopThread implements Runnable {
private boolean flag = true; //设置一个标志位
@Override
public void run() { //Runnable 接口要求的方法。当线程启动时,这个方法会被调用
int i = 0;
while (flag) { //只要 flag 为 true,它就会持续打印 "run...Thread" 后跟一个递增的整数
System.out.println("run...Thread" + i++);
}
}
//设置一个公开的方法转换标志位,允许外部调用以更改 flag 的值,控制线程停止
public void stop() {
this.flag = false;
}
public static void main(String[] args) {
Demo15_StopThread Stop = new Demo15_StopThread();
new Thread(stop).start(); //使用实例stop作为参数创建了一个新的线程并启动它
for (int i = 0; i < 1000; i++) {
System.out.println("main..." + i);
//当循环变量 i 达到900时,它会调用 stop.stop() 来将标志位 flag 设置为 false,从而终止线程
if (i == 900) {
Stop.stop(); //调用 Stop 对象上的 stop() 方法
System.out.println("该线程停止了");
}
}
}
}
线程休眠
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep 存在异常InterruptedException
- sleep 时间达到后线程进入就绪状态
- sleep 可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
语法格式
//表示执行到此句,则使当前线程暂停执行 n 毫秒(n是整数)
//Thread 类是 Java 中的一个预定义类,位于 java.lang 包中,因此不需要定义
Thread.sleep(n);
示例
package com.mine.demo05;
import java.text.SimpleDateFormat; //导入SimpleDateFormat类,用于日期格式化
import java.util.Date; //导入Date类,用于获取当前系统时间
//模拟倒计时,模拟时钟,模拟网络延时(买火车票例子)
public class TestSleep2 {
public static void main(String[] args){
try { //1.模拟倒计时
tenDown(); //调用tenDown方法,开始10秒倒计时
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印当前系统时间
Date startTime = new Date(System.currentTimeMillis()); //获取当前时间,并存储在startTime变量中
while (true){ //开始一个无限循环,模拟时钟
try {
Thread.sleep(1000); //每隔1s
//使用SimpleDateFormat格式化startTime为"HH:mm:ss"格式,并打印
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis()); //更新当前时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void tenDown() throws InterruptedException {
int num = 10; //初始化一个变量num为10,表示倒计时从10开始
while (true){ //开始一个无限循环,进行倒计时
Thread.sleep(1000); //当前线程休眠1000毫秒(即1秒)
System.out.println(num--);
if(num<=0) break;
}
}
}
线程礼让
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转化为就绪状态
- 让CPU重新调度,但不一定礼让成功
语法格式
//表示执行到此句,则提示给操作系统当前线程愿意让出CPU使用权,让其他线程有机会运行,不保证一定能切换线程
//Thread 类是 Java 中的一个预定义类,位于 java.lang 包中,因此不需要定义
Thread.yield();
示例
public class ThreadYield {
public static void main(String[] args) {
TestYield testYield = new TestYield();
//创建新的Thread对象,testYield 是一个实现了 Runnable 接口的类的对象
//这意味着它可以作为线程的“运行对象”。当线程启动时,run 方法会被调用,"a" 是新线程的名字
new Thread(testYield,"a").start();
new Thread(testYield,"b").start(); //同上
}
}
class TestYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始"); //打印当前线程的名称和"线程开始"
//调用Thread类的yield方法,使当前线程“礼让”,表示它愿意放弃对处理器的时间片,使得其他线程可以运行
//但实际上,是否真的能让出处理器取决于操作系统的调度器
Thread.yield();//线程礼让
System.out.println(Thread.currentThread().getName()+"线程结束");
}
}
//礼让成功的输出
a线程开始
b线程开始
a线程结束
b线程结束
线程插队
- Join合并线程,待线程执行完成后,在执行其他线程,其他线程堵塞,相当于插队
语法格式
//表示执行到此句,则中止正在的线程,开始执行[线程对象名]的线程直到执行完毕
[线程对象名].join();
示例
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 1000; i++) {
System.out.println("线程vip来了"+i);//打印一条消息,表示线程正在运行,并显示当前的计数器值
}
}
public static void main(String[] args) throws InterruptedException {//表示该方法可能会抛出InterruptedException异常
TestJoin testJoin = new TestJoin();
//使用前面创建的testJoin实例创建一个新的线程,并将其引用赋值给变量thread
Thread thread = new Thread(testJoin);
thread.start(); //启动thread线程
//主线程
for (int i = 0; i <= 500; i++) {
//如果当前计数器的值是200,则thread线程先执行完毕再执行主线程
if(i==200){
thread.join();//插队
}
System.out.println("main"+i);
}
}
}
//输出结果
main0
...
main199
线程vip来了0
...
线程vip来了999
main200
...
线程状态观测
通过 Thread 类中的 getState() 方法来获取线程的状态。这个方法返回一个表示线程状态的 Thread.State 枚举值
Thread.State是一个枚举类型,用于表示线程的状态。它是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种状态:
- NEW 尚未启动的线程处于此状态
- RUNNABLE 在Java虚拟机中执行的线程处于此状态
- BLOCKED被阻塞等待监视器锁定的线程处于此状态
- WAITING 正在等待另一个线程执行特定动作的线程处于此状态
- TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。
语法格式
//获取[线程对象名]的线程的状态,并将该状态存储在名为[变量名]的Thread.State类型变量中
Thread.State [变量名] = [线程对象名].getState();
//检查或者判断线程状态是否为RUNNABLE
state == Thread.State.RUNNABLE //而不是 state == RUNNABLE,同理还有state != Thread.State.TERMINATED
//而打印一个Thread.State类型的变量则仅输出6个枚举常量之一
System.out.println(state) //RUNNABLE
示例
//这个类的目的是观察线程的状态
public class TestState {
public static void main(String[] args) throws InterruptedException {
//创建一个新的线程,并将一个Lambda表达式作为其运行对象。这个Lambda表达式定义了线程应该执行的代码
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000); //每次循环,线程休眠1秒(1000毫秒)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("///");
});
//观察状态
Thread.State state = thread.getState(); //获取新创建的线程的状态并赋值给state变量
System.out.println(state); //NEW。此时线程还未启动,所以它的状态是NEW
thread.start(); //启动 Run()方法
state = thread.getState();
System.out.println(state); //RUNNABLE
//只要线程不终止
while (state != Thread.State.TERMINATED){
Thread.sleep(1000);
state = thread.getState(); //每隔1s,打印一次线程状态
System.out.println(state);
}
//thread.start();死亡之后的线程不能再次启动,报错
}
}
//输出结果
NEW
RUNNABLE
TIMED_WAITING //在线程内部的循环中,调用Thread.sleep(1000)方法,使线程进入TIMED_WAITING状态,表示线程正在休眠或等待一段时间
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TERMINATED //循环5次后,线程任务结束,进入TERMINATED状态
线程优先级
Java提供一个线程调度器来监控程序中已启动处于就绪状态的所有线程,优先级会影响线程的调动顺序,优先级越高的线程被优先调动的概率越大
Java 线程优先级使用 1 ~ 10 的整数表示
特殊的:
- 最低优先级 1:Thread.MIN_PRIORITY
- 最高优先级 10:Thread.MAX_PRIORITY
- 普通优先级 5(默认):Thread.NORM_PRIORITY
优先级若设置范围在 1-10 之外,则抛出 java.lang.IllegalArgumentException 异常
若要为线程设置优先级,则先设置后启动
若在主线程main中创建线程,且没有显式地指定线程名称,则线程名称是"main"
使用 getPriority方法 获取线程优先级的语法格式
//返回运行此代码的线程的优先级
Thread.currentThread().getPriority();
//返回[线程对象名]的线程的优先级
[线程对象名].getPriority();
//以上代码等同于整数,可以赋给变量或打印
使用 setPriority方法 设置线程优先级的语法格式
//设置运行此代码的线程的优先级为 n
Thread.currentThread().setPriority(n);
//设置[线程对象名]的线程的优先级为 n
[线程对象名].setPriority(n);
getPriority方法 与 setPriority方法 的示例
public class TestPriority{
public static void main(String[] args) {
//main 默认优先级 5
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
t1.start(); //Thread-0 默认优先级 5
t2.setPriority(Thread.MAX_PRIORITY); //最大优先级
t2.start(); //先设置优先级,再启动
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}
//可能的输出
main-->5
Thread-0-->5
Thread-1-->10
或
main-->5
Thread-1-->10
Thread-0-->5
守护线程
定义:一种特殊的后台线程,其主要作用是为其他线程提供服务。其在后台执行一些不需要用户交互的任务,并且可以在所有非守护线程执行完毕后自动关闭
- 线程分为用户线程和守护线程(daemon)
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 主要用于执行一些辅助任务,如,后台记录操作日志,监控内存,垃圾回收等待…
语法格式
//DaemonTask 是一个实现了 Runnable 接口的类
Thread [线程对象名] = new Thread(new DaemonTask());
[线程对象名].setDaemon(true);
//DaemonThread 是一个继承了 Thread 类的类
DaemonThread [线程对象名] = new DaemonThread();
[线程对象名].setDaemon(true);
示例
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
//上帝是守护线程,用户线程结束自己也结束
Thread thread = new Thread(god);
thread.setDaemon(true); //设置为守护线程 默认是false(表示用户线程)
thread.start(); //守护线程启动
new Thread(you).start(); //用户线程启动
}
}
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝保佑着你");
}
}
}
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你度过了开心的第"+i+"天");
}
System.out.println("-==goodbye,world!==-");
}
}
线程同步
定义:一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前一个线程使用完毕,下一个线程才能使用
形成条件:队列+锁
并发:同一对象被多个线程同时操作(如,抢票)
锁机制
原因:同一进程的多个线程共享同一块存储空间,这带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized
作用:每个对象都有把锁,当一个线程获得对象的排它锁、独占资源时,其他线程必须等待当前线程执行完毕后释放锁,才能接着执行
存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
不安全的线程示例
public class Demo24_UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
new Thread(buyTicket, "张三").start();
new Thread(buyTicket, "李四").start();
new Thread(buyTicket, "王五").start();
}
}
class BuyTicket implements Runnable {
private int ticketNums = 10; //初始化票数
boolean flag = true;
@Override
public void run() { //计划买票
while (flag) {
try {
buy();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void buy() { //进行买票
if (ticketNums <= 0) { //判断是否有票
flag = false;
return;
}
try { //延迟
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--); //买票
}
}
分析:
程序不安全的原因:存在竞态条件。竞态条件指发生在两个或更多的线程同时访问共享数据,并且至少有一个线程在修改共享数据时。由于线程调度的不确定性,因此可能导致结果不可预测。最常见的竞态条件是“先检测后执行”,即是否执行由检测的结果决定,而检测结果又被多个线程的执行结果影响 时。
在这段代码中,ticketNums是一个共享资源,被多个线程(张三、李四、王五)同时访问和修改。当多个线程几乎同时进入buy()方法时,它们可能会同时检查ticketNums的值,然后都发现票数大于0,于是都继续执行买票操作。这样就会导致票数被多次减少,甚至出现负数,这显然是不合理的。
synchronized 关键字
同步块和同步方法里面有需要修改的内容才需要锁
同步方法
语法格式
[修饰符] synchronized void [方法名](){}
每个对象都有自己的锁,当一个线程进入一个被synchronized修饰的方法时,它首先会尝试获取该方法所属类的 对象的锁,并且在该线程执行完该方法之前,其他线程无法获得该对象的锁;如果该锁已经被其他线程持有,那么当前线程将被阻塞,直到其他线程执行完毕,从而避免了多线程并发访问造成的数据不一致问题。
- 缺陷:若将一大段的方法申明为synchronized将会影响效率
安全的线程示例
public class SafeBuyTicket {
public static void main(String[] args) {
BuyTicket1 buyTicket = new BuyTicket1();
new Thread(buyTicket, "张三").start();
new Thread(buyTicket, "李四").start();
new Thread(buyTicket, "王五").start();
}
}
class BuyTicket1 implements Runnable {
private int ticketNums = 10; //初始化票数
boolean flag = true;
@Override
public void run() { //计划买票
while (flag) {
try {
buy();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private synchronized void buy() { //synchronized 同步方法,锁的是调用当前方法的对象
if (ticketNums <= 0) { //判断是否有票
flag = false;
return;
}
try { //延迟
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--); //买票
}
}
同步块
synchronized 实现跨对象的同步:首先需要选择一个合适的对象作为锁对象。这个对象通常是 被保护的共享资源 所在的对象,或者是与多个对象相关的对象。然后,在同步块的代码中使用这个对象作为锁对象。最后,需要被安全访问的对象以需要的方式放在同步块中。
- 共享资源指多个线程同时对同一份资源进行访问(读写操作)的数据或对象
- 锁对象又叫同步监视器
语法格式
public class [类名] {
private Object [锁对象] = new Object();
private Object [保护对象1] = new Object();
private Object [保护对象2] = new Object();
public void [方法名]() {
synchronized([锁对象]) {
// 同步块,访问 [保护对象1] 和 [保护对象2]
// ...
}
}
}
当一个线程进入 [方法名]() 方法时,它将获取 [锁对象] 的锁,并执行同步块 synchronized(锁对象) { ... } 中的代码,以安全地访问[保护对象1]和[保护对象2]对象。其他线程将被阻塞,直到当前线程退出同步块并释放锁。
示例
public class MyClass {
private Object lock = new Object();
private Object obj1 = new Object();
private Object obj2 = new Object();
public void accessObjects() {
synchronized(lock) {
System.out.println("访问 obj1"); // 访问 obj1
System.out.println("访问 obj2"); // 访问 obj2
}
}
public static void main(String[] args) {
MyClass myClass = new MyClass();
Thread thread1 = new Thread(() -> { // 创建两个线程并启动它们
myClass.accessObjects();
});
Thread thread2 = new Thread(() -> {
myClass.accessObjects();
});
thread1.start();
thread2.start();
}
}
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
测试Java并发(JUC)库中的安全类型的集合
public class ThreadJuc {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
分析:
Java并发包java.util.concurrent中的一个类CopyOnWriteArrayList的使用
- 泛型 <String>:
CopyOnWriteArrayList<String>:这里,CopyOnWriteArrayList是一个泛型类,专门用来存储String类型的元素。泛型是Java的一个功能,它允许为类、接口或方法定义一个类型参数,使得代码更加灵活和可重用。
- CopyOnWriteArrayList 简介:
CopyOnWriteArrayList是线程安全的,它是java.util.ArrayList的一个线程安全变种。当列表的内容被修改(例如,通过add,set,remove等操作)时,它会创建列表的一个新副本,而不是在原有的列表上进行修改。这样可以确保在迭代列表时不会抛出ConcurrentModificationException异常。- 当你只读取列表而不修改它时,
CopyOnWriteArrayList不会进行任何复制操作,因此读取操作是非常高效的。但是,由于每次修改都会导致复制整个列表,所以写操作可能会相对较慢。
- 为什么使用 CopyOnWriteArrayList:
- 在多线程环境中,当你有一个线程在遍历列表的同时,另一个线程试图修改列表(例如,添加或删除元素),这可能会导致并发问题。
CopyOnWriteArrayList就是为了解决这个问题而设计的。 - 它特别适用于读操作远多于写操作的场景,因为在这种情况下,读操作的性能不会受到写操作的影响。
- 示例用途:
- 假设你有一个Web应用程序,它需要处理大量用户的请求。在处理每个请求时,你可能需要从某个列表中获取一些数据。由于请求处理是并发的,使用
CopyOnWriteArrayList可以确保数据的线程安全性。即使有多个线程同时访问和修改这个列表,你的代码也不需要额外的同步开销。
总的来说,CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();这行代码创建了一个线程安全的字符串列表,可以在多线程环境中安全地使用。
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程释放各自占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
理解:
- 张三占用着A资源,等着B资源;李四占用着B资源,等着A资源
- 张三、李四都只有同时完成AB资源才能完成任务,但是两个人在对方放手之前都不会先放手
- 于是,进入了死锁
产生死锁的四个必要条件:
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
只要破除任意一个就能避免死锁
示例
//解决:一个锁只锁一个对象
public class DeadLock {
public static void main(String[] args) {
Makeup makeup = new Makeup(0, "灰姑娘");
Makeup makeup1 = new Makeup(1, "白雪公主");
makeup.start();
makeup1.start();
}
}
class Lipstick { } //口红
class Mirror { } //镜子
class Makeup extends Thread {
static Lipstick lipstick = new Lipstick(); //需要的资源只有一份,用static保证只有一份
static Mirror mirror = new Mirror();
int choice; //选择
String girlName; //使用化妆品的人
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup(); //化妆
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { //获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) { //一秒钟后想获得镜子
System.out.println(this.girlName + "获得镜子的锁");
}
}
} else {
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) { //二秒钟后想获得的锁
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}
分析
关键点在于,两个线程试图以不同的顺序获取相同的锁。这就导致了经典的死锁场景:
- “灰姑娘”线程可能首先获得口红的锁。
- 同时,“白雪公主”线程可能首先获得镜子的锁。
- 当“灰姑娘”线程试图获取镜子的锁时,它必须等待,因为“白雪公主”已经持有这个锁。
- 同样,当“白雪公主”线程试图获取口红的锁时,它也必须等待,因为“灰姑娘”已经持有这个锁。
这样,两个线程就会永远地等待对方释放锁,从而导致了死锁。
解决
要避免这种死锁,有几种常见的策略:
- 锁顺序:总是以相同的顺序请求锁。(最简单)
- 锁超时:设置获取锁的超时时间。如果线程在指定的时间内无法获取锁,就放弃并稍后重试。
- 死锁检测:使用专门的算法或工具来检测并恢复死锁。但这种方法通常比较复杂,且可能导致性能问题。
- 资源分级:给资源分配不同的优先级,并规定只有持有较低优先级锁的线程才能请求较高优先级的锁。这可以避免循环等待,从而防止死锁。
Lock 锁
通过显示定义同步锁对象(Lock)来实现同步,同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
语法格式
主函数 { 根据[类名A]创建线程,并启动 }
class [类名A] implements Runnable {
ReentrantLock [锁名] = new ReentrantLock();
[锁名].lock(); // 获取锁
try {
// 执行需要同步的代码块
} finally {
lock.unlock(); // 释放锁
}
示例
//测试Lock锁
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock = new TestLock2(); //创建实现Runnable接口的类的对象,意味着可以被线程执行
new Thread(testLock).start();
new Thread(testLock).start();
new Thread(testLock).start();
}
}
class TestLock2 implements Runnable {
int tickerNums = 10;
//定义Lock锁,定义了一个ReentrantLock类型的私有、不可变的变量lock,并立即初始化它。这是一个互斥体,用于确保在任何时刻只有一个线程可以执行某个代码块。
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) { //加锁
try {
lock.lock(); //尝试获取锁。如果锁当前被另一个线程持有,则当前线程将被禁用,直到锁被释放
if (tickerNums <= 0) {
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tickerNums--);
} catch (Exception e) {
e.printStackTrace();
} finally { //解锁.无论前面的代码是否成功或抛出异常,`finally`块都会被执行。在这里,它确保锁总是被释放,从而防止死锁
lock.unlock();
}
}
}
}
区别
- 显式与隐式:Lock锁是显式锁,需要手动获取和释放;而synchronized关键字是隐式锁,无需手动获取和释放,可以自动管理。
- 适用场景:Lock锁只有代码块锁;而synchronized关键字有代码块锁和方法锁
- 属性:
Lock锁是Java并发包java.util.concurrent.locks中提供的接口,而synchronized关键字是Java语言的一部分 - 功能:
Lock锁提供了更灵活的锁控制,例如可以中断等待锁的线程、尝试获取锁等,更适合用在复杂的同步场景。而synchronized关键字则比较简单,一旦一个线程获得了锁,其他线程就必须等待 - 性能:在重度竞争情况下,Lock锁可能具有更好的性能。
优先使用顺序: Lock锁 > 同步代码块 > 同步方法
线程协作
线程通信——生产者-消费者模式
应用场景:生产者-消费者模式 (生产者 → 数据缓存区 → 消费者)
- 生产者与消费者共享一个资源,同时生产者与消费者相互依赖互为条件
- 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
- 如果仓库中放有产品,则消费者将产品取出消费,否则停止消费并等待,直到仓库中再次放入产品为止
分析
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
对于生产者,在生产开始之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者取出
对于消费者,在消费完毕之后,要通知生产者消费已结束,需要生产新的产品以供消费
在生产者消费者问题中,仅有synchronized是不够的 。synchronized 可阻止并发更新同一个共享资源以实现同步,但不能用来实现不同线程之间的消息传递(通信)
达到通信效果主要方法
| 方法名 | 作用 |
|---|---|
| wait() | 先释放锁,并开始等待,直到其他线程通知 |
| wait(n) | 释放锁并等待 n 毫秒 |
| notify() | 唤醒一个处于等待状态的线程 |
| notifyAll() | 唤醒同一个对象上所有调用了wait()方法的线程,优先级高的优先调度 |
唤醒:将线程从等待状态变为可运行状态
这都是Object类的方法,只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException
管程法
设置一个缓冲区,用于暂存数据
管程法示例
// 测试:生产者消费者模型-->利用缓冲区解决:管程法
import java.util.LinkedList;
import java.util.Queue;
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer(); //创建一个SynContainer对象,作为共享缓冲区
//双线程
new Producer(container).start(); //创建生产者线程,并启动。生产者将使用提供的container对象
new Consumer(container).start(); //创建消费者线程,并启动。消费者也将使用提供的container对象
}
}
//定义生产者线程类。生产者线程将创建产品并将其放入缓冲区
class Producer extends Thread{
SynContainer container; //声明一个 SynContainer 类的变量 container
public Producer(SynContainer container) { //构造器的声明,创建Producer对象
this.container = container; //将传入的参数container 赋值给当前对象的成员变量container
}
@Override
public void run() { //生产者线程的执行主体。其将创建100个产品,并将其放入缓冲区
for (int i = 0; i < 100; i++) {
System.out.println("生产了第" + i + "件产品");
container.push(new Products(i)); //利用push方法将新创建的产品放入缓冲区
}
}
}
//定义消费者线程类。消费者线程将从缓冲区取出产品
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {//消费者线程的执行主体。其将消费缓冲区中的所有产品
for (int i = 0; i < 100; i++) {
System.out.println("消费了第" + container.pop().id + "件产品"); //打印从缓冲区中取出的产品id
}
}
}
class Products{ //定义产品类,包含一个整数id
int id;
public Products(int id) {
this.id = id;
}
}
class SynContainer{ //定义同步缓冲区类,生产者和消费者通过这个类来进行交互
//初始化一个空的队列来存储产品:创建一个 LinkedList 对象,并将其引用赋值给一个类型为 Queue<Products> 的变量 queue
Queue<Products> queue = new LinkedList<Products>();
int count = 0; //初始化一个计数器,用于跟踪队列中产品的数量
int size = 10; //设置队列的最大容量为10
//定义同步方法,允许一个线程在修改队列时独占访问权。如果队列已满,生产者线程将被阻塞,直到有空间可用
public synchronized void push(Products product) //生产者放入产品
{
if(count == size)
//容器满,等待消费者消费
{
try {
this.wait(); //生产者线程调用wait()方法释放锁并进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//容器未满,需要存入成品,并唤醒消费者
count++;
queue.offer(product);
this.notifyAll();
}
//定义同步方法,允许一个线程在修改队列时独占访问权。如果队列为空,消费者线程将被阻塞,直到有产品可用
public synchronized Products pop()
{
if (count == 0)
//容器为空,消费者线程调用wait()方法并进入等待状态,等待生产者生产
{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//容器有产品可用,消费者线程被唤醒以取出物品,并通知等待的生产者线程可以生产了
count--;
Products productPoll = queue.poll();
this.notifyAll();
return productPoll;
}
}
分析
P57Queue<Products> queue = new LinkedList<Products>(); 创建了一个新的 LinkedList 对象,并将其引用赋值给一个类型为 Queue 的变量 queue。
这里是对代码的逐部分解释:
-
泛型
<Products>:
Queue<Products>表示这是一个队列,该队列中的元素类型是 Products。泛型允许为类、接口或方法指定一个类型参数,这样可以确保添加到队列中的对象是 Products 类型或其子类型。 -
变量声明 :
Queue<Products> queue:声明了一个名为 queue 的变量,其类型为 Queue。这意味着这个变量可以存储一个队列,其中每个元素都是 Products 类型。 -
实例化 :
new LinkedList<Products>():这部分代码创建了一个新的 LinkedList 对象,该对象是 Queue 接口的一个实现。LinkedList 是一个双向链表,通常用作队列的实现。 -
赋值 :
通过使用=操作符,新创建的 LinkedList 对象被赋值给 queue 变量。
之后,可以使用 queue 变量来执行队列操作,例如添加元素(使用 add 或 offer 方法)、移除元素(使用 remove 或 poll 方法)等。由于这是一个队列,元素将按照它们被添加的顺序(先进先出,FIFO)被移除。
P75~P77
count++;queue.offer(product); this.notifyAll(); 对这段代码的逐句解释:
count++:将 count 变量的值增加1。 count 变量通常用于跟踪队列中当前有多少个元素queue.offer(product):将一个 product 对象添加到队列的末尾。这里使用的是 offer 方法,offer方法尝试将元素添加到队列中,如果队列已满,它会立即返回一个特殊的标志值,而不是抛出异常this.notifyAll():是线程同步的关键部分。它调用 Object 类的 notifyAll 方法,该方法将唤醒所有正在等待该对象的线程。在这种情况下,这意味着如果有任何生产者线程或消费者线程正在等待(因为它们在调用 wait() 方法后被阻塞),它们现在将被唤醒并继续执行
这段代码的作用是:当一个生产者线程成功地向队列中添加了一个产品时,它会通知所有等待的线程(无论是其他生产者还是消费者),以便它们可以继续执行。这确保了生产者和消费者线程之间的正确同步,防止了可能的竞态条件。
P93~P96
count--; Products productPoll = queue.poll(); this.notifyAll(); return productPoll;对这段代码的逐句解释:
count--;:将count的值减少1。count 可能是一个计数器,用于跟踪缓冲区中当前有多少个产品Products productPoll = queue.poll();:从队列中取出一个产品并赋值给 productPoll 变量。如果队列为空, poll() 方法将返回 nullthis.notifyAll();:唤醒在此对象监视器上等待的所有线程(如果有)。这通常是为了通知任何等待的生产者或消费者线程,产品已经被取出或消费,他们现在可以继续执行。poll() 是 Java 并发库中BlockingQueue接口的一个方法。它尝试从队列中移除并返回 头部的元素。如果队列为空,它会立即返回 null。这个方法是非阻塞的,也就是说,如果队列为空,它不会等待元素可用,而是立即返回。return productPoll;:这行代码返回取出的产品。在消费者线程中,这会结束方法调用并返回给调用者
这段代码的作用是:确保在多线程环境中对共享资源(如缓冲区)的访问是安全的。通过使用 synchronized 关键字和方法内的 wait() 和 notifyAll() 方法,可以确保生产者和消费者线程不会相互干扰,从而避免产生竞态条件。
信号灯法
设置一个标记位(类似于容量为1的管程法)
信号灯法示例
// 测试:生产者消费者模型-->通过标志位解决:信号灯法
public class TestPC_01 {
public static void main(String[] args) {
TV tv = new TV(); //创建一个 TV 对象,是生产者和消费者的共享资源
//创建线程并启动,将 tv 作为参数传递给 Player\Watcher 构造函数。tv 变量被初始化或存储为线程的一个属性
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者-->演员
class Player extends Thread { //定义一个的类 Player ,该类继承自 Thread 类,表示这是一个线程
TV tv; //定义类型为 TV 的成员变量 tv
public Player(TV tv) {
this.tv = tv; //定义一个构造函数,接受一个类型为 TV 的参数,并将其赋值给成员变量
}
@Override
public void run() { //定义线程的执行逻辑
//循环播放两个节目:当i为偶数时播放"快乐大本营播放中",当i为奇数时播放"抖音:记录美好生活"。循环总共执行20次
for (int i = 0; i < 20; i++) {
if (i % 2 == 0) {
this.tv.play("快乐大本营播放中");
} else {
this.tv.play("抖音:记录美好生活");
}
}
}
}
//消费者-->观众
class Watcher extends Thread {
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
//这个循环会执行20次。每次循环都会调用 tv.watch() 方法。这意味着这个线程会反复地观看某个节目,至少观看20次
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品-->节目
class TV {
//演员表演,观众等待 true
//观众观看,演员等待 fault
String voice; // 表演的节目
boolean flag = true;
//表示演员表演节目。该方法内部使用了标志位来控制线程的执行顺序
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了:" + voice);
//通知观众观看
this.notifyAll(); //唤醒等待的线程
//将传入的 voice 参数值赋给类的成员变量 this.voice。这意味着当前线程更新了 TV 对象中的节目内容
this.voice = voice;
this.flag = !this.flag; //将 flag 的值取反,实现生产者和消费者之间的交替执行
}
//表示观众观看节目。该方法内部也使用了标志位来控制线程的执行顺序
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:" + voice);
//通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
线程池参数
corePoolSize:核心池的大小
maximumPoolSize: 最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止
JDK 5.0起提供了线程池相关API:ExecutorService 和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- <T>Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行 Callable
- void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
语法格式
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class [线程池类名] {
public static void main(String[] args) {
// 使用Executors.newFixedThreadPool()方法创建一个固定大小为 n 的线程池
//因为线程池大小是 n ,所以同时最多只能有 n 个任务在执行,其余任务会在队列中等待
ExecutorService [线程池对象名] = Executors.newFixedThreadPool(n);
//每次循环都会创建一个新的 [任务类名] 任务并提交给 [线程池对象名]
//因此,如果这个循环执行了 m 次,就会创建 m 个任务
for () {
Runnable [任务对象名] = new [任务类名](); // 括号()中是传递给 [任务类名] 构造函数的参数
[线程池对象名].execute([任务对象名]); // 使用execute方法提交 任务[任务对象名] 给线程池
}
[线程池对象名].shutdown(); // 关闭线程池,不再接受新的任务,但还是会等待已提交的任务完成后再关闭
//这个循环会阻塞主线程,当所有提交给线程池的任务都执行完毕isTerminated 方法会返回 true
while (![线程池对象名].isTerminated()) {
}
System.out.println("所有线程执行完毕"); //当所有任务都执行完毕后,打印一条消息到控制台
}
}
class [任务类名] implements Runnable {
... //其他代码
@Override
public void run() { //实现 Runnable 接口的 run 方法,这是线程执行时的入口点
... //run方法线程体,包含线程执行的代码
}
<---------------------------------------有时会用到------------------------------------------>
// 模拟耗时操作,使线程睡眠 n 毫秒,以确保所有任务都完成
private void processCommand() {
try {
Thread.sleep(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//作用:
/**1.在多线程编程中,线程间的同步和协作是很重要的。通过模拟耗时操作,开发者可以更好地理解线程如何等待、
* 如何被唤醒以及如何进行同步。
* 2.在某些情况下,可能希望通过模拟耗时操作来控制并发的程度。例如,不希望所有线程都同时完成,
* 而是希望它们按照一定的顺序一个接一个地完成。
*/
}
分析
P8
ExecutorService 是 Java 中用于执行多线程任务的框架之一,可以创建一个线程池,将多个任务提交到线程池中执行。
P8
Executors.newFixedThreadPool() 是Java中 java.util.concurrent.Executors 类的一个静态方法,用于创建一个固定大小的线程池。线程池是用于管理线程的一种机制,它能够控制同时运行的线程数量,并提供了线程的复用,从而避免了为每个任务都创建新线程的开销。
P17
shutdown() 方法是 ExecutorService 接口的一部分,该接口是 java.util.concurrent 包的一部分。当你创建一个 ExecutorService 实例时(例如通过 Executors 工厂类),你可以调用其 shutdown() 方法来关闭执行器服务。这会停止接受新的任务,但会等待已提交的任务完成后再关闭。
P20
isTerminated() 是 ExecutorService 接口中的一个方法。这个方法返回一个布尔值,表示 ExecutorService 是否已经终止。如果 ExecutorService 已经完成执行所有提交的任务并且所有线程都已终止,那么 isTerminated() 将返回 true 。如果 ExecutorService 还在执行任务或仍然有活动的线程,那么 isTerminated() 将返回 false 。
示例
public class ThreadPool {
public static void main(String[] args) {
// 这里创建了一个固定大小为10的线程池,并将其引用赋给变量service(注意:通常变量名应使用小写开头,这里service不符合Java的命名规范)。
// 所有提交给这个线程池的任务将由这10个线程来处理,这意味着即使你提交了超过10个任务,也只有10个线程会同时运行,
// 其他任务会在队列中等待,直到有线程空闲出来。
// newFixedThreadPool 参数为:线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
// 通过调用service的execute方法,向线程池提交了四个任务。每个任务都是一个myThread类的实例
// execute 方法是 Java 的 ExecutorService 接口中的一个方法,用于将任务添加到线程池的工作队列中,并由一个线程来执行
// 当任务被提交时,线程池中的一个线程会被用来执行该任务。如果所有线程都在忙,任务会进入队列等待
service.execute(new myThread());
service.execute(new myThread());
service.execute(new myThread());
service.execute(new myThread());
service.shutdown(); // 关闭线程池,不再接受新的任务;当所有已提交的任务都执行完毕,线程池中的所有线程将会终止
}
}
class myThread implements Runnable { // 实现了 Runnable 接口的类 myThread
@Override
public void run() { // 当线程池中的一个线程开始执行myThread实例时,它会调用这个run方法
System.out.println(Thread.currentThread().getName()); // 获取并打印当前正在执行的线程的名称
}
}