(笔记)多线程
线程、进程、多线程
定义
当讨论计算机编程时,线程(Thread)和进程(Process)是两个重要的概念。
进程是指计算机中运行的程序的一个实例。每个进程都有自己的内存空间和系统资源,并且在操作系统中以独立的实体存在。进程之间是相互独立的,它们无法直接访问彼此的内存空间。进程通常由操作系统创建和管理,可以执行自己的代码,并且可以与其他进程进行通信。
线程是进程中的一个执行单元。一个进程可以拥有多个线程,这些线程共享进程的内存空间和资源。同一个进程中的线程可以通过共享内存进行通信和共享数据。在多线程编程中,线程可以独立执行不同的任务,使得程序可以同时处理多个任务,提高程序的效率和响应性。
多线程是指在一个程序中同时运行多个线程的技术。多线程编程可以提供更好的并发性和资源利用率。但同时,多线程编程也增加了编程的复杂性,需要更加谨慎地处理共享资源和线程同步的问题,以避免竞态条件和死锁等并发编程问题。
总结起来,进程是操作系统中执行的程序实例,而线程是进程的执行单元,多线程编程可以提高程序的并发性和资源利用率,但也增加了编程的复杂性。
- 线程就是弗里德执行路径
- 在程序运行的时候,即使没有用户自己创建线程,后台也会有多个线程(主线程/main线程,gc线程)
- main():主线程,为系统的入口,用于执行整个程序
- 在同一个进程中,如果开辟了多个线程,那么线程的运行有调度器(cpu)安排调度,调度器时与操作系统紧密相连的,先后顺序是不能人为的干预
- 对同一份资源进行操作的时候,会存在资源抢夺的问题,需要加入并发控制
多线程创建方式(三种)
Java Platform Standard Edition 8 Documentation (oracle.com)
- Thread Class:继承Thread类
- Runnable接口:实现Runnable接口
- Callable接口:实现Callable接口
Thread
- 自定义线程类并继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
代码01:显示多线程功能
package com.demo01;
/**
* TODO 创建线程方式01:继承Thread类并重写run()方法,调用start()方法启动线程
* 注意:线程开启了但并不意味着立即执行,需要待cpu调度
* @author pangyangjian
* @since 2023/8/16 10:52
*/
public class TextThread_1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
System.out.println("Thread_" + i);
}
}
public static void main(String[] args) {
TextThread_1 textThread1 = new TextThread_1();
textThread1.start();
for (int i = 0; i < 5000; i++) {
System.out.println("main_" + i);
}
}
}
代码02:多线程下载网络图片
package com.demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* TODO 实现多线程同步下载图片
* 注意:需要commons io这个jar包
* @author pangyangjian
* @since 2023/8/16 11:43
*/
public class TextThread_2 extends Thread {
private String url; // 下载地址url
private String fileName; // 下载到指定的文件夹的名字
public TextThread_2(String url, String fileName) {
this.url = url;
this.fileName = fileName;
}
@Override
public void run() {
WebPicDownloader webPicDownloader = new WebPicDownloader();
webPicDownloader.downloader(url, fileName);
System.out.println("fileName:" + fileName);
}
public static void main(String[] args) {
TextThread_2 t1 = new TextThread_2("https://bkimg.cdn.bcebos" +
".com/pic/0823dd54564e9258d1099db7c1c9c658ccbf6c81e01d?x-bce" +
"-process=image/watermark,image_d2F0ZXIvYmFpa2UxNTA=,g_7,xp_5," +
"yp_5/format,f_auto", "pic1.jpg");
TextThread_2 t2 = new TextThread_2("https://bkimg.cdn.bcebos" +
".com/pic/e61190ef76c6a7efce1b1e0c0db6b851f3deb48fc8ce?x-bce" +
"-process=image/format,f_auto", "pic2.jpg");
TextThread_2 t3 = new TextThread_2("https://th.bing.com/th/id/OIP" +
".wbeOtDbxc5LL8qwDqCHh-gHaEK?w=315&h=180&c=7&r" +
"=0&o=5&pid=1.7", "pic3.jpg");
t1.start();
t2.start();
t3.start();
}
}
class WebPicDownloader {
/**
* @param url:下载地址url
* @param fileName:下载后的图片名称
*/
public void downloader(String url, String fileName) {
try {
FileUtils.copyURLToFile(new URL(url), new File(fileName));
} catch (IOException e) {
e.printStackTrace();
System.out.println("io图片下载器出现异常");
}
}
}
Runnable
- 申明实现类Runnable接口
- 实现run()方法
- 通过分配类的实例,在创建Thread时作为参数进行传递并启动
多线程05:实现Runnable接口_哔哩哔哩_bilibili
代码01:显示多线程实现方法
package com.demo01;
/**
* TODO 创建线程方式2:实现Runnable接口,重写run()方法,执行线程需要传入Runnable接口实现类,最后再调用start方法
*
* @author pangyangjian
* @since 2023/8/16 14:17
*/
public class TextThread_3 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
System.out.println("Thread_" + i);
}
}
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TextThread_3 textThread3 = new TextThread_3();
// 创建线程实现类对象并通过start启动
new Thread(textThread3).start();
for (int i = 0; i < 5000; i++) {
System.out.println("main_" + i);
}
}
}
代码02:模拟抢票,表明多线程操作同一资源时容易出现数据错误的问题
package com.demo01;
/**
* TODO 多个线程同时操作同一个资源
* 注意:容易出现线程不安全,数据出现紊乱的问题
*
* @author pangyangjian
* @since 2023/8/16 15:30
*/
public class TextThread_4 implements Runnable {
private int tickets = 20;
@Override
public void run() {
while (tickets > 0) {
// 因为cpu运行速度很快,所以需要模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + tickets-- + "张票");
}
}
public static void main(String[] args) {
TextThread_4 textThread4 = new TextThread_4();
new Thread(textThread4, "小红").start();
new Thread(textThread4, "小黄").start();
new Thread(textThread4, "小蓝").start();
}
}
代码03:龟兔赛跑
package com.demo01;
/**
* TODO 模拟归途赛跑
*
* @author pangyangjian
* @since 2023/8/16 16:10
*/
public class TextThread_5 implements Runnable {
private volatile boolean gameOver = false;
@Override
public void run() {
for (int i = 0; i <= 200; i++) {
System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
if (!gameOver(i)) {
break;
}
if ("Rabbit".equals(Thread.currentThread().getName()) && i % 100 == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized boolean gameOver(int step) {
if (gameOver) {
return false;
}
if (step >= 200) {
System.out.println("the winner is:" + Thread.currentThread().getName());
gameOver = true;
return false;
}
return true;
}
public static void main(String[] args) {
TextThread_5 race = new TextThread_5();
new Thread(race, "Rabbit").start();
new Thread(race, "Turtle").start();
}
}
Callable
- 实现Callable接口,需返回值类型
- 重写call方法,需抛出异常
- 创建目标对象
- 创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(1) - 提交执行
Future<Boolean> result1 = ser.submit(t1) - 获取结果
boolean r1 = result1.get - 关闭服务
ser.shutdownNow()
多线程08:实现Callable接口_哔哩哔哩_bilibili
代码01(复写Thread代码02)
package com.demo01.TextCallable;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
/**
* TODO 类实现描述
*
* @author pangyangjian
* @since 2023/8/17 10:35
*/
public class TextCallable_1 implements Callable<Boolean> {
private final String url; // 下载地址url
private final String fileName; // 下载到指定的文件夹的名字
public TextCallable_1(String url, String fileName) {
this.url = url;
this.fileName = fileName;
}
@Override
public Boolean call() {
WebPicDownloader webPicDownloader = new WebPicDownloader();
webPicDownloader.downloader(url, fileName);
System.out.println("fileName:" + fileName);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TextCallable_1 t1 = new TextCallable_1("https://bkimg.cdn.bcebos" +
".com/pic/0823dd54564e9258d1099db7c1c9c658ccbf6c81e01d?x-bce" +
"-process=image/watermark,image_d2F0ZXIvYmFpa2UxNTA=,g_7," +
"xp_5," +
"yp_5/format,f_auto", "pic1.jpg");
TextCallable_1 t2 = new TextCallable_1("https://bkimg.cdn.bcebos" +
".com/pic/e61190ef76c6a7efce1b1e0c0db6b851f3deb48fc8ce?x-bce" +
"-process=image/format,f_auto", "pic2.jpg");
TextCallable_1 t3 = new TextCallable_1("https://th.bing.com/th/id/OIP" +
".wbeOtDbxc5LL8qwDqCHh-gHaEK?w=315&h=180&c=7&r" +
"=0&o=5&pid=1.7", "pic3.jpg");
// 1. 创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
// 2. 提交执行
Future<Boolean> result1 = ser.submit(t1);
Future<Boolean> result2 = ser.submit(t2);
Future<Boolean> result3 = ser.submit(t3);
// 3. 获取结果
boolean rs1 = result1.get();
boolean rs2 = result2.get();
boolean rs3 = result3.get();
// 4. 关闭服务
ser.shutdownNow();
}
}
class WebPicDownloader {
/**
* @param url:下载地址url
* @param fileName:下载后的图片名称
*/
public void downloader(String url, String fileName) {
try {
FileUtils.copyURLToFile(new URL(url), new File(fileName));
} catch (IOException e) {
e.printStackTrace();
System.out.println("io图片下载器出现异常");
}
}
}
小结
-
区别
Thread、Runnable 和 Callable 都是 Java 中用于实现多线程的机制,它们有以下区别:
-
实现方式:Thread 是一个类,它继承自 java.lang.Thread 类,并重写了 run() 方法来定义线程要执行的任务。Runnable 是一个接口,它定义了一个 run() 方法,需要在实现类中实现该方法来定义线程要执行的任务。Callable 也是一个接口,它定义了一个 call() 方法,需要在实现类中实现该方法来定义线程要执行的任务。与 Runnable 不同的是,call() 方法可以返回一个结果。
-
返回结果:Runnable 的 run() 方法没有返回值,因此无法直接获取线程任务的执行结果。Callable 的 call() 方法可以返回一个结果,可以通过 Future 对象获取该结果。
-
异常处理:在 Runnable 和 Callable 中,都可以在其任务执行过程中抛出异常。但是对于 Runnable,异常只能在任务内部进行处理,无法被外部捕获;而对于 Callable,异常可以由外部进行捕获和处理。
-
使用方式:创建线程时,Thread 可以直接使用,而 Runnable 和 Callable 必须作为参数传递给 Thread 来创建线程。
总结来说,Thread 是通过继承 Thread 类创建线程,Runnable 是通过实现 Runnable 接口创建线程,而 Callable 是通过实现 Callable 接口创建线程,并可以返回执行结果。Runnable 和 Callable 对于多线程任务的执行更加灵活且扩展性强,推荐在多线程开发中使用 Callable,特别是需要获取任务执行结果或需要对异常进行处理的情况下。
-
静态代理模式
定义
静态代理模式是设计模式中的一种结构型模式,它通过创建一个代理对象来控制对原始对象的访问。在静态代理模式中,代理类和原始类实现相同的接口,代理类包含一个对原始类的引用,在调用代理类的方法时,通过代理类间接地调用原始类的方法,并可以在方法调用前后进行一些额外的操作。
[多线程09:静态代理模式_哔哩哔哩_bilibili
以下是静态代理模式的主要角色:
- 抽象主题(Subject):定义了代理类和真实主题类的共同接口,可以是接口或抽象类。
- 真实主题(Real Subject):实现了抽象主题接口,是代理对象所代表的真实对象。
- 代理(Proxy):包含一个对真实主题的引用,并实现了抽象主题接口。代理对象可以在调用真实主题方法前后执行额外的操作。
静态代理模式的优点包括:
- 可以在不修改原始类的情况下增加额外的功能,使得系统更加灵活可扩展。
- 可以对客户端屏蔽真实主题的具体实现,客户端只关心与代理对象的交互。
- 可以实现对真实主题的访问控制和约束。
然而,静态代理模式的缺点是每个代理类只能为一个具体主题服务,如果需要代理多个主题类,就需要创建多个代理类,导致代码量增加。
请注意,静态代理模式是在编译时确定代理类和真实主题类的关系,因此称为静态。相对于静态代理模式,还有一种动态代理模式,它是在运行时根据需要动态生成代理类,提供更大的灵活性。
代码
package com.staticProxy;
/**
* TODO 静态代理 模拟人、婚庆公司、结婚
* real subject和proxy都需要实现同一个接口subject
* proxy必须代理real subject
* 好处:1.proxy可以做real subject做不了的事情;2.real subject可以专注做自己的事情
*
* @author pangyangjian
* @since 2023/8/17 11:47
*/
public class StaticProxy {
public static void main(String[] args) {
WeddingCompany weddingCompany = new WeddingCompany(new Person());
weddingCompany.marry();
}
}
/**
* TODO Subject
*
* @author pangyangjian
* @since 2023/8/17 11:47
*/
interface Marry {
void marry();
}
/**
* TODO Real Subject
*
* @author pangyangjian
* @since 2023/8/17 11:47
*/
class Person implements Marry {
@Override
public void marry() {
System.out.println("happy marry");
}
}
/**
* TODO Proxy
*
* @author pangyangjian
* @since 2023/8/17 11:47
*/
class WeddingCompany implements Marry {
// 代理的real subject
private final Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void marry() {
before();
this.target.marry(); //
after();
}
private void before() {
System.out.println("before getting merry");
}
private void after() {
System.out.println("after getting merry");
}
}
Lamda表达式
定义
Lambda表达式是Java 8引入的一种新的语法特性,用于支持函数式编程。它可以使代码更加简洁、易读,并且可以通过函数式接口来实现匿名函数的功能。
Lambda表达式由三部分组成:
- 形参列表(Parameters):指定在Lambda表达式中可以被使用的参数。
- 箭头(Arrow):由箭头"->"表示Lambda表达式的分隔符,将形参列表与Lambda体分隔开。
- Lambda体(Body):包含了Lambda表达式需要执行的功能,可以是一个表达式或一段代码块。
Lambda表达式的语法如下:
(parameters) -> expression
或
(parameters) -> { statements; }
Lambda表达式可以用于替代传统的匿名内部类,特别适用于函数式接口(只有一个抽象方法的接口)的实现。通过Lambda表达式,可以以非常简洁的方式传递行为和逻辑,使代码更加简洁、易读。
以下是Lambda表达式的一些使用场景:
- 作为函数式接口的实现:Lambda表达式可以直接作为函数式接口的实现,避免了编写冗余的匿名内部类代码。
- 作为参数传递:可以将Lambda表达式作为方法的参数传递,简化了回调函数的使用。
- 集合的遍历与操作:可以使用Lambda表达式对集合进行遍历和操作,提高了代码的简洁性。
Lambda表达式在Java中实现了函数式编程的一些核心思想,使得代码更加简洁、灵活,但需要注意在使用Lambda表达式时,要保证代码的可读性和维护性。
代码01(lambda推导,几种类的代码迭代优化)
package com.lambda;
/**
* TODO Lambda表达式推导
*
* @author pangyangjian
* @since 2023/8/17 15:22
*/
public class TextLambda {
/**
* @description 3.静态内部类
* @author pangyangjian
* @date 2023/8/17 15:53
*/
static class ILke1 implements ILike {
@Override
public void lambda() {
System.out.println("lambda 1");
}
}
public static void main(String[] args) {
final Like like = new Like();
like.lambda();
final ILke1 iLke1 = new ILke1();
iLke1.lambda();
/*
* @description 4.局部内部类
* @return void
* @author pangyangjian
* @date 2023/8/17 15:59
*/
class Like2 implements ILike{
@Override
public void lambda() {
System.out.println("lambda 2");
}
}
final Like2 like2 = new Like2();
like2.lambda();
/*
* @description 5.匿名内部类(没有类名,必须借助接口或父类才能实现)
* @return void
* @author pangyangjian
* @date 2023/8/17 16:05
*/
final ILike iLike3 = new ILike() {
@Override
public void lambda() {
System.out.println("lambda 3");
}
};
iLike3.lambda();
/*
* @description 6.lambda简化
* ()表达的是传参
* @return void
* @author pangyangjian
* @date 2023/8/17 16:09
*/
final ILike iLike4 = () -> System.out.println("lambda 4");
iLike4.lambda();
}
}
// 1.首先定义一个函数式接口(只有一个方法的接口)
interface ILike {
void lambda();
}
// 2.实现类
class Like implements ILike {
@Override
public void lambda() {
System.out.println("i like lambda");
}
}
代码02(使用lambda表达式创建一个Runnable)
package com.lambda;
/**
* TODO 使用lambda表达式创建一个Runnable
*
* @author pangyangjian
* @since 2023/8/17 16:41
*/
public class LambdaRunnableExample {
public static void main(String[] args) {
Runnable runnable = () -> {
for (int i = 0; i <= 10; i++) {
System.out.println("Running: " + i);
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
在这个示例中使用lambda表达式创建了一个匿名的Runnable对象,并在其中实现了run()方法。lambda表达式的格式为() -> {},其中() ->表示无参数,{}表示代码块。
然后,我们创建了一个线程,并将lambda表达式创建的Runnable对象传递给线程的构造方法。通过thread.start()方法启动线程,使其开始执行run()方法中的代码块。
在每次循环中,线程会打印"Running: "和当前的循环变量i的值。
线程状态
定义
线程状态是指线程在不同的阶段或情况下所处的状态。在多线程编程中,线程可以存在多个不同的状态,每个状态代表了线程在执行过程中的特定情况。不同的线程状态可以帮助我们理解线程的行为,并且可以通过线程状态的转换来控制线程的执行。
-
新建状态(New):线程对象被创建但还未调用start()方法时,线程处于新建状态。此时,线程初始化完成,但尚未开始执行。
-
可运行状态(Runnable):当线程调用start()方法之后,线程进入可运行状态。此时,线程可能正在运行,也可能正在等待CPU时间片来执行。处于可运行状态的线程有机会被系统调度为当前运行的线程。
-
运行状态(Running):线程在可运行状态下,被系统调度为当前运行的线程,并执行其中的代码。处于运行状态的线程正在执行自己的任务。
-
阻塞状态(Blocked):线程在某些情况下无法继续执行时,会进入阻塞状态。例如,线程可能因为等待某个锁、等待输入/输出完成或等待其他线程通知等而被阻塞。处于阻塞状态的线程在满足阻塞条件之前会一直处于等待状态。
-
等待状态(Waiting):线程在等待某个特定条件的情况下,会进入等待状态。例如,线程可能调用了wait()方法或某个对象的wait()方法来等待其他线程的通知。处于等待状态的线程需要其他线程的唤醒或通知才能继续执行。
-
计时等待状态(Timed Waiting):与等待状态类似,线程在等待一个特定时间的情况下,会进入计时等待状态。例如,线程可能调用了Thread.sleep(long millis)方法或调用了Object.wait(long millis)方法。处于计时等待状态的线程会在指定的时间过后自动唤醒或超时。
-
终止状态(Terminated):线程正常执行完run()方法或发生了未捕获的异常导致线程终止,进入终止状态。处于终止状态的线程不会再执行任何代码。
常见状态
停止线程
定义
线程的停止可以有多种定义,具体取决于应用程序的需求和设计。
-
完全停止:线程会立即停止执行,并彻底终止。这种方式可能导致一些资源无法正确释放,不建议使用。
-
软性停止:线程会在合适的时机正常退出。可以通过设置一个标志位,在适当的时候让线程自行退出执行。
-
优雅停止:线程会在合适的时机完成当前的工作,然后按照预定逻辑执行清理操作,并正常退出。
最常见的做法是使用标志位来控制线程的停止。例如,在线程的任务循环中,可以使用一个标志位来判断是否继续执行任务。当标志位被设置为false时,线程会在下一个循环迭代中退出循环,从而停止执行。
另外,还可以使用Thread.interrupt()方法发送中断信号给线程,让线程从阻塞状态(如Thread.sleep()、Object.wait()等)中被唤醒,然后按照预设逻辑进行停止处理。
无论使用哪种方式,都需要遵循以下原则:
- 确保线程的安全退出,不造成资源泄露或不稳定的状态。
- 尽量避免使用强制停止的方法,如
Thread.stop(),因为它可能会导致程序状态不一致或资源泄露。 - 良好的线程设计和停止策略可以提高程序的稳定性和可靠性。
需要根据具体的应用场景和需求,选择合适的线程停止方式。
代码(通过标志位反转的方式停止线程)
package com.thread.textThreadStop;
/**
* TODO 测试线程stop
* 1.建议线程正常停止运行
* 2.建议使用标志位
* 3.建议尽量不要使用stop、destroy等过时的方法
*
* @author pangyangjian
* @since 2023/8/17 17:26
*/
public class TextThreadStop implements Runnable {
// 公共标志位,用于停止线程
private boolean stopFlag = true;
@Override
public void run() {
int i = 0;
while (stopFlag) {
System.out.println("thread run " + i++);
}
System.out.println("thread stop run " + i);
}
/*
* @description 用于反转标志位
* @return void
* @author pangyangjian
* @date 2023/8/17 17:44
*/
public void ThreadStop() {
this.stopFlag = false;
}
public static void main(String[] args) {
final TextThreadStop textThreadStop = new TextThreadStop();
new Thread(textThreadStop).start();
for (int i = 0; i < 2000; i++) {
System.out.println("main thread run" + i);
if (i == 999) textThreadStop.ThreadStop();
}
}
}
线程休眠
定义
在线程中,可以使用Thread.sleep()方法使线程进入休眠状态。Thread.sleep()方法可以暂停线程的执行,以便其他线程有机会执行。
Thread.sleep()方法有两个重载版本:
- sleep(long millis):使当前线程休眠指定的时间(以毫秒为单位)。例如,Thread.sleep(1000)将使当前线程休眠1秒。
- sleep(long millis, int nanos):使当前线程休眠指定的时间(以毫秒和纳秒为单位)。例如,Thread.sleep(1000, 500000)将使当前线程休眠1秒500毫秒。
在调用Thread.sleep()方法时,需要处理InterruptedException异常。当其他线程调用当前线程的interrupt()方法时,当前线程的sleep()方法可能会抛出InterruptedException异常,以提醒当前线程被中断。
代码01(模拟网络延迟)
public class SleepExample {
public static void main(String[] args) {
System.out.println("Main thread starts");
try {
// 让主线程休眠3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread ends");
}
}
在上面的示例中,主线程调用Thread.sleep(3000)方法使自己休眠3秒。在休眠期间,其他线程有机会执行。一旦3秒过去,主线程会恢复执行,并打印出"Main thread ends"。
通过合理地使用Thread.sleep()方法,可以控制线程的执行顺序和时间间隔,从而实现多线程应用中的定时任务或其他需求。
代码02(模拟倒计时)
package com.thread.textThreadSleep;
/**
* TODO 线程休眠 模拟倒计时
*
* @author pangyangjian
* @since 2023/8/17 18:08
*/
public class TextThreadStop {
public static void main(String[] args) {
try {
tenDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void tenDown() throws InterruptedException {
int num = 10;
while (true) {
Thread.sleep(500);
System.out.println("time is " + num--);
if (num <= 0) {
System.out.println("time is over");
break;
}
}
}
}
在tenDown()方法中设置了一个初始值为10的变量num,然后使用一个无限循环来进行倒计时的逻辑。在每次循环中,线程通过调用Thread.sleep(500)方法来休眠500毫秒,然后打印当前的倒计时数字num。
每次循环结束后,倒计时数字num减1。当倒计时数字num减少到等于0或小于0时,表示倒计时结束,在控制台上打印"Time is over",然后跳出循环,结束倒计时。
这段代码中的Thread.sleep(500)方法可以使当前线程休眠指定的毫秒数。这里通过休眠500毫秒来模拟每秒钟倒计时一次。
需要注意的是,Thread.sleep()方法会抛出InterruptedException异常,所以方法签名上需要声明throws InterruptedException。
线程礼让
定义
线程礼让是指一个线程在执行过程中,主动暂停自己的执行,让其他优先级相同或更高的线程先执行。通过线程礼让,可以提高程序的效率和公平性,避免某个线程长时间占用CPU资源。
在Java中,可以使用Thread.yield()方法来实现线程的礼让。当一个线程调用Thread.yield()时,它会暂停自己的执行,并将CPU资源让给其他线程。
具体来说,当一个线程调用Thread.yield()时,它会进入就绪状态,然后等待调度器重新选择下一个要执行的线程。这个过程是由操作系统的线程调度器来管理的,具体的调度策略可能因操作系统而异。
需要注意的是,调用Thread.yield()并不保证立即让出CPU资源,它仅是给线程调度器一个提示,表示当前线程愿意让出执行权。实际上,调用Thread.yield()之后,仍然有可能被选择为下一个要执行的线程。
线程礼让并不是必需的,通常只在特定的场景中使用。例如,在编写长时间运行的循环时,可以通过在适当的位置调用Thread.yield()来避免长时间的占用CPU资源,以便让其他线程有机会执行。
需要注意的是,尽管线程礼让可以提高程序的效率和公平性,但过度的使用可能会导致线程切换过于频繁,降低程序的性能。因此,在使用线程礼让时需要谨慎评估和调整。
代码
package com.thread.textThreadYield;
/**
* TODO 模拟线程礼让
*
* @author pangyangjian
* @since 2023/8/17 19:01
*/
public class TextThreadYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" is running");
if ("a".equals(Thread.currentThread().getName())) {
System.out.println(Thread.currentThread().getName()+" is read fpr yield");
Thread.yield();
System.out.println(Thread.currentThread().getName()+" yield success");
}
}
}
强制执行
定义
join()方法是多线程编程中的一个方法,用于强制等待一个线程的执行完成。当一个线程调用另一个线程的join()方法时,它将被阻塞,直到被调用的线程执行完毕。
语法:
thread.join()
参数说明:
- 无参数:调用线程将等待被调用的线程执行完毕。
- 带有超时参数:可以传入一个超时时间,指定最长等待时间,如果在指定的时间内被调用的线程没有执行完毕,调用线程将从阻塞状态返回。
作用:
- 实现线程之间的协调与同步:在某个线程中调用另一个线程的
join()方法,可以确保调用线程等待被调用的线程执行完毕,实现线程间的协调与同步。 - 等待子线程执行完毕:通常在主线程中创建并启动多个子线程时,通过在主线程中调用每个子线程的
join()方法,可以确保主线程在所有子线程执行完毕后再继续执行后续代码。
需要注意的是,join()方法会导致当前线程阻塞,因此需要在适当的时机调用以避免阻塞过长时间或造成死锁等问题。
代码
package com.thread.textThreadJoin;
/**
* TODO 线程强制执行join 模拟
*
* @author pangyangjian
* @since 2023/8/18 16:37
*/
public class TextThreadJoin implements Runnable {
@Override
public void run() {
System.out.println("textThreadJoin star");
for (int i = 0; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " run " + i);
}
}
public static void main(String[] args) throws InterruptedException {
TextThreadJoin textThreadJoin = new TextThreadJoin();
Thread thread = new Thread(textThreadJoin, "textThreadJoin");
thread.start();
for (int i = 0; i <= 20; i++) {
System.out.println("main thread run " + i);
if (i == 10) {
thread.join();
}
}
}
}
线程状态观测
方法
- 使用编程语言提供的线程管理工具:不同编程语言提供了不同的线程管理工具和API,可以通过这些工具来获取线程的状态信息。例如,在Java中,可以使用
Thread类的方法来获取线程状态,如getState()方法。 - 输出日志信息:在代码中适当的位置使用日志系统打印线程的状态信息,如开始执行、暂停、终止等。这样可以观察线程的状态变化,并根据日志信息进行分析和判断。
- 使用线程调试工具:许多集成开发环境(IDE)和调试工具提供了线程状态的监控和调试功能。通过这些工具,可以查看线程的调用栈、局部变量等信息,以及观察线程的状态变化。例如,在Java中,可以使用Eclipse、IntelliJ IDEA等IDE的调试工具来观测线程状态。
- 使用操作系统提供的工具:操作系统通常提供一些工具,如命令行工具或图形界面工具,可以观测和管理线程。例如,在Unix/Linux系统中,可以使用命令行工具
top、ps等来查看进程和线程的状态信息。
代码(方法1)
package com.thread.textThreadObservation;
/**
* 线程观测
*
* @author pangyangjian
* @since 2023/8/31 12:13
*/
public class textThreadObservation {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 观测线程状态
Thread.State state = thread.getState();
System.out.println(state);
// 观察线程启动后的状态
thread.start();
System.out.println();
while(thread.getState() != Thread.State.TERMINATED){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(thread.getState());
}
}
}
线程优先级
定义
线程优先级是操作系统或编程语言提供的一种机制,用于确定在多线程环境下各个线程被调度执行的顺序和优先级。线程优先级可以影响线程的调度顺序,高优先级的线程有更高的几率被操作系统或调度器选择执行。
线程优先级通常由整数或枚举类型表示,具体定义和范围可能因操作系统和编程语言而异。以下是一般性的定义和常见的线程优先级范围:
-
最低优先级(Lowest Priority):表示最低的线程优先级,该优先级的线程通常在其他线程都处于阻塞或等待状态时才会被调度执行。在某些系统中,该优先级可能被表示为0或较大的正整数。
-
低优先级(Low Priority):表示低优先级的线程,该优先级的线程在竞争其他优先级较高的线程时会有较低的执行几率。在某些系统中,该优先级可以是整数范围中的一个较小值。
-
正常优先级(Normal Priority):表示正常的线程优先级,并且通常是默认的优先级。在某些系统中,该优先级可能被表示为中等范围内的整数。
-
高优先级(High Priority):表示较高的线程优先级,这些线程在竞争其他优先级较低的线程时有更高的执行几率。在某些系统中,该优先级可以是整数范围中的一个较大值。
-
最高优先级(Highest Priority):表示最高的线程优先级,该优先级的线程拥有最高的执行优先级,通常会优先被调度执行。在某些系统中,该优先级可能被表示为整数的最大值。
代码
package com.thread.textThreadPriority;
/**
* 观测线程优先级
*
* @author pangyangjian
* @since 2023/8/31 14:19
*/
public class textThreadPriority {
public static void main(String[] args) {
// main thread
System.out.println(Thread.currentThread().getName() + "-->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
Thread t5 = new Thread(myPriority);
// 启动前先设置线程的优先级
t1.start();
t2.setPriority(2);
t2.start();
t3.setPriority(6);
t3.start();
t4.setPriority(Thread.MIN_PRIORITY);
t4.start();
t5.setPriority(Thread.MAX_PRIORITY);
t5.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->"+Thread.currentThread().getPriority());
}
}
守护线程
定义
守护线程(Daemon Thread)是一种在操作系统后台运行的线程,它的生命周期与程序或应用的主线程(非守护线程)相关联。当主线程结束运行时,守护线程也会被终止。
守护线程通常被用于执行一些后台任务或提供服务的工作,而不需要干预用户交互或程序主要逻辑的执行。它们在程序运行过程中默默地工作,以支持其他非守护线程的运行。
守护线程具有以下特点:
- 生命周期与主线程相关联:当主线程结束运行时,守护线程也会被强制终止,不会阻塞程序的退出。
- 后台执行:守护线程通常在后台默默执行任务,不会对用户交互或程序主要逻辑产生影响。
- 具有低优先级:守护线程通常在优先级上低于其他非守护线程,以防止它们抢占主线程和其他关键线程的执行时间。
守护线程在许多场景中都有用途。例如,垃圾回收器(Garbage Collector)是一个常见的守护线程,它在后台清理不再使用的内存。此外,后台任务的执行、日志记录、定时任务等也可以使用守护线程来实现。
代码
package com.thread.threadState.textDaemon;
/**
* 观测守护线程
* God Protect You
*
* @author pangyangjian
* @since 2023/8/31 15:55
*/
public class TextDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
class God implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("god is protecting you");
}
}
}
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("survival success" + i + "day");
}
System.out.println("dead");
}
}
线程同步
定义
线程同步(Thread Synchronization)是指在多线程环境中,为了确保多个线程按照一定的顺序、协调和合作执行,而需要采取的一系列机制和技术。
在并发编程中,多个线程同时访问和修改共享资源可能会导致一些问题,例如数据竞争(Data Race)、死锁(Deadlock)、活锁(Livelock)等。线程同步的目的是解决这些问题,确保线程之间正确、有序地共享和访问共享资源。
常用的线程同步机制有以下几种:
- 互斥锁(Mutex):提供了对共享资源的互斥访问,保证同一时间只有一个线程可以访问共享资源。
- 信号量(Semaphore):用于控制对共享资源的访问权限,可以设置为多个线程同时访问共享资源的数量。
- 条件变量(Condition Variable):用于线程之间的通信和协作,通过等待和唤醒机制实现线程间的同步。
- 读写锁(Read-Write Lock):在共享资源很多读操作和少量写操作的场景下,提供了更高效的线程同步机制。
- 屏障(Barrier):用于将多个线程分成多个阶段执行,确保每个线程在某个阶段完成之前都必须等待。
线程同步机制的正确使用可以解决并发编程中的很多问题,但过度的同步也可能导致性能下降。因此,在设计和实现多线程程序时,需要仔细考虑同步机制的选择和使用,并进行适当的性能优化。
代码01 BuyTicket
package com.thread.synchronization;
/**
* @description 抢票
* @author pangyangjian
* @date 2023/9/1 11:13
*/
public class safeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"张三").start();
new Thread(station,"李四").start();
new Thread(station,"王二麻子").start();
}
}
class BuyTicket implements Runnable {
int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
// 模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
buy();
}
}
private void buy() {
if (ticketNum <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + " get " + ticketNum--);
}
}
通过向线程操作数据的方法添加synchronized的方法,来实现启用线程锁
// synchronized 同步方法,锁的是this
private synchronized void buy() {
if (ticketNum <= 0) {
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + " get " + ticketNum--);
}
代码02 Withdraw money
package com.thread.synchronization;
/**
* @author pangyangjian
* @description 取钱
* @since 2023/9/1 11:28
*/
public class safeWithoutDrawMoney {
public static void main(String[] args) {
// 账户
Account account = new Account(100, "基金");
Bank man = new Bank(account, 50, "man");
Bank woman = new Bank(account, 100, "woman");
man.start();
woman.start();
}
}
class Account {
int money;
String userName;
public Account(int money, String userName) {
this.money = money;
this.userName = userName;
}
}
class Bank extends Thread {
final Account account;
int drawingMoney; // 取值
int nowMoney; // 手里的钱
public Bank(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
// 判断是否取钱
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + " money is not enough");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
account.money -= drawingMoney;
nowMoney += drawingMoney;
System.out.println(this.getName() + " 取钱:" + drawingMoney);
System.out.println(account.userName + " 余额:" + account.money);
// System.out.println(this.getName()+Thread.currentThread().getName());
System.out.println(this.getName() + " 手里的钱:" + nowMoney);
}
}
通过添加同步块synchronized(Obj){}的方式解决问题,其中Obj被称作同步监视器
// 锁的对象就是变化的量
synchronized (account) {
// 判断是否取钱
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + " money is not enough");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
account.money -= drawingMoney;
nowMoney += drawingMoney;
System.out.println(this.getName() + " 取钱:" + drawingMoney);
System.out.println(account.userName + " 余额:" + account.money);
// System.out.println(this.getName()+Thread.currentThread().getName());
System.out.println(this.getName() + " 手里的钱:" + nowMoney);
}
锁
死锁(synchronization)
定义
死锁(Deadlock)是指在多进程或多线程系统中,每个进程(或线程)都在等待其他进程(或线程)释放资源,导致所有进程(或线程)都无法继续执行的一种状态。
死锁通常涉及到两个或多个进程(或线程)互相持有对方所需的资源,并且在等待对方释放资源的同时,又拒绝释放自己持有的资源。这种情况下,所有进程(或线程)都无法继续执行,形成了死锁。
死锁通常发生在以下情况下:
-
互斥条件(Mutual exclusion):资源只能被一个进程(或线程)同时占用,当一个进程(或线程)占用了一个资源,其他进程(或线程)无法使用该资源。
-
不可抢占条件(Non-preemption):已经被一个进程(或线程)占用的资源只能由持有者释放,其他进程(或线程)无法强行抢占。
-
占有和等待条件(Hold and Wait):一个进程(或线程)在等待其他进程(或线程)的资源同时继续持有自己已经获得的资源。
-
循环等待条件(Circular wait):系统中存在一个进程(或线程)的资源请求序列形成了一个循环,其中每个进程(或线程)在该循环中等待下一个进程(或线程)所持有的资源。
当这四个条件同时满足时,就可能出现死锁。死锁会导致系统无响应,进程(或线程)无法继续执行下去,需要通过适当的死锁检测和死锁解除机制来处理死锁情况,以恢复系统的正常运行。
代码
package com.thread.synchronization;
/**
* 死锁观测
*
* @author pangyangjian
* @since 2023/9/1 15:53
*/
public class DeadlockExample {
private static final Object RESOURCE1 = new Object();
private static final Object RESOURCE2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (RESOURCE1) {
System.out.println("Thread 1: Holding resource 1...");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (RESOURCE2) {
System.out.println("Thread 1: Holding resource 1 and resource 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (RESOURCE2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (RESOURCE1) {
System.out.println("Thread 2: Holding resource 2 and resource 1...");
}
}
});
thread1.start();
thread2.start();
}
}
在上述示例中,我们有两个线程(thread1和thread2),它们都试图获取两个资源(RESOURCE1和RESOURCE2)。
当thread1开始执行时,它先获取RESOURCE1,然后在执行一段时间后,试图获取resource2。但是,在此期间,thread2已经获取了RESOURCE2,并且正在等待获取resource1。
同样,当thread2开始执行时,它先获取RESOURCE2,然后在执行一段时间后,试图获取RESOURCE1。但是,在此期间,thread1已经获取了RESOURCE1,并且正在等待获取RESOURCE2。
这样,两个线程都在等待对方释放资源,导致了死锁的发生。如果你运行这段代码,你会发现程序无法终止,并且没有输出结果。
注意:为了演示死锁,我们在代码中使用了Thread.sleep方法来模拟执行时间。在实际开发中,请避免在锁内部执行长时间的操作,以免影响程序的性能和响应性。
synchronized (RESOURCE1) {
System.out.println("Thread 1: Holding resource 1...");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
System.out.println("Thread 1: Waiting for resource 2...");
}
synchronized (RESOURCE2) {
System.out.println("Thread 1: Holding resource 1 and resource 2...");
}
synchronized (RESOURCE2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
System.out.println("Thread 2: Waiting for resource 1...");
}
synchronized (RESOURCE1) {
System.out.println("Thread 2: Holding resource 2 and resource 1...");
}
Lock锁
定义
锁(Lock)是用于多线程编程中的同步机制。它是确保程序中的共享资源在同一时间只能被一个线程访问的重要工具。
通常,锁有两种主要类型:
-
内部锁(Intrinsic Lock)或监视器锁(Monitor Lock):它是Java中内置的同步机制,通过
synchronized关键字来使用。当线程进入synchronized块时,它会尝试获取对象的内部锁,如果锁可用,线程将获得该锁并执行代码。如果锁已被另一个线程持有,线程将被阻塞直到锁释放。在Java中,每个对象都有一个内部锁,线程可以获取该对象的内部锁来同步对对象数据的访问。 -
显式锁(Explicit Lock):它是Java中的一种高级同步机制,通过
java.util.concurrent.locks包中的Lock接口及其实现类来实现。显式锁提供了比内部锁更多的控制和灵活性。可以使用lock()方法获取锁,并使用unlock()方法释放锁。与内部锁相比,显式锁提供了更多功能,如可中断锁(可被其他线程中断等待锁)、定时锁(锁等待一定时间后自动释放)以及公平锁(按照线程等待时间的先后顺序获取锁)等。
显式锁的定义(Lock接口的定义)如下:
public interface Lock {
void lock(); // 获取锁
void unlock(); // 释放锁
boolean tryLock(); // 尝试获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 尝试在指定时间内获取锁,并返回是否获取成功
Condition newCondition(); // 获取与锁相关联的条件
}
总之,锁是一种同步机制,用于在多线程环境下控制对共享资源的访问。内部锁和显式锁是两种常见类型的锁,它们都有助于保护共享资源的一致性和线程安全性。
代码
package com.thread.synchronization;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @description Lock锁观测
* @author pangyangjian
* @date 2023/9/1 16:18
*/
public class LockExample {
private final Lock lock = new ReentrantLock(); // 创建一个可重入锁对象
public void performTask() {
lock.lock(); // 获取锁
try {
// 在这里执行需要同步的代码块
System.out.println("Performing task...");
Thread.sleep(2000); // 模拟执行时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
LockExample example = new LockExample();
// 创建多个线程并发执行任务
for (int i = 1; i <= 5; i++) {
Thread thread = new Thread(() -> {
example.performTask();
});
thread.start();
}
}
}
小结
当涉及到多线程编程时,锁(Lock)和同步(Synchronization)都是用来控制对共享资源的访问的机制。它们在实现上有一些区别和特点。
-
实现方式:
- 锁:使用显式锁(Explicit Lock),通过
java.util.concurrent.locks.Lock接口及其实现类来实现。在代码中,我们需要显式地调用lock()方法获取锁,并在适当的时候调用unlock()方法释放锁。 - 同步:使用内部锁(Intrinsic Lock)或监视器锁(Monitor Lock),通过
synchronized关键字来实现。在代码中,通过在方法或代码块上使用synchronized关键字,我们隐式地使用内部锁。
- 锁:使用显式锁(Explicit Lock),通过
-
灵活性:
- 锁:显式锁提供了更多的灵活性,例如可中断锁(可被其他线程中断等待锁)、定时锁(锁等待一定时间后自动释放)以及公平锁(按照线程等待时间的先后顺序获取锁)等。我们可以根据实际需求选择合适的锁操作。
- 同步:内部锁相对较简单,提供的同步机制是固定的,没有过多的选择和灵活性。
-
代码结构:
- 锁:使用显式锁时,我们可以通过代码中的
lock()和unlock()方法明确了解到锁的位置和范围,这使得代码的同步控制更加清晰和可见。 - 同步:使用内部锁时,同步控制的范围可能隐含在方法或代码块的作用域中,这可能对代码的可读性和维护性产生一定的影响。
- 锁:使用显式锁时,我们可以通过代码中的
-
Lock只有代码块锁,synchronization有代码块锁和方法锁
总的来说,锁和同步都是控制多线程访问共享资源的机制,但它们在实现方式、灵活性和代码结构上有所不同。使用锁时可以更灵活地控制并发线程的行为,并提供更多的锁特性。然而,同步提供了一种相对简单和隐式的方式来实现同步控制。在选择使用锁还是同步时,可以根据实际需求和场景的复杂性来进行决策。
消费者-生产者问题
定义
生产者-消费者问题是在并发编程中常见的一种同步问题。它涉及到两类线程:生产者线程和消费者线程,它们共享一个有限大小的缓冲区。
生产者的任务是向缓冲区放入数据,而消费者的任务是从缓冲区取出数据。然而,需要确保以下几点:
- 生产者不能向已满的缓冲区放入数据,只有当缓冲区有空位时才能放入。
- 消费者不能从空的缓冲区取出数据,只有当缓冲区有数据时才能取出。
- 生产者和消费者之间要保持互斥,即同一时间只能有一个生产者或消费者操作缓冲区。
解决生产者-消费者问题的常见方法有以下几种:
- 使用信号量:可以使用两个信号量,一个表示缓冲区的空位,另一个表示缓冲区的数据个数。当生产者想要放入数据时,首先检查空位信号量,如果有空位则放入数据,并减少空位信号量的计数。当消费者想要取出数据时,首先检查数据个数信号量,如果有数据则取出,并减少数据个数信号量的计数。
- 使用条件变量:可以使用条件变量来实现生产者和消费者之间的同步和通信。当生产者想要放入数据时,首先检查缓冲区是否已满,如果满了则等待条件变量的通知。当消费者取出数据后,通过通知条件变量唤醒等待的生产者线程。类似地,当消费者想要取出数据时,首先检查缓冲区是否为空,如果为空则等待条件变量的通知。当生产者放入数据后,通过通知条件变量唤醒等待的消费者线程。
- 使用阻塞队列:许多编程语言和框架提供了阻塞队列的实现,如Java的
BlockingQueue。阻塞队列内部实现了上述的同步和通信机制,可以方便地在生产者和消费者之间传递数据。生产者通过调用队列的插入方法(如put()或offer())放入数据,如果队列已满则自动阻塞等待。消费者通过调用队列的取出方法(如take()或poll())取出数据,如果队列为空则自动阻塞等待。
无论使用哪种方法,重要的是确保生产者和消费者之间的同步和通信,并避免出现缓冲区溢出或下溢的情况。同时,需要注意处理并发访问缓冲区时可能出现的竞态条件和死锁情况。
总的来说,生产者-消费者问题是一个重要的并发编程问题,解决它需要合理地管理共享资源并确保线程之间的同步和互斥。合理选择适当的同步机制可以避免潜在的问题,并提高程序的性能和可靠性。
解决方法
管程法
定义
管程法(Monitor)是一种并发编程中用于实现线程同步的概念和技术。管程是由一组共享变量和对这些变量的操作(称为管程操作)组成的一个抽象数据类型。
在管程中,通过使用互斥锁(也称为管程锁)来实现对共享变量的互斥访问。只有当前持有管程锁的线程才能执行管程操作,其他线程必须等待直到锁被释放。
管程法通常包含以下三个要素:
-
共享变量:表示不同线程之间需要共享的数据,通过管程的方法来访问和修改这些变量。
-
条件变量:用于线程间的条件等待和通知机制。条件变量提供了线程等待某个条件(采用循环等待方式)和通知某个条件满足的机制。在管程中,通常会有一个或多个条件变量和相关的等待和通知方法。
-
管程操作:用于对共享变量进行操作的方法。这些方法由管程提供,并且只有当前获得管程锁的线程才能执行这些方法。在执行管程操作期间,如果发现某个条件不满足,则当前线程会释放管程锁并进入等待状态,直到其他线程通知该条件满足为止。
通过使用管程法,可以将线程的同步和互斥控制封装在一个抽象的数据类型中,使代码具有更高的可读性和可维护性。管程法能够解决线程间的互斥、同步和条件等待的问题,帮助开发者编写更安全和高效的并发程序。
需要注意的是,管程法是一种概念和设计模式,并不限定于特定的编程语言或技术。
代码
package com.thread.synchronization;
/**
* 管程法解决生产者-消费者问题
*
* @author pangyangjian
* @since 2023/9/1 17:53
*/
public class MonitorExample {
private int count = 0; // 共享变量
public synchronized void increment() {
while (count >= 10) { // 条件不满足时等待
try {
wait(); // 线程等待并释放管程锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count++;
System.out.println("Increment: " + count);
notifyAll(); // 唤醒其他等待线程
}
public synchronized void decrement() {
while (count <= 0) { // 条件不满足时等待
try {
wait(); // 线程等待并释放管程锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
System.out.println("Decrement: " + count);
notifyAll(); // 唤醒其他等待线程
}
public static void main(String[] args) {
MonitorExample monitorExample = new MonitorExample();
Thread incrementThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
monitorExample.increment();
}
});
Thread decrementThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
monitorExample.decrement();
}
});
incrementThread.start();
decrementThread.start();
try {
incrementThread.join();
decrementThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
信号灯法
定义
信号灯法是一种并发编程的概念,通过使用信号量来实现对并发资源的控制和同步。信号量是一个计数器,可以用来控制对共享资源的访问。
信号灯法包括两种基本操作:P 和 V 操作。
P操作(也称为wait操作)用于申请一个资源。如果信号量的计数器大于 0,表示有可用资源,那么就可以执行P操作,将计数器减 1。如果计数器为 0,则当前线程会被阻塞等待资源的释放。V操作(也称为signal操作)用于释放一个资源。当线程使用完共享资源后,应该执行V操作,将计数器加 1,以便其他等待资源的线程可以继续执行。
代码
package com.thread.synchronization;
import java.util.concurrent.Semaphore;
/**
* @author pangyangjian
* @description 信号灯法
* @since 2023/9/1 18:11
*/
public class SemaphoreExample {
private final Semaphore semaphore = new Semaphore(5); // 信号量初始化为 5
public void doWork() {
try {
semaphore.acquire(); // 申请一个资源
System.out.println("Doing work..."); // 模拟工作
Thread.sleep(2000); // 线程睡眠 2 秒
semaphore.release(); // 释放一个资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SemaphoreExample example = new SemaphoreExample();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(example::doWork);
thread.start();
}
}
}
在上述代码中,我们创建了一个 SemaphoreExample 类,其中包含一个 Semaphore 对象 semaphore。我们初始化 semaphore 的计数器为 5,表示同一时间最多可以有 5 个线程同时执行 doWork() 方法。
在 doWork() 方法中,首先调用 acquire() 方法来申请一个资源。如果当前可用资源大于 0,则可以继续执行后续工作。如果资源已经用尽(计数器为 0),那么线程将会被阻塞,直到其他线程释放资源。
在模拟工作执行期间,其他线程可能会被阻塞,直到当前线程完成工作并释放资源。一旦线程完成工作,我们调用 release() 方法来释放资源,让其他线程可以继续执行。
在 main 方法中,我们创建了 10 个线程来执行 doWork() 方法。由于信号量的计数器为 5,所以同一时间最多只有 5 个线程可以获得资源并开始工作,其他线程需要等待资源的释放。
信号灯法适用于多线程环境下对临界区的控制,可以确保同时执行的线程数量不超过特定的限制。这在限制资源访问的场景下非常有用,例如线程池、连接池、任务调度等,以及其他需要限制并发访问的情况。
线程池
定义
线程池是一种用于管理和控制线程执行的机制,它在应用程序启动时创建一组线程,并维护这些线程的执行状态。线程池可以重复使用线程,避免了线程创建和销毁的开销,提高了应用程序的性能和效率。
线程池的定义如下:线程池是一个具有固定数量的线程集合,用于执行提交给它的任务。它通过在池中预先创建一组线程,维护任务队列,并在需要时将任务分配给空闲线程来优化线程的创建和销毁过程。
线程池通常由以下几个组件组成:
- 线程池管理器(ThreadPool Manager):负责管理线程池的创建、销毁和维护。它可以根据具体的需求来配置线程池的大小以及其他相关参数。
- 任务队列(Task Queue):用于存储待执行的任务。当线程池中的线程空闲时,它们会从任务队列中获取任务并执行。
- 工作线程(Worker Threads):由线程池生成和维护的一组线程。这些线程可以被循环使用,避免了频繁创建和销毁线程的开销。
- 任务接口(Task Interface):定义待执行任务的接口或抽象类。它包含了要执行的具体任务逻辑。
使用线程池的好处包括:
- 降低线程创建和销毁的开销:线程的创建和销毁是一种开销较大的操作。使用线程池可以避免频繁地创建和销毁线程,从而提高应用程序的性能和效率。
- 控制资源和并发度:线程池可以限制同时执行的线程数量,控制资源的使用和防止系统资源耗尽。它还可以防止应用程序因为过多的并发线程而导致的性能问题和内存溢出等。
- 提供任务队列和调度机制:线程池利用任务队列来存储待执行的任务,通过调度机制来分配任务给空闲线程执行。这样可以更好地管理任务,提高任务的执行效率和响应速度。
总之,线程池是一种管理和控制线程执行的机制,通过重复使用线程、维护任务队列和调度机制来提高应用程序的性能和效率。
代码
package com.thread.synchronization;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @description 线程池观测
*
* @author pangyangjian
* @since 2023/9/5 16:22
*/
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个线程池,线程池中有 3 个线程
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
int taskNumber = i;
executor.submit(() -> {
System.out.println("Task " + taskNumber + " is being executed by " +
Thread.currentThread().getName());
try {
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskNumber + " is finished");
});
}
// 关闭线程池
executor.shutdown();
}
}

浙公网安备 33010602011771号