目录
- 2. 进程和线程的区别及应用
- **3. 多线程程序含义和作用的提出**
- 5. Runnable (可运行的)
- 6. 简化操作以及线程名
- 7. 抢购鞋——多线程案例
- 8. 后台、守护进程的提出
- 9. 匿名内部类创建多线程——你们老师喜欢的
- 10. 发现问题,提出 `synchronized`(同步)的概念和用途
- 11. `synchronized` 同步方法
- 12. `Lock`、`ReentrantLock` 同步锁
- 13. 浅谈 `synchronized` 和 `Lock` 的区别
- 15. CPU线程调度、Priority(优先权)线程优先级、优先级常量
- 16. `join()` 线程插队
- 17. `sleep()` 线程休眠
- 18. `yield()` 线程让步
- 19. 线程状态:以斗地主为例
- 20. 线程通信的含义与线程优先级
- 21. 线程的通信:`wait` 和 `notify`
- 22. `notifyAll` 线程通信
2. 进程和线程的区别及应用
在多任务的操作系统中,进程和线程是两个核心的概念,它们有不同的特点和用途。理解它们的区别及应用场景对于开发高效且稳定的程序非常重要。
进程与线程的区别:
- 基本定义:
- 进程:是操作系统资源分配的基本单位,通常代表一个正在运行的程序。每个进程有独立的内存空间、文件描述符等资源。
- 线程:是进程中的一个执行单元,一个进程至少有一个线程。线程共享进程的资源(如内存、文件句柄等),但每个线程有自己独立的执行栈和程序计数器。
- 内存隔离:
- 进程之间相互独立,有各自的内存空间,进程间不会直接影响,但通过 IPC(进程间通信)可以进行数据交换。
- 线程共享同一个进程的内存空间。如果一个线程修改了共享数据,可能会影响到同一进程中的其他线程,导致数据不一致。
- 资源占用:
- 进程之间的切换需要更多的资源和时间,因为每个进程都需要独立的资源和内存空间。
- 线程共享进程资源,线程之间的切换相比进程切换更高效,因为它们之间的数据交换无需跨进程边界。
- 健壮性与效率:
- 多进程程序更为健壮,因为进程间是独立的,一个进程崩溃不会影响其他进程的执行。
- 多线程程序的效率更高,线程的创建、销毁和切换开销更小,但如果一个线程出现问题,整个进程都会受到影响。
- 独立性:
- 进程是独立的操作系统资源管理单位,它们在操作系统中有各自的地址空间。
- 线程则依附于进程存在,操作系统不会将线程当作独立的程序单元。
并发与并行:
- 并发(Concurrent):
- 指一个CPU或多个CPU调度多个任务,任务之间并不是同时执行,而是通过快速切换执行,让它们看起来是同时执行的。常见的并发应用如多线程程序的执行,操作系统会通过时间分片来轮流执行各个线程,保证每个线程有机会执行。
- 示例:在单核CPU上运行多个进程或线程,CPU通过快速切换让它们看起来是并行执行的。
- 并行(Parallel):
- 是指在多核CPU或多处理器系统中,多个任务真的同时在多个处理器上运行。每个任务都在独立的CPU核心上执行,速度上通常要比并发更高。
- 示例:在双核或四核CPU上,同时运行多个进程,每个进程占用一个核心,真正实现了同时执行。
应用场景:
- 进程的应用场景:
- 当程序需要更强的隔离性时,使用多进程可以避免一个进程的崩溃影响到其他进程。例如,Web服务器通常会使用多进程模型,每个请求由独立的进程处理,避免了单个请求崩溃影响到其他请求的处理。
- 线程的应用场景:
- 多线程适用于需要高并发但共享资源的场景。例如,银行系统中可能会有多个线程并发处理客户请求,所有线程共享一些数据(如账户信息),此时需要保证线程间的同步。
- 多线程非常适合处理IO密集型任务(如文件读写、网络请求等),因为它们大多数时间都在等待IO操作完成,这时候可以让其他线程继续执行。
- 并发与并行的应用场景:
- 并发适合处理任务量大但不需要真正同时执行的情况,例如多个用户访问同一个网站,操作系统可以通过时间分片来模拟并发执行。
- 并行适用于计算密集型任务,如图像处理、科学计算等。多个核心可以同时处理多个任务,显著提高处理速度。
总结:
- 进程是资源分配的单位,具有完全独立的内存空间和资源,适合处理互不干扰的任务。
- 线程是进程中的执行单位,适合处理需要共享资源的任务。
- 并发指在单个或多个CPU上交替执行任务。
- 并行指在多个CPU上同时执行多个任务。
在编写并发和并行程序时,需要根据具体的需求选择合适的技术,合理使用多进程和多线程,以实现高效且稳定的程序设计。
3. 多线程程序含义和作用的提出
多线程程序的含义与作用:
-
多线程的概念:
- 在Java中,线程是程序中的独立执行路径,它能够并行执行程序中的代码。每个线程都有自己的执行过程,多个线程可以共享同一进程的资源。
-
程序实现:
- 要创建一个线程,我们需要继承
Thread类,并重写run()方法来定义线程的任务。run()方法包含了线程需要执行的具体操作。
- 要创建一个线程,我们需要继承
-
执行方式:
-
启动线程
:
- 使用
start()方法来启动线程。调用start()后,JVM会为线程分配资源并调用线程的run()方法。这样线程就开始运行,run()方法中的代码会在该线程中执行。
- 使用
-
避免死循环问题
:
- 如果直接调用
run()方法,它并不会启动一个新的线程,而是在当前线程中执行run()方法的代码,这可能会导致死循环。
- 如果直接调用
-
-
代码解析:
public class NixThread extends Thread { @Override public void run() { // 用户一直在抢购 while (true) { System.out.println("NixThread....."); } } } public class DemoTest { public static void main(String[] args) { NixThread nixThread = new NixThread(); // 用start()启动新线程,调用run()方法会导致死循环 nixThread.start(); // 主线程继续执行其他任务 while (true) { System.out.println("main.....Thread..."); } } }- 在这个例子中,
NixThread是一个继承自Thread的类,run()方法中的代码表示线程执行的任务。在main()方法中,我们创建了一个NixThread对象,并通过start()方法启动了一个新的线程。主线程与NixThread线程会并发执行,不会互相阻塞。
- 在这个例子中,
-
多线程的效果:
- 当执行
start()方法时,main()线程和NixThread线程会交替执行,它们在 CPU 上轮流分配时间片,从而在屏幕上交替打印"NixThread....."和"main.....Thread..."。
- 当执行
-
应用场景:
- 多线程的应用非常广泛,例如抢购活动、文件下载、数据处理等多个任务同时进行时,都可以使用多线程来提高效率。
5. Runnable (可运行的)
在多线程编程中,线程的创建是一个基础且重要的概念。Java 提供了几种方式来创建线程,其中最常见的是通过继承 Thread 类来实现。但这种方法有一个局限性——Java 不支持多继承,所以一个类只能继承一个父类。如果我们已经继承了 Thread 类,就无法再继承其他类。为了克服这一局限性,Java 提供了 Runnable 接口来帮助我们更灵活地创建线程。
1. 为什么使用 Runnable 接口?
当我们使用继承 Thread 类来创建线程时,类只能继承 Thread,无法继承其他类,这限制了代码的复用和扩展性。通过实现 Runnable 接口,我们可以将线程创建与类的继承关系解耦,使得线程类可以继承其他类,从而提高代码的灵活性。
2. 代码实现
在下面的代码中,我们通过实现 Runnable 接口来创建线程。首先,定义了一个 NixThread 类,实现了 Runnable 接口并重写了 run() 方法,然后创建了一个 Thread 对象并将 NixThread 实例传递给它,最后启动了线程。
package com.nix.demo;
// 5. Runnable (可运行的)
/**
* 前面我们创建线程单继承了Thread,无法继承别的类,因为Java不支持多继承
*/
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
// 使用Runnable接口来创建线程,要使用其方法,必须创建Thread对象实现
// 因为NixThread没有继承Thread
Thread thread = new Thread(nixThread);
thread.start();
while (true) {
System.out.println("mainThread....");
}
}
}
package com.nix.demo;
public class NixThread implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("NixThread.....");
}
}
}
3. 关键点分析
Runnable接口:Runnable是一个函数式接口,它只有一个方法run(),这个方法定义了线程执行的任务。实现Runnable接口的类需要重写run()方法,执行线程要做的具体操作。- 解耦线程类和任务:通过实现
Runnable接口,可以将线程的创建和执行的任务解耦,从而让一个类既可以继承其他类,又可以作为线程的执行者。 Thread与Runnable的关系:Thread是一个表示线程的类,而Runnable则代表线程将要执行的任务。我们可以通过将实现了Runnable接口的对象传给Thread构造函数,从而启动线程。- 线程的启动:通过
thread.start()启动线程,这会调用run()方法,执行线程中的任务。在这个例子中,主线程不停地打印"mainThread....",而nixThread线程则打印"NixThread....."。
4. 总结
通过实现 Runnable 接口来创建线程,我们不仅避免了 Thread 类的继承限制,还能够让线程类在不干扰其他继承关系的情况下执行任务。这是一种非常灵活且推荐的线程创建方式,尤其是在需要多重继承或接口的场景中。使用 Runnable 接口可以帮助我们编写更具可扩展性和可维护性的多线程代码。
6. 简化操作以及线程名
在这篇文章中,我们将探讨如何简化线程的创建操作,并学习如何设置和获取线程的名称。线程名称在多线程调试和管理中非常重要,它有助于我们在程序执行时识别不同的线程,尤其是在进行调试或性能监控时。
1. 线程名称的重要性
每个线程都有一个名称,可以帮助我们在调试和多线程环境中更好地识别和管理线程。默认情况下,Java 为每个线程分配了一个名称,但我们可以自定义线程的名称。线程名称通常在调试和日志记录中非常有用,尤其是当有多个线程并发执行时。
2. 代码实现
在这段代码中,我们使用 Thread.currentThread().getName() 获取当前线程的名称,并打印出来。我们通过 Thread 构造函数直接为线程指定了一个名称 "anotherThread...",然后启动了线程。
package com.nix.demo;
// 6. 简化操作以及线程名
/**
* Thread源码里面有传线程名的构造方法,要在原来线程类中自动获取我们在主线程中设置的名字,使用Thread.currentThread().getName()方法,.currentThread()是指当前线程,.getName()是指获取名字
*/
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
// // 使用Runnable接口来创建线程,要使用其方法,必须创建Thread对象实现
// // 因为NixThread没有继承Thread
// Thread thread = new Thread(nixThread);
// thread.start();
// 这种方法跟上面一样,但是不提倡,以下为示例
new Thread(nixThread, "anotherThread...").start();
while (true) {
System.out.println("mainThread....");
}
}
}
package com.nix.demo;
public class NixThread implements Runnable {
@Override
public void run() {
while (true) {
// Thread.currentThread().getName()方法,.currentThread()是指当前线程,.getName()是指获取名字
System.out.println("NixThread....." + Thread.currentThread().getName());
}
}
}
3. 关键点分析
- 简化线程创建:在代码中,我们通过
new Thread(nixThread, "anotherThread...").start()创建了一个新线程,并为该线程指定了一个名称"anotherThread..."。这种方式简化了线程的创建过程,因为我们不再需要单独创建Thread对象并启动它。 - 线程名称:在
NixThread中,使用Thread.currentThread().getName()来获取当前线程的名称,并将其打印出来。这个方法可以帮助我们在多线程环境中识别和区分不同的线程。 Thread.currentThread()和getName():Thread.currentThread()获取当前正在执行的线程对象,而getName()则返回该线程的名称。通过这两个方法,我们可以动态地获取当前线程的名称并在日志或控制台输出中查看。
4. 总结
通过简化线程创建操作以及为线程设置合适的名称,我们可以更加高效地管理和调试多线程程序。在实际项目中,线程名称对于问题定位和性能监控至关重要。合理的线程命名不仅能提高程序的可读性,还能帮助我们快速识别问题和优化代码。
7. 抢购鞋——多线程案例
在这篇文章中,我们将通过一个简单的多线程抢购鞋子的案例来演示如何使用多线程实现并发操作。我们将模拟一个场景,假设有 10 双鞋,并且有三个人同时抢购,每个线程代表一个用户。通过这个例子,我们不仅可以理解线程的基本操作,还可以看到线程安全问题的潜在风险。
1. 多线程创建方式
在这个案例中,我们展示了两种线程创建方式:继承 Thread 类和实现 Runnable 接口。我们通过实现 Runnable 接口的方式来创建线程。
2. 代码实现
我们先定义了一个 NixThread 类,它实现了 Runnable 接口,并在 run() 方法中定义了抢购鞋子的逻辑。每个线程都在抢购鞋子时减少库存,直到没有鞋子为止。然后,我们通过创建多个线程并传递不同的线程名称来模拟三个人抢购。
package com.nix.demo;
// 7. 抢购鞋——多线程案例
/*
两种创建线程的方式,一种使用继承,一种使用接口实现,解决了线程名的问题,接下来我们模拟一个多线程的抢鞋程序
*/
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
new Thread(nixThread, "Good").start();
new Thread(nixThread, "Shea").start();
new Thread(nixThread, "Yeats").start();
}
}
package com.nix.demo;
/*
抢鞋的逻辑代码涵盖在线程当中,假设有10双鞋,有三个人来抢,一个线程就是一个用户,所以这就有三个名称不一样的线程名
以下代码 使用Runnable接口创建
*/
public class NixThread implements Runnable {
int nike = 10; // 共有 10 双鞋
@Override
public void run() {
while (true) {
if (nike > 0) {
System.out.println(Thread.currentThread().getName() + "抢到了第" + (nike--) + "双鞋");
}
}
}
}
3. 运行结果
假设我们运行这段代码,输出结果大致如下(由于线程调度是随机的,所以实际输出的顺序和结果可能会有所不同):
Good抢到了第10双鞋
Shea抢到了第9双鞋
Yeats抢到了第8双鞋
Good抢到了第7双鞋
Shea抢到了第6双鞋
Yeats抢到了第5双鞋
Good抢到了第4双鞋
Shea抢到了第3双鞋
Yeats抢到了第2双鞋
Good抢到了第1双鞋
4. 关键点分析
- 多线程并发:我们通过
Thread.currentThread().getName()来获取每个线程的名称,模拟了三个用户同时抢购鞋子的场景。每个线程在执行时都会抢购一双鞋,库存会相应减少。 - 线程安全问题:虽然代码看起来非常简单,但在实际应用中,线程之间的竞争可能会导致线程安全问题。比如,如果没有同步机制,多个线程可能会同时读取和更新
nike变量,从而导致库存的错误。 Runnable接口的应用:通过实现Runnable接口,我们可以将任务与线程分开,从而避免了Thread类的继承限制,增加了代码的可扩展性。
5. 总结
通过这个多线程抢购鞋子的例子,我们演示了如何创建线程并模拟并发操作。这个示例非常简单,但它展示了多线程编程中的一些重要概念,比如线程的创建、线程名称的设置以及并发操作的处理。
8. 后台、守护进程的提出
在操作系统中,进程可以分为前台进程和后台进程。前台进程是用户直接交互的进程,而后台进程则是在系统后台运行的进程,通常负责提供一些系统服务或监控任务。在 Java 中,线程的概念与进程相似,也有前台线程和后台线程。前台线程为用户提供服务,而后台线程则提供辅助服务,保证程序的正常运行。
1. 前台线程与后台线程
- 前台线程:前台线程是应用程序的主要线程,用户直接与其交互,程序的结束通常依赖于这些线程的终止。
- 后台线程:后台线程(也叫守护线程)是为前台线程提供服务的线程,例如垃圾回收、文件监控等。后台线程的生命周期与前台线程密切相关,当所有前台线程结束时,后台线程会自动终止。
2. 代码实现
在下面的代码中,我们创建了一个前台线程 NixThread 和一个后台线程 DaemonThread。后台线程通过 Thread.setDaemon(true) 方法设置为守护线程。前台线程在后台线程之前启动,并抢购鞋子。后台线程仅负责输出 “守护线程开始…”。
package com.nix.demo;
// 8. 后台、守护进程的提出
/**
* 电脑任务管理器
*
* Apps是前台进程,Background processes是后台进程也叫守护进程,这些进程在电脑开机时就被启动,这样电脑才能正常且安全的运作起来,在程序中也是同理
* 与进程同理,前台线程为用户提供服务,也有后台线程为前台线程提供的服务进行保护或者守护
*/
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
DaemonThread daemonThread = new DaemonThread();
// 后台线程 --守护线程(优先执行,一定要放在前台之前)
Thread dThread = new Thread(daemonThread);
dThread.setDaemon(true); // 设置为后台线程
dThread.start();
// 判断是不是守护线程
System.out.println(dThread.isDaemon());
// 前台线程
new Thread(nixThread, "Good").start();
new Thread(nixThread, "Shea").start();
new Thread(nixThread, "Yeats").start();
}
}
package com.nix.demo;
// 守护线程
/**
* 后台线程的创建过程:
* 创建一个DaemonThread,实现Runnable接口
* 重写run()方法
* 在运行类中创建先一个DaemonThread,再用 Thread 用来实现DaemonThread
* 最后调用setDaemon(true) 设置成后台守护线程,.start()开启线程
*/
public class DaemonThread implements Runnable {
@Override
public void run() {
System.out.println("守护线程开始....");
}
}
package com.nix.demo;
public class NixThread implements Runnable {
int nike = 10;
@Override
public void run() {
while (true) {
if (nike > 0) {
System.out.println(Thread.currentThread().getName() + "抢到了第" + (nike--) + "双鞋");
}
}
}
}
3. 运行结果
假设我们运行这段代码,输出结果可能如下所示(实际运行时,前台线程和后台线程的输出顺序可能会有所不同):
true
守护线程开始....
Good抢到了第10双鞋
Shea抢到了第9双鞋
Yeats抢到了第8双鞋
Good抢到了第7双鞋
Shea抢到了第6双鞋
Yeats抢到了第5双鞋
Good抢到了第4双鞋
Shea抢到了第3双鞋
Yeats抢到了第2双鞋
Good抢到了第1双鞋
true:输出true表示后台线程成功设置为守护线程。- 守护线程输出:在后台线程被启动时,它首先输出
"守护线程开始....",尽管后台线程通常不会阻止程序的退出,但它为前台线程提供了一些支持服务。 - 前台线程输出:每个前台线程(如
Good、Shea和Yeats)抢到了鞋子,并输出相应的信息。
4. 关键点分析
- 后台线程的生命周期:守护线程的生命周期由前台线程控制。当所有前台线程结束时,后台线程会自动退出。可以通过调用
setDaemon(true)将一个线程设置为后台线程。 - 前台线程的优先性:前台线程是程序的主要线程,其结束会导致整个程序的结束。即使后台线程仍在运行,程序也会停止。
- 守护线程的应用场景:守护线程通常用于执行一些不重要的辅助任务,比如定时任务、资源清理等。因为守护线程没有优先权,它会在所有前台线程结束后自动停止。
5. 总结
通过本例,我们学习了如何创建并使用守护线程。在实际应用中,后台线程非常适合执行一些后台维护任务,如日志记录、垃圾回收等。掌握前台和后台线程的工作原理,能够帮助我们设计更加健壮和高效的多线程应用。
9. 匿名内部类创建多线程——你们老师喜欢的
在 Java 中,我们可以通过匿名内部类来简化多线程的创建过程。匿名内部类是一种没有名称的类,它可以用来快速实现接口或继承类,特别适用于那些只需要一次性的实现。我们可以利用这种方式创建线程,从而减少代码的冗余。
在这篇文章中,我们将通过匿名内部类来创建一个多线程程序,模拟一个简单的并发执行。
1. 匿名内部类简介
匿名内部类是在使用时直接定义的类,没有显式的类名。它通常用来实现接口或继承类,尤其适合用于需要快速实现单一功能的场合。在创建多线程时,使用匿名内部类可以省去创建单独类文件的步骤,使代码更加简洁。
2. 代码实现
在以下代码中,我们通过匿名内部类来实现 Runnable 接口,并创建了一个线程。在该线程中,我们持续打印线程的名称。主线程则持续打印 “Main thread”。
// 9. 匿名内部类创建多线程——你们老师喜欢的
// 将Runnable接口进行匿名处理
public class Main {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName());
}
}
}).start();
while (true) {
System.out.println("Main thread");
}
}
}
3. 运行结果
假设我们运行这段代码,输出结果大致如下(因为线程调度是随机的,输出的顺序和内容可能有所不同):
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
...
Main thread
Main thread
Main thread
Main thread
Main thread
...
- Thread-0:匿名内部类创建的线程会自动分配一个名称(例如
Thread-0),并持续打印这个线程的名称。该线程的执行会一直进行下去,直到程序结束。 - Main thread:主线程会持续打印
"Main thread",与匿名线程同时并行运行。
4. 关键点分析
- 匿名内部类:匿名内部类提供了一种简洁的方式来实现接口或继承类。在创建线程时,匿名内部类通过实现
Runnable接口来定义线程的任务逻辑。这种方式减少了代码的冗余,使得代码更加紧凑。 - 线程并发:主线程和匿名线程会并发执行。虽然
Thread.currentThread().getName()输出线程的名称,但线程的调度是由操作系统管理的,因此不同的线程输出顺序是随机的。主线程和匿名线程交替输出各自的内容。 Thread-0:当通过new Thread()创建线程时,如果没有显式设置线程名称,Java 会自动为线程分配一个名称,如Thread-0、Thread-1等。
5. 总结
通过本例,我们展示了如何使用匿名内部类来创建线程,简化了代码的实现。匿名内部类适用于需要快速实现某个接口或功能的场景,它使得线程创建更加简洁和高效。掌握匿名内部类的使用,可以帮助我们更灵活地处理多线程任务,尤其是在简单场景下。
根据你提供的代码,下面是为你编写的博客草稿,包含了运行结果。
10. 发现问题,提出 synchronized(同步)的概念和用途
在多线程编程中,线程之间的并发执行会引发很多问题,尤其是在访问共享资源时。线程不安全的一个常见表现是,多个线程同时修改共享数据,导致数据的竞态条件。为了解决这个问题,Java 提供了同步机制,即 synchronized 关键字,用于确保同一时刻只有一个线程能够执行某段代码。
1. 问题的发现
假设我们正在模拟一个抢购鞋子的场景,共有 10 双鞋,并且有多个线程(即多个用户)同时抢购。如果每个线程在抢购之间有一定的延时操作(如 Thread.sleep()),就会出现线程不安全的问题。具体来说,两个线程可能会同时判断 nike > 0,然后都执行抢购操作,最终导致同一双鞋被两个人抢到。
2. 代码实现
我们在以下代码中模拟了多线程抢购鞋子的情况。我们使用 Thread.sleep() 方法模拟了每次抢购之间的延时。为了避免线程不安全问题,我们通过 synchronized 关键字来同步访问共享资源 nike,确保每次只有一个线程能够修改库存。
package com.nix.demo;
// 10. 发现问题,提出synchronized(同步)的概念和用途
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
new Thread(nixThread, "Good").start();
new Thread(nixThread, "Shea").start();
new Thread(nixThread, "Yeats").start();
}
}
package com.nix.demo;
// 现实情况中,抢鞋肯定是有延时操作的,如果我们用.sleep()设置每次抢鞋之间的间隙,会产生了一个问题,就是线程不同步导致线程不安全,两个人同时抢了第7双鞋
public class NixThread implements Runnable {
// 解决这个问题要用到线程同步,及时更新数据,即创建一个synchronized(同步)锁对象,同步数据
private int nike = 10;
// 锁对象
Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
if (nike > 0) {
try {
Thread.sleep(500); // 模拟抢购延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + (nike--) + "双鞋");
}
}
}
}
}
3. 运行结果
假设我们运行这段代码,输出结果可能如下所示:
Good抢到了第10双鞋
Shea抢到了第9双鞋
Yeats抢到了第8双鞋
Good抢到了第7双鞋
Shea抢到了第6双鞋
Yeats抢到了第5双鞋
Good抢到了第4双鞋
Shea抢到了第3双鞋
Yeats抢到了第2双鞋
Good抢到了第1双鞋
- 每个线程都会抢购到鞋子,且每双鞋只会被抢购一次。
- 通过使用
synchronized关键字,我们确保了每次只有一个线程可以访问nike变量,从而避免了线程不安全的问题。
4. synchronized 的作用与用途
在这段代码中,synchronized 用于确保每次只有一个线程能够执行抢购操作。它保证了对共享资源(nike)的访问是互斥的,从而避免了多个线程同时修改 nike 变量时发生竞态条件。
- 同步块:
synchronized (lock)是一个同步代码块,它保证了只有获得lock锁的线程才能执行其中的代码块。其他线程如果要访问这个同步代码块,会被阻塞,直到当前线程执行完毕释放锁。 - 线程安全:通过同步控制,我们确保了每次只有一个线程能修改
nike变量,避免了多线程间的冲突和不一致。
5. 总结
在多线程编程中,线程同步是保证程序正确性的重要手段。通过 synchronized 关键字,我们可以避免多个线程同时修改共享资源时发生的数据冲突。使用 synchronized 可以确保同一时刻只有一个线程能执行特定代码块,从而保证线程安全。
虽然 synchronized 是一种有效的同步机制,但它也会引入性能开销,因为每次执行同步代码时,都需要获取和释放锁。因此,在高并发的情况下,除了 synchronized 外,还可以考虑使用其他并发控制机制,如 ReentrantLock、ReadWriteLock 等。
11. synchronized 同步方法
在多线程编程中,同步是确保线程安全的关键。当多个线程并发访问共享资源时,如果没有适当的同步机制,可能会导致数据的竞态条件,进而产生错误。在 Java 中,synchronized 关键字是用于实现线程同步的一个常用工具。它可以确保某段代码在同一时刻只能有一个线程访问。
1. 问题背景
假设我们模拟一个多线程的抢购场景,有 10 双鞋,多个用户同时抢购。这些用户在抢购过程中可能会发生竞争条件,即多个线程同时操作共享资源(nike 变量),导致线程不安全的问题。
在上一个例子中,我们使用了 synchronized 块来同步线程的执行,但这次我们将 synchronized 用于同步方法,进一步简化代码并确保线程安全。
2. 代码实现
在这段代码中,我们将 shoeCatch() 方法标记为 synchronized,这样每次只有一个线程能够执行该方法,从而确保每次只有一个线程能够抢购鞋子。
package demo;
// 11. synchronized同步方法
// 如何理解锁呢?当用户一抢到第一双鞋时,锁住第一双鞋,其它用户就无法抢了
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
new Thread(nixThread, "Good").start();
new Thread(nixThread, "Shea").start();
new Thread(nixThread, "Yeats").start();
}
}
package demo;
public class NixThread implements Runnable {
private int nike = 10;
// 锁对象
Object lock = new Object();
@Override
public void run() {
while (true) {
shoeCatch();
}
}
// synchronized可以创建成一个 同步方法 ,将同步代码块抽离出来
public synchronized void shoeCatch() {
if (nike > 0) {
try {
Thread.sleep(500); // 模拟抢购延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + (nike--) + "双鞋");
}
}
}
3. 运行结果
假设我们运行这段代码,输出结果可能如下所示:
Good抢到了第10双鞋
Shea抢到了第9双鞋
Yeats抢到了第8双鞋
Good抢到了第7双鞋
Shea抢到了第6双鞋
Yeats抢到了第5双鞋
Good抢到了第4双鞋
Shea抢到了第3双鞋
Yeats抢到了第2双鞋
Good抢到了第1双鞋
- 每个线程成功抢到一双鞋,并且每双鞋只能被一个线程抢到。
- 由于使用了
synchronized同步方法,抢购操作变得线程安全,避免了多个线程同时修改nike变量的情况。
4. synchronized 同步方法的工作原理
- 同步方法:
synchronized可以应用于方法上,表示该方法的每次调用都必须获得该类实例的锁,才能执行。在这段代码中,我们将shoeCatch()方法声明为synchronized,从而确保每次只有一个线程能够进入该方法。其他线程在执行同步方法时会被阻塞,直到当前线程完成执行并释放锁。 - 锁的作用:当一个线程执行
shoeCatch()方法时,它会自动获取当前对象的锁。其他线程在访问该方法时会被阻塞,直到获取锁的线程释放锁。这就确保了每次只有一个线程能够修改nike变量,避免了竞态条件。
5. 总结
在 Java 中,synchronized 关键字用于确保多线程中的共享资源访问是线程安全的。通过将方法标记为 synchronized,我们可以简化代码,并确保每次只有一个线程能够执行该方法,从而避免数据冲突和竞态条件。
- 同步方法的优势:同步方法使得代码更加简洁,适用于在整个方法内都需要进行同步的情况。
- 性能考虑:虽然
synchronized确保了线程安全,但在高并发的场景下,频繁的锁竞争可能导致性能下降。在这种情况下,可能需要考虑更精细的同步策略,如使用显式的锁(ReentrantLock)或使用并发容器。
通过合理使用 synchronized,我们可以确保多线程环境下的共享资源访问是安全的,从而有效避免出现问题。
12. Lock、ReentrantLock 同步锁
在 Java 中,synchronized 是一种用于线程同步的方式,它通过锁机制来确保同一时刻只有一个线程能够执行某段代码。然而,synchronized 具有一些限制,比如它不支持中断、无法实现公平锁等。因此,Java 还提供了 Lock 和 ReentrantLock 作为更为灵活和强大的同步工具。
1. synchronized 与 ReentrantLock 的区别
synchronized:在方法或代码块上使用synchronized关键字,Java 会自动管理锁。即当代码执行完后,系统会自动释放锁。ReentrantLock:是java.util.concurrent.locks包中的一个实现,它提供了比synchronized更加灵活的锁控制机制。与synchronized不同,ReentrantLock需要手动释放锁,否则可能会导致死锁现象。
2. 代码实现
我们将模拟一个抢购鞋子的场景。与前面的同步方法不同,这一次我们使用 ReentrantLock 来同步线程的访问。ReentrantLock 允许我们更细粒度地控制锁的获取和释放。
package demo;
// 12. Lock、ReentrantLock同步锁
/**
* synchronized与reentrantLock区别
* synchronized 不需要用户去手动释放锁,代码执行完后系统会自动让线程释放对锁的占用
* reentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象
*/
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
new Thread(nixThread, "Good").start();
new Thread(nixThread, "Shea").start();
new Thread(nixThread, "Yeats").start();
}
}
package demo;
// Lock、ReentrantLock同步锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* synchronized与reentrantLock区别
* synchronized(同步锁)不需要用户去手动释放锁,代码执行完后系统会自动让线程释放对锁的占用
* reentrantLock(重入锁)则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象
*/
public class NixThread implements Runnable {
private int nike = 10;
// 重入锁
Lock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
reentrantLock.lock(); // 获取锁
try {
if (nike > 0) {
Thread.sleep(500); // 模拟抢购延时
System.out.println(Thread.currentThread().getName() + "抢到了第" + (nike--) + "双鞋");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 确保在执行完之后释放锁
reentrantLock.unlock(); // 释放锁
}
}
}
}
3. 运行结果
假设我们运行这段代码,输出结果可能如下所示:
Good抢到了第10双鞋
Shea抢到了第9双鞋
Yeats抢到了第8双鞋
Good抢到了第7双鞋
Shea抢到了第6双鞋
Yeats抢到了第5双鞋
Good抢到了第4双鞋
Shea抢到了第3双鞋
Yeats抢到了第2双鞋
Good抢到了第1双鞋
- 每个线程成功抢到一双鞋,并且每双鞋只能被一个线程抢到。
- 通过
ReentrantLock来保证每次只有一个线程能够执行抢购操作,避免了线程不安全的问题。
4. ReentrantLock 的优势
-
手动释放锁:
ReentrantLock需要手动释放锁。如果没有调用unlock()方法,就会导致死锁。因此,使用ReentrantLock时,我们必须小心确保锁能够在finally块中释放。 -
可中断的锁:
ReentrantLock提供了lockInterruptibly()方法,它允许线程在等待锁时可以响应中断,避免出现死锁的情况。synchronized则无法提供类似的功能。 -
公平锁:
ReentrantLock支持公平锁。如果设置ReentrantLock为公平锁(通过构造方法传入true),那么线程将按照请求锁的顺序来获得锁,避免了线程饥饿的问题。Lock lock = new ReentrantLock(true); // 公平锁
5. 总结
synchronized:使用简便,Java 自动管理锁的释放。适合简单的同步需求,但缺乏灵活性,无法提供更多的控制。ReentrantLock:提供了更多的控制和功能,如可中断锁、公平锁等。适用于更复杂的同步需求,但需要手动释放锁,增加了出错的风险。
当你需要更加精细控制线程的同步时,ReentrantLock 是比 synchronized 更加灵活的选择。不过,使用时一定要小心,确保每次获取锁后都会在 finally 块中释放锁,避免死锁的发生。
13. 浅谈 synchronized 和 Lock 的区别
在 Java 中,线程同步是多线程编程的一个关键概念,保证了多线程环境下共享资源的正确性和一致性。Java 提供了两种常用的同步机制:synchronized 和 Lock。虽然这两者都可以用来实现线程同步,但它们的实现方式和特性有所不同。在本篇文章中,我们将深入探讨 synchronized 和 Lock 的区别及其使用场景。
1. 性能和优化
- JDK 1.5 中,
synchronized被认为是重量级操作,性能较低,因为它涉及到操作系统的线程调度,可能会导致较大的性能开销。与此同时,Lock的性能相对较高,更加稳定。 - JDK 1.6 及之后的版本,
synchronized得到了优化,性能得到了提升。尤其是在JVM层面,JIT编译器对其进行了更多的优化,使得synchronized的性能比 JDK 1.5 时期更好。
2. 锁的释放机制
synchronized:当线程执行同步代码时,JVM 会自动管理锁的释放。如果线程在执行同步代码时发生异常,JVM 会确保锁被自动释放。Lock:Lock需要手动释放锁。为了避免死锁,必须在finally块中调用unlock()来释放锁。如果没有正确释放锁,可能会导致线程永远无法获得锁,进而发生死锁。
3. 死锁问题
synchronized:如果在执行同步代码时发生异常,synchronized会自动释放锁,因此不会出现死锁的情况。Lock:如果在执行同步代码时发生异常,Lock不会自动释放锁,需要手动调用unlock()方法来释放锁。如果忘记释放锁,可能导致死锁。
4. 使用方式
synchronized:可以直接加在方法上,或者在特定的代码块中使用synchronized关键字,括号中指定需要锁的对象。Lock:通常使用ReentrantLock类来实现,通过lock()方法加锁,通过unlock()方法解锁。为了避免死锁,unlock()方法必须在finally块中调用。
5. 代码示例
以下是使用 Lock 和 ReentrantLock 实现的一个多线程抢购鞋子的示例。在此示例中,我们通过 ReentrantLock 来控制线程同步,确保每个线程在操作共享资源时能够互斥。
package demo;
// 13. 浅谈synchronized和Lock的区别
/**
* JDK1.5中,synchroized是重量级操作,性能低效,Lock性能高,更稳定
* JDK1.6中,synchroized加入很多优化,更加稳定了
* 锁的释放
* synchronized以获取锁的线程执行完同步代码,如果线程执行发生异常,jvm会让线程释放锁
* Lock在finally中必须释放锁,不然容易造成线程死锁
* 死锁产生
* synchronized在发生异常时候会自动释放占有的锁,不会出现死锁
* Lock发生异常时候,不会主动释放,必须手动unlock来释放锁,可能引起死锁的发生
* 用法
* synchronized在需要同步的对象中加入,可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象
* Lock一般使用ReentrantLock类做为锁,通过lock()加锁和unlock()解锁指出,在finally中写unlock()防止死锁
*/
public class DemoTest {
public static void main(String[] args) {
NixThread nixThread = new NixThread();
new Thread(nixThread, "Good").start();
new Thread(nixThread, "Shea").start();
new Thread(nixThread, "Yeats").start();
}
}
package demo;
// Lock、ReentrantLock同步锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* synchronized与reentrantLock区别
* <p>
* synchronized(同步锁)不需要用户去手动释放锁,代码执行完后系统会自动让线程释放对锁的占用
* reentrantLock(重入锁)则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象
*/
public class NixThread implements Runnable {
private int nike = 10;
// 重入锁
Lock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
reentrantLock.lock(); // 获取锁
if (nike > 0) {
try {
Thread.sleep(500); // 模拟抢购延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + (nike--) + "双鞋");
}
}
}
}
6. 运行结果
假设我们运行上面的代码,输出结果可能如下所示:
Good抢到了第10双鞋
Shea抢到了第9双鞋
Yeats抢到了第8双鞋
Good抢到了第7双鞋
Shea抢到了第6双鞋
Yeats抢到了第5双鞋
Good抢到了第4双鞋
Shea抢到了第3双鞋
Yeats抢到了第2双鞋
Good抢到了第1双鞋
- 每个线程成功抢到一双鞋,并且每双鞋只能被一个线程抢到。
- 使用
ReentrantLock保证线程安全,避免了线程同步问题。
7. 总结
synchronized:适合简单的同步需求,易于使用和理解,但性能较低。JDK 1.6 后,synchronized已经得到了很多优化,性能大幅提升。Lock:提供了更多的控制和功能,如可中断锁、公平锁等。ReentrantLock更适用于复杂的同步场景,但需要手动释放锁,容易引发死锁问题。
在选择使用 synchronized 还是 Lock 时,考虑到项目的复杂性和性能要求,synchronized 更适合轻量级的同步,而 Lock 更适合需要灵活控制锁的复杂场景。
15. CPU线程调度、Priority(优先权)线程优先级、优先级常量
在多线程程序中,线程的执行顺序通常是由操作系统调度器根据系统的资源分配策略决定的。默认情况下,线程的执行权是平等的,即所有线程都有相同的机会去执行。然而,在某些情况下,我们可能需要控制某些线程的执行优先级,以便它们能够先执行。
Java 提供了设置线程优先级的功能,通过 Thread 类的 setPriority() 方法可以设置线程的优先级。线程的优先级是一个整数,范围从 Thread.MIN_PRIORITY(1)到 Thread.MAX_PRIORITY(10),默认的优先级是 Thread.NORM_PRIORITY(5)。线程优先级高的线程通常会比优先级低的线程获得更多的 CPU 时间片。
线程优先级常量:
Thread.MIN_PRIORITY = 1:最低优先级。Thread.NORM_PRIORITY = 5:默认优先级。Thread.MAX_PRIORITY = 10:最高优先级。
代码解析
package com.nix.demo;
public class Main {
public static void main(String[] args) {
// 打印主线程信息
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName());
}
// 创建最大优先级线程和最小优先级线程
Thread maxThread = new Thread(new MaxPriorityThread(), "maxThread...");
Thread minThread = new Thread(new MinPriorityThread(), "minThread...");
// 设置线程优先级
minThread.setPriority(Thread.MAX_PRIORITY); // 设置最大优先级
maxThread.setPriority(Thread.MIN_PRIORITY); // 设置最小优先级
// 启动线程
maxThread.start();
minThread.start();
}
}
在上面的代码中,maxThread 线程设置为最低优先级(1),minThread 线程设置为最高优先级(10)。当两个线程启动时,操作系统将根据线程的优先级调度它们的执行。
线程实现:
package com.nix.demo;
public class MaxPriorityThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
package com.nix.demo;
public class MinPriorityThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
MaxPriorityThread 和 MinPriorityThread 这两个类都实现了 Runnable 接口,在 run() 方法中打印线程的名称。每个线程会打印三次自己的名称。
运行结果
运行该程序时,由于 minThread 的优先级较高,因此它有更高的可能性先执行。假设系统调度器遵循优先级规则,你可能会看到类似如下的输出:
main
main
main
minThread...
minThread...
minThread...
maxThread...
maxThread...
maxThread...
- 主线程(
main)首先打印,因为它是默认启动的主线程。 minThread打印三次,表示该线程在 CPU 调度中获得优先执行的机会。maxThread打印三次,表示该线程虽然优先级较低,但依然会执行,可能因为操作系统的调度策略而稍晚一些。
重要注意事项
- 线程优先级并不保证执行顺序:虽然我们为
minThread设置了最高优先级,maxThread设置了最低优先级,但实际上,操作系统调度线程的具体顺序依赖于许多因素,例如操作系统的线程调度策略和当前系统的负载。即使minThread的优先级更高,操作系统也可能出于资源管理等原因,按其他方式调度线程执行。 - 线程优先级是建议而非强制:虽然设置线程优先级能够影响线程调度的顺序,但 Java 并不能强制操作系统按照这个优先级进行调度。操作系统仍然根据自己的调度策略来分配 CPU 时间。
总结
- Java 允许我们通过
setPriority()方法来设置线程的优先级,以便控制线程的执行顺序。 - 设置线程优先级的常量分别是:
Thread.MIN_PRIORITY(1),Thread.NORM_PRIORITY(5),Thread.MAX_PRIORITY(10)。 - 线程优先级影响的是线程获得 CPU 时间片的概率,但不保证线程会按优先级顺序执行,具体调度顺序由操作系统控制。
16. join() 线程插队
在多线程程序中,线程的执行顺序通常是由操作系统的调度程序决定的。然而,有时我们需要控制某些线程的执行顺序,确保一个线程在另一个线程执行完之后再继续执行。在这种情况下,Java 提供了 join() 方法,它能够让一个线程插队,等待另一个线程完成后再继续执行。
join() 方法作用:
join() 方法使得当前线程等待目标线程结束后再继续执行。换句话说,调用 join() 的线程会“插队”到目标线程后面,确保目标线程完成执行后再继续执行。
代码解析
package com.nix.demo;
// 16. join线程插队
/*
.join()方法可以抢占优先级,实现插队
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
MaxPriorityThread maxPriorityThread = new MaxPriorityThread();
Thread thread_1 = new Thread(maxPriorityThread, "1");
// 线程插队
thread_1.start();
// main线程
for (int i = 1; i < 8; i++) {
System.out.println(Thread.currentThread().getName());
if (i == 3) {
// 主线程在第3次循环时调用 thread_1.join(),等待 thread_1 完成
thread_1.join();
}
}
}
}
代码解释:
MaxPriorityThread实现了Runnable接口,在run()方法中输出当前线程的名字。该线程将在主线程中被启动。- 主线程中有一个循环,在循环到第三次时,调用了
thread_1.join()方法。 - 当主线程执行到
thread_1.join()时,主线程会暂停执行,直到thread_1执行完毕,主线程才会继续执行后面的代码。
MaxPriorityThread 代码:
package com.nix.demo;
public class MaxPriorityThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
该线程简单地输出当前线程的名字三次。
运行结果:
运行该程序时,thread_1 线程会先启动并打印其线程名。当主线程进入第3次循环时,调用了 join(),导致主线程暂停执行,直到 thread_1 完成。此时,thread_1 会先输出它的线程名,然后主线程继续执行并输出自己的线程名。
输出可能如下所示:
1
1
1
main
main
main
main
解释:
- 线程
1会首先执行并打印三次。 - 当主线程运行到第3次输出时,它调用了
join(),此时主线程会暂停,直到thread_1执行完。 - 然后主线程继续执行,输出自己的名字。
总结:
join()方法会让调用它的线程(如主线程)等待目标线程(如thread_1)执行完毕后,再继续执行后续代码。- 它通常用于确保某个线程执行完毕后,再执行其他线程的任务,类似于“线程插队”的机制。
这种方式非常适合在程序中有明确的执行顺序要求时使用,比如确保某个线程完成工作后再让其他线程执行。
17. sleep() 线程休眠
在多线程编程中,有时我们希望让一个线程暂停一段时间后再继续执行。Java 提供了 Thread.sleep() 方法来实现线程的休眠。调用 sleep() 方法时,当前线程会进入休眠状态,指定的时间过后,线程才会恢复执行。
sleep() 方法作用:
Thread.sleep()会让当前线程暂停执行,指定的时间后自动恢复。sleep()方法是静态的,它是让调用它的线程休眠,不会影响其他线程。
代码解析
package com.nix.demo;
// 17. sleep线程休眠
/*
还是上一个例子,使用.sleep()方法休眠后,thread_1线程插队时,会等待1000毫秒再打印出结果
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
MaxPriorityThread maxPriorityThread = new MaxPriorityThread();
Thread thread_1 = new Thread(maxPriorityThread, "1");
// 线程插队
thread_1.start();
// main线程
for (int i = 1; i < 8; i++) {
System.out.println(Thread.currentThread().getName());
if (i == 3) {
// 主线程在第3次循环时调用 thread_1.join(),等待 thread_1 完成
thread_1.join();
}
}
}
}
代码解释:
MaxPriorityThread实现了Runnable接口,重写了run()方法,并在其中使用了Thread.sleep(1000)来让thread_1线程休眠 1000 毫秒(即 1 秒)。- 主线程中有一个循环,在循环到第三次时,调用了
thread_1.join()方法,主线程会暂停,直到thread_1执行完毕。 MaxPriorityThread的run()方法会在每次循环时休眠 1000 毫秒,然后打印当前线程的名字。这样,thread_1会在每次打印前等待 1 秒。
MaxPriorityThread 代码:
package com.nix.demo;
public class MaxPriorityThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
// 休眠 1000 毫秒(即 1 秒)
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
运行结果:
运行该程序时,thread_1 会先启动并输出其线程名。由于在 thread_1 的 run() 方法中调用了 Thread.sleep(1000),每次输出前都会暂停 1 秒。主线程会在第三次循环时调用 thread_1.join(),导致主线程暂停执行,直到 thread_1 完成。
输出可能如下所示:
1
1
1
main
main
main
main
解释:
thread_1会先执行并在每次输出其线程名时休眠 1 秒。- 主线程会执行三次输出,且在第三次调用
join()时,主线程暂停执行,直到thread_1完成。 - 然后主线程继续执行并输出自己的线程名。
总结:
Thread.sleep()用于让当前线程暂停执行一段时间,可以帮助控制线程的执行顺序。- 该方法不会影响其他线程,只会让当前线程休眠指定的时间。
join()和sleep()可以结合使用,确保某些线程在主线程或其他线程执行完毕后再继续执行。
这种方式非常适合模拟线程间的延时操作,或者在需要控制线程执行顺序时使用。
18. yield() 线程让步
Thread.yield() 是一个静态方法,用于让当前线程“让步”,即提示当前线程放弃剩余的时间片,允许其他线程执行。它并不会强制当前线程停止,仍然由操作系统的线程调度器来决定什么时候调度其他线程。
主要作用:
yield()可以用来让当前线程主动放弃 CPU 执行时间,给其他线程更多的执行机会。- 它是线程自愿的“让步”,而非强制停止。
注意:
yield()方法不会导致当前线程挂起,它只是请求操作系统调度其他线程。- 由于线程调度是由操作系统控制的,因此它不一定能达到期望的效果,有时即使调用了
yield(),操作系统也可能继续让当前线程执行。
示例:yield() 使用
package com.nix.demo;
// 18. yield线程让步
/*
.yield()方法可以实现线程让步,让其它线程执行,thread_1输出一次的时候给thread_2让步了,有时程序运行的太快了,以至于还没打印出让步输出,thread_2已经输出完毕了
*/
public class Main {
public static void main(String[] args) {
MaxPriorityThread maxPriorityThread = new MaxPriorityThread();
MinPriorityThread minPriorityThread = new MinPriorityThread();
Thread thread_1 = new Thread(maxPriorityThread, "1");
Thread thread_2 = new Thread(minPriorityThread, "2");
thread_1.start();
thread_2.start();
}
}
MaxPriorityThread 代码:
package com.nix.demo;
public class MaxPriorityThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
if (i == 1) {
System.out.println("thread_" + Thread.currentThread().getName() + "==========线程现在开始让步");
// .yield()方法可以实现线程让步,让其它线程执行,thread_1输出一次的时候给thread_2让步了,有时程序运行的太快了,以至于还没打印出让步输出,thread_2已经输出完毕了
Thread.yield(); // 当前线程让步,允许其他线程执行
}
System.out.println(Thread.currentThread().getName());
}
}
}
MinPriorityThread 代码:
package com.nix.demo;
public class MinPriorityThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
代码分析:
MaxPriorityThread和MinPriorityThread是两个线程,它们分别会打印自己的线程名称。- 在
MaxPriorityThread的run()方法中,当循环到第二次时,调用了Thread.yield(),这意味着当前线程MaxPriorityThread会主动让出 CPU 时间片,允许MinPriorityThread先执行。 MinPriorityThread会在MaxPriorityThread调用yield()之后继续执行。
运行结果:
由于线程的调度由操作系统控制,yield() 并不保证 MinPriorityThread 一定会执行。有时,你会看到主线程和线程 1 输出交替,有时 yield() 并没有立即让出 CPU 时间片,线程 1 可能会继续执行。
假设程序输出类似如下:
thread_1==========线程现在开始让步
thread_2
thread_1
thread_2
thread_1
解释:
- 线程
thread_1在第二次输出时调用了yield(),试图让出 CPU 时间片。 - 操作系统将 CPU 分配给了
thread_2,thread_2执行并打印它的线程名。 - 然后
thread_1继续执行并打印自己的线程名。
总结:
yield()提示当前线程放弃 CPU 时间片,并请求操作系统调度其他线程,但不一定能保证它会被立即调度。- 这种方法有时对解决 CPU 密集型任务中的线程公平性有所帮助,但并不总是有效,取决于操作系统的线程调度策略。
19. 线程状态:以斗地主为例
在 Java 中,线程有六种不同的状态。这些状态可以通过 Thread.State 枚举类来查看。我们可以通过一个斗地主游戏的比喻来理解这些状态。
1. 新建(NEW):
- 解释:当你创建了一个线程对象,但还没有调用
start()方法时,线程处于新建状态。相当于你准备好了一副扑克牌,但还没有开始游戏。 - 比喻:就像你准备好了一副牌,游戏还没有开始。
2. 可运行(RUNNABLE):
- 解释:线程已经启动,可以开始执行。线程进入可运行状态时,表示它有机会被 CPU 调度执行,但不一定立即执行。
- 比喻:游戏开始,玩家准备出牌,但需要等待轮到自己。
3. 阻塞(BLOCKED):
- 解释:线程因为获取一个同步锁而被阻塞,直到获得锁后才会继续执行。
- 比喻:当一个玩家正在出牌,其他玩家必须等待,直到当前玩家打完一轮才可以继续出牌。
4. 等待(WAITING):
- 解释:线程因调用
wait()方法进入等待状态,直到其他线程通知(通过notify()或notifyAll()方法)它继续执行。 - 比喻:一个玩家出完牌后,其他玩家在等待他的回合,这时候他们并不会进行任何操作,直到接收到出牌信号。
5. 计时等待(TIMED_WAITING):
- 解释:线程进入一个限制时间的等待状态,超时后会自动从等待状态返回可运行状态。线程可能通过
sleep(time),join(time)等方法进入此状态。 - 比喻:当一个玩家出完牌,其他玩家有一个规定的等待时间来进行决策。如果超过规定时间,游戏就会自动继续。
6. 终止(TERMINATED):
- 解释:线程执行完毕,或者因为异常终止时,线程进入终止状态。此时,线程无法再启动。
- 比喻:当游戏结束时,所有玩家都已经出完牌,游戏终止。
线程状态的转换示例(斗地主比喻)
| 状态 | 说明 | 比喻 |
|---|---|---|
| 新建(NEW) | 创建线程对象后尚未调用 start() 方法 | 准备好扑克牌,但游戏还没有开始 |
| 可运行(RUNNABLE) | 调用 start() 后,线程有机会运行 | 游戏开始,玩家准备好出牌,等轮到自己出牌 |
| 阻塞(BLOCKED) | 当前线程尝试获取锁时,进入阻塞状态 | 玩家出牌时,其他玩家必须等他出完牌才能出牌 |
| 等待(WAITING) | 线程调用 wait() 等待其他线程通知它 | 玩家已经出完牌,等待其他玩家继续出牌或接收通知 |
| 计时等待(TIMED_WAITING) | 线程调用 sleep() 等待指定时间,超时后恢复执行 | 玩家有时间限制来出牌,超时后自动跳过 |
| 终止(TERMINATED) | 线程执行结束,不能再被启动 | 游戏结束,所有玩家都出完牌,无法再开始新的回合 |
示例代码:线程状态模拟
package com.nix.demo;
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread player1 = new Thread(new Player("Player 1"));
Thread player2 = new Thread(new Player("Player 2"));
// 新建状态
System.out.println("游戏准备开始...");
// 启动线程,进入可运行状态
player1.start();
player2.start();
// 阻塞状态:player1 出牌,player2 等待
player1.join(); // player1 执行完毕后,player2 才能继续
// 计时等待:玩家等待时间结束后,继续游戏
player2.join(1000); // player2 等待 1 秒,超时后继续
System.out.println("游戏结束!");
}
static class Player implements Runnable {
private String name;
Player(String name) {
this.name = name;
}
@Override
public void run() {
try {
// 可运行状态:玩家准备出牌
System.out.println(name + " 轮到我出牌!");
// 阻塞状态:模拟出牌时等待
Thread.sleep(500); // 出牌需要时间
System.out.println(name + " 已出牌,等待其他玩家出牌...");
// 计时等待:等待指定时间
Thread.sleep(1000); // 等待 1 秒,模拟玩家考虑
System.out.println(name + " 出完牌,等待其他玩家...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果:
游戏准备开始...
Player 1 轮到我出牌!
Player 1 已出牌,等待其他玩家出牌...
Player 1 出完牌,等待其他玩家...
Player 2 轮到我出牌!
Player 2 已出牌,等待其他玩家出牌...
Player 2 出完牌,等待其他玩家...
游戏结束!
线程状态总结:
- 线程状态描述了一个线程的生命周期和运行情况,帮助我们理解线程的执行过程。
- 在 Java 中,线程可以通过方法如
wait()、sleep()、join()等进入不同的状态。线程的状态转换是由线程的生命周期、等待和同步控制等因素决定的。
20. 线程通信的含义与线程优先级
线程调度和线程通信是 Java 中两个重要的多线程概念,它们虽然看似相关,但实际上是两个不同的机制。理解它们的区别有助于更好地设计多线程程序。
1. 线程优先级(Thread Priority)
线程优先级用于决定线程在操作系统调度中的相对优先性。线程优先级高的线程通常会比优先级低的线程更频繁地获得 CPU 时间片。但是,线程的优先级并不是线程调度的唯一因素,操作系统会根据多种因素(如 CPU 空闲时间、操作系统策略等)来调度线程。
-
Java 中线程优先级常量
:
Thread.MIN_PRIORITY= 1Thread.NORM_PRIORITY= 5(默认)Thread.MAX_PRIORITY= 10
你可以通过 Thread.setPriority(int priority) 方法设置线程的优先级,但不同操作系统对线程优先级的处理方式可能有所不同。即使你在 Java 中设置了线程的优先级,操作系统的调度算法依然可能会做出调整。因此,线程优先级更多的是一个建议,实际效果可能因平台而异。
2. 线程通信(Thread Communication)
线程通信是指线程间的相互通知与协作,它允许一个线程通过通知机制告诉另一个线程执行特定的操作。线程通信通常通过 wait(), notify(), 和 notifyAll() 方法来实现。
-
线程通信和线程调度的区别
:
- 线程调度:线程调度是操作系统或虚拟机根据一定的策略自动选择哪个线程可以获得 CPU 时间执行。线程调度一般由操作系统来管理,并且是一个自动的过程。线程优先级影响线程调度的顺序,但不直接涉及线程间的协作。
- 线程通信:线程通信是线程间通过某些机制相互传递信息或协调工作,它允许线程之间交换数据或同步操作。线程通信通常发生在一个线程需要等待另一个线程完成某个任务之后才能继续执行。
线程通信不等同于线程调度,虽然线程调度涉及到线程的优先级和 CPU 时间分配,但线程通信涉及到如何协调不同线程之间的操作,确保它们能够同步工作。
线程通信的基本概念
Java 提供了多种线程间通信的机制,最常见的是通过 Object 类的 wait() 和 notify() 方法来实现。
1. wait() 方法:
当一个线程调用 wait() 方法时,它会放弃当前的锁,并进入等待状态,直到另一个线程调用 notify() 或 notifyAll() 方法唤醒它。
2. notify() 和 notifyAll() 方法:
notify():唤醒一个处于等待状态的线程。如果有多个线程在等待同一锁,notify()会随机选择一个线程唤醒。notifyAll():唤醒所有在等待状态的线程。
3. synchronized 锁机制:
为了确保线程安全,wait() 和 notify() 必须在同步代码块或同步方法内调用,因为它们需要先获得该对象的锁。
示例:线程通信
假设我们有一个生产者-消费者问题,生产者线程生成数据,消费者线程消费数据。生产者和消费者通过线程通信协作,确保消费者只在生产者生产了数据后才消费。
package com.nix.demo;
public class ThreadCommunicationDemo {
public static void main(String[] args) throws InterruptedException {
// 创建共享缓冲区
Buffer buffer = new Buffer();
// 创建并启动生产者线程
Thread producer = new Thread(new Producer(buffer));
producer.start();
// 创建并启动消费者线程
Thread consumer = new Thread(new Consumer(buffer));
consumer.start();
}
}
class Buffer {
private int product = -1; // 缓冲区中的数据,初始时为空
// 生产者生产数据
public synchronized void produce(int product) throws InterruptedException {
// 如果缓冲区已经有数据,等待消费者消费
while (this.product != -1) {
wait(); // 等待消费者消费
}
this.product = product; // 生产者生产数据
System.out.println("生产者生产了数据: " + product);
notify(); // 通知消费者消费
}
// 消费者消费数据
public synchronized void consume() throws InterruptedException {
// 如果缓冲区没有数据,等待生产者生产
while (this.product == -1) {
wait(); // 等待生产者生产
}
System.out.println("消费者消费了数据: " + product);
product = -1; // 消费后缓冲区为空
notify(); // 通知生产者生产数据
}
}
class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 1; i <= 5; i++) {
buffer.produce(i); // 生产数据
Thread.sleep(500); // 模拟生产过程
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 1; i <= 5; i++) {
buffer.consume(); // 消费数据
Thread.sleep(1000); // 模拟消费过程
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
代码解释:
Buffer类:该类充当了生产者和消费者之间的缓冲区。它提供了produce()和consume()方法,用于生产和消费数据。生产者生产数据时,如果缓冲区已经有数据,它会调用wait()进入等待状态;消费者消费数据时,如果缓冲区没有数据,它也会调用wait()进入等待状态。每次生产或消费完数据后,都会通过notify()唤醒另一个线程继续执行。- 生产者和消费者线程:生产者线程生产数据并调用
buffer.produce()方法,消费者线程消费数据并调用buffer.consume()方法。
输出结果:
生产者生产了数据: 1
消费者消费了数据: 1
生产者生产了数据: 2
消费者消费了数据: 2
生产者生产了数据: 3
消费者消费了数据: 3
生产者生产了数据: 4
消费者消费了数据: 4
生产者生产了数据: 5
消费者消费了数据: 5
总结:
- 线程优先级:控制线程在操作系统中调度的相对优先性。它决定了线程的执行顺序,但不保证线程一定按优先级顺序执行。
- 线程通信:通过
wait()、notify()和notifyAll()实现线程之间的协作。线程通信确保了线程在需要时能够等待、唤醒或通知其他线程,从而协调工作。
线程调度和线程通信是两个不同的机制,前者是由操作系统控制的,后者是由 Java 提供的同步机制来控制的。
21. 线程的通信:wait 和 notify
这个例子演示了 Java 中的 线程通信机制,也就是如何利用 wait() 和 notify() 方法实现线程间的协作。
1. 基本概念
wait():当一个线程调用wait()时,线程会释放当前持有的锁并进入等待队列,直到被其他线程通过notify()或notifyAll()唤醒。notify():notify()方法会随机唤醒一个在该对象监视器上等待的线程(如果有多个线程在等待),使得该线程重新获取锁并继续执行。
在这个例子中,模拟的是一个 生产者 和 消费者 问题(类似于生产者-消费者模型)。生产者负责生产产品(在这个例子中是避孕套 Condom),消费者购买产品。生产者和消费者之间的协作通过 wait() 和 notify() 方法实现。
2. 代码解析
Condom类:代表共享资源,模拟生产的产品(避孕套)。isStatus标识库存状态,true表示有库存,false表示库存空。Customer类:代表消费者线程,消费者会判断是否有库存。如果没有库存(isStatus == false),则调用wait()进入等待状态,等待生产者生产完货物后被唤醒。Producer类:代表生产者线程,生产者会判断库存状态。如果库存已经有货(isStatus == true),则调用wait()进入等待状态,等待消费者购买完货物后被唤醒。
3. 执行流程
- 生产者和消费者通过
wait()和notify()方法相互通知,保持对库存状态的控制。 - 当库存为
false(没有货物)时,消费者调用wait()进入等待状态;生产者生产完货物后通过notify()唤醒消费者。 - 当库存为
true(有货)时,生产者调用wait()等待消费者购买;消费者购买完货物后通过notify()唤醒生产者。
4. 改进与优化
- 由于
notify()只是唤醒一个线程,如果多个线程在等待,可能导致线程竞争资源的情况。此时,notifyAll()会唤醒所有等待线程,适用于生产者和消费者模型中。 - 在多线程环境下,使用
wait()和notify()时需要格外注意线程安全问题,确保所有共享资源(如Condom)的访问都受到正确的同步保护。
代码示例
package com.nix.demo;
public class Main {
public static void main(String[] args) {
Condom condom = new Condom();
new Customer(condom).start(); // 启动消费者线程
new Producer(condom).start(); // 启动生产者线程
}
}
class Condom {
// 状况,表示是否有库存
public boolean isStatus = false; // 默认没有库存
}
class Customer extends Thread {
private Condom condom;
public Customer(Condom condom) {
this.condom = condom;
}
@Override
public void run() {
while (true) {
synchronized (condom) {
// 如果没有库存,消费者就等待
if (condom.isStatus == false) {
try {
// 商品卖完了,消费者等待生产者生产
condom.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 模拟卖光了,那么isStatus状态是false
condom.isStatus = false;
System.out.println(Thread.currentThread().getName() + " 我买完了,可以生产了,通知生产者");
// 唤醒生产者生产
condom.notify();
}
}
}
}
class Producer extends Thread {
private Condom condom;
public Producer(Condom condom) {
this.condom = condom;
}
@Override
public void run() {
while (true) {
synchronized (condom) {
// 如果有库存,生产者就等待消费者买完
if (condom.isStatus == true) {
try {
// 商品还有库存,生产者等待消费者购买
condom.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 模拟生产好了,那么isStatus状态是true
condom.isStatus = true;
System.out.println(Thread.currentThread().getName() + " 我生产完了,你可以买了,通知消费者");
// 唤醒消费者购买
condom.notify();
}
}
}
}
输出示例:
Thread-1 我买完了,可以生产了,通知生产者
Thread-0 我生产完了,你可以买了,通知消费者
Thread-1 我买完了,可以生产了,通知生产者
Thread-0 我生产完了,你可以买了,通知消费者
5. 总结
wait():线程进入等待状态,直到收到通知才会继续执行。notify():唤醒一个在当前对象监视器上等待的线程。- 生产者消费者模型是一个经典的线程通信场景,使用
wait()和notify()方法可以轻松实现生产者和消费者之间的同步与协调。
22. notifyAll 线程通信
这个示例展示了 notifyAll() 方法的使用,它会唤醒在同一个对象监视器上等待的所有线程,与 notify() 方法的区别在于,后者只会唤醒一个等待线程,而 notifyAll() 会唤醒所有等待该对象锁的线程。
1. notifyAll() vs notify()
notify():唤醒一个在对象上等待的线程。如果多个线程在等待该对象的锁,notify()会随机唤醒其中一个线程。notifyAll():唤醒所有在对象上等待的线程。它更为彻底,适用于有多个线程在等待同一个资源时,需要唤醒所有线程的情况。
在生产者和消费者模型中,通常只有一个消费者和一个生产者,因此 notify() 就足够了。但如果涉及到多个消费者或多个生产者,使用 notifyAll() 可以更有效地协调多个线程。
2. 代码说明
Condom类:共享资源,表示产品的库存。isStatus用于标识库存状态(true表示有货,false表示无货)。Producer类:生产者线程,检查库存状态。如果库存有货,则调用wait()等待消费者购买完;如果库存为空,则生产者生产新货,并通过notify()或notifyAll()唤醒消费者。Customer类:消费者线程,检查库存状态。如果库存为空,则调用wait()等待生产者生产更多货物;如果库存有货,则消费产品并通过notify()或notifyAll()唤醒生产者。
3. 执行流程
- 生产者:当库存有货时,生产者进入等待状态(调用
wait()),直到消费者购买完并通过notify()或notifyAll()唤醒生产者。生产者生产新货后,更新库存状态并通知消费者。 - 消费者:当库存为空时,消费者进入等待状态(调用
wait()),直到生产者生产新货并通过notify()或notifyAll()唤醒消费者。消费者购买产品后,更新库存状态并通知生产者。
4. 改进与优化
- 多个生产者和消费者:如果你有多个生产者和消费者,在这种情况下,
notifyAll()可能更加合适,因为多个消费者和生产者需要协调生产和消费的顺序。 - 性能考虑:虽然
notifyAll()可以唤醒所有线程,但在一些性能要求较高的场景下,如果只是想唤醒一个线程,使用notify()可能会更高效。对于较为复杂的生产者-消费者模型,notifyAll()的使用会更适合。
代码示例
package com.nix.demo;
public class Main {
public static void main(String[] args) {
Condom condom = new Condom();
new Customer(condom).start(); // 启动消费者线程
new Producer(condom).start(); // 启动生产者线程
}
}
class Condom {
// 状况,表示是否有库存
public boolean isStatus = false; // 默认没有库存
}
class Producer extends Thread {
private Condom condom;
public Producer(Condom condom) {
this.condom = condom;
}
@Override
public void run() {
while (true) {
synchronized (condom) {
// 如果库存有货,生产者等待消费者消费
if (condom.isStatus == true) {
try {
// 商品已经有库存,生产者等待
condom.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 模拟生产新货物,库存变为true
condom.isStatus = true;
System.out.println(Thread.currentThread().getName() + " 我生产完了,你可以买了,唤醒消费者");
// 唤醒消费者
condom.notify(); // 使用 notify() 唤醒消费者
// condom.notifyAll(); // 使用 notifyAll() 唤醒所有线程(如果有多个消费者)
}
}
}
}
class Customer extends Thread {
private Condom condom;
public Customer(Condom condom) {
this.condom = condom;
}
@Override
public void run() {
while (true) {
synchronized (condom) {
// 如果库存为空,消费者等待
if (condom.isStatus == false) {
try {
// 商品卖完了,消费者等待
condom.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 模拟购买产品,库存变为false
condom.isStatus = false;
System.out.println(Thread.currentThread().getName() + " 我买完了,可以生产了,通知生产者");
// 唤醒生产者
condom.notify(); // 使用 notify() 唤醒生产者
// condom.notifyAll(); // 使用 notifyAll() 唤醒所有线程(如果有多个生产者)
}
}
}
}
5. 总结
notifyAll()唤醒所有在该对象监视器上等待的线程,而notify()只唤醒一个线程。notifyAll()更适用于多线程之间的协作,尤其是当多个线程在等待相同的资源时。- 在 生产者-消费者模型 中,使用
wait()和notify()方法可以有效地同步生产者和消费者之间的交互。 - 在一些场景下,如果多个线程都在等待资源,使用
notifyAll()可以确保所有线程都有机会被唤醒。


被折叠的 条评论
为什么被折叠?



