多线程
一、多线程的概念
1. 线程的基本概念
- 线程(Thread): 是程序执行的最小单位。每个线程都有自己的执行路径,与其他线程并发执行。
- 进程(Process): 一个进程可以包含多个线程,进程内的线程共享内存和资源,但进程之间的线程不共享内存。
2. 并发和并行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
3. 什么是多线程
- 是指从软件或者硬件上实现多个线程并发执行的技术。
- 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
4. 多线程好处
-
充分利用CPU的资源
-
简化编程模型
-
带来良好的用户体验
1. 并行处理
多线程可以同时处理多个任务。在多核处理器上,每个线程可以分配给不同的核心并行执行,而单线程只能在一个核心上运行。这意味着多线程程序能够充分利用多核 CPU 的资源,多个线程可以在不同的核心上同时工作,加速任务的执行。
比如,如果你有四个核心的 CPU,单线程程序只能用一个核心,另外三个核心则是空闲的;而多线程程序可以让四个核心同时工作,从而加快任务的执行。
2. 隐藏 I/O 等待时间
程序在执行过程中,某些操作(例如读取文件、网络通信等)会导致线程阻塞(等待资源),这段时间 CPU 可能闲置。多线程程序可以利用这个时间去执行其他任务。例如,一个线程在等待文件读取时,其他线程可以继续执行计算任务,从而提高了 CPU 的利用率。
在单线程情况下,如果遇到 I/O 操作,整个程序都会等待,而无法处理其他任务,因此效率较低。
3. 任务分解和负载均衡
某些复杂任务可以拆分为多个独立的子任务。这些子任务可以分配给多个线程并行处理。例如,图像处理、大数据计算、矩阵运算等可以通过多线程划分任务,使得每个线程负责处理一部分数据,从而加快整体任务的完成时间。
4. 减少闲置时间
多线程程序在执行时,可以根据任务的不同需求合理分配 CPU 时间片。通过线程切换,不会有一个线程长期占用 CPU 资源,也不会因为等待导致 CPU 闲置。这样可以更有效地利用 CPU。
二、线程的创建和启动
1.主线程
在Java中,主线程是程序启动时自动创建的线程,它是整个Java程序的入口点。每个Java应用程序在启动时都有一个主线程,这个主线程负责执行main
方法中的代码
特点
- 启动时自动创建: 当你运行一个Java程序时,JVM会自动创建一个主线程,并执行
main
方法。 main
方法: 主线程执行的是public static void main(String[] args)
方法中的代码。main
方法是Java应用程序的入口。- 线程生命周期: 主线程的生命周期与程序的生命周期相关。主线程完成其
main
方法中的所有任务后,程序通常会结束,主线程也会终止。 - 创建和启动其他线程: 主线程可以创建和启动其他线程。其他线程的创建和管理不会影响主线程,除非你需要等待其他线程完成。
2. 创建线程的方式
Java提供了三种主要方式来创建线程:
1.继承 Thread
类
- 定义MyThread类继承Thread类
- 重写run()方法,编写线程执行体
- 创建MyThread类的对象
- 启动线程
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的代码
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value: " + i);
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
thread1.start(); // 启动线程
}
}
优点:
- 简单直接,尤其适用于快速实现线程的场景。
- 由于
Thread
类自身包含很多线程控制方法(如sleep()
、interrupt()
等),继承Thread
可以直接调用这些方法。
缺点:
- 单继承限制:由于 Java 只支持单继承,继承了
Thread
类后,不能继承其他类。这会限制类的设计。 - 灵活性差:继承
Thread
后,线程任务与线程控制代码耦合,无法将任务与线程分离出来。
2. 实现 Runnable
接口
- 定义MyRunnable类实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的代码
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value: " + i);
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
thread1.start(); // 启动线程
}
}
优点:
- 更灵活:任务逻辑与线程控制分离,任务类只需实现
Runnable
接口,不需要继承Thread
类,因此可以与其他类一起继承。 - 方便线程复用:同一个
Runnable
实例可以被多个线程使用。 - 资源更高效:可以通过
ThreadPoolExecutor
或其他线程池管理大量线程,避免手动创建线程的开销。
缺点:
Runnable
不返回结果,无法抛出受检异常,适合没有返回值的任务。如果需要获取任务的执行结果,需要借助其他机制。
3.实现 callable
接口
- 定义MyRunnable类实现callable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建FutureTask类的对象,把MyRunnable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author Advancer
* @version 1.0.0
* @ClassName Test.java
* @Description
* 继承Callable接口,实现多线程
* @createTime 2024年08月23日 16:08
*/
public class Test {
public static void main(String[] args) {
Myhtread mt = new Myhtread();
FutureTask ft1 = new FutureTask(mt);//未来任务
Thread t1 = new Thread(ft1);
FutureTask ft2 = new FutureTask(mt);
Thread t2 = new Thread(ft2);
t1.start();
t2.start();
try {
System.out.println(ft1.get());//线程运行结束后才能得到返回信息
System.out.println(ft2.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Myhtread implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println(i+1+".你好,来自线程"+Thread.currentThread().getName()+"-"+i);
}
return "线程"+Thread.currentThread().getName()+"结束";
}
}
优点:
- 有返回值:可以通过
call()
方法返回任务的执行结果,结果可以通过Future.get()
获取。 - 抛出异常:
Callable
的call()
方法允许抛出受检异常,适合复杂任务的错误处理。 - 更适合并发任务:结合
FutureTask
或ExecutorService
,可以方便地管理多线程任务的结果和状态。
缺点:
- 相较于
Runnable
和Thread
实现稍微复杂一些。 - 任务的返回值会导致
Future.get()
方法阻塞线程直到任务完成。
3. run()方法和start()方法的区别?
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由JVM调用此线程的run()方法
4. 三种创建方式对比
- 实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
方法 | 是否返回结果 | 是否抛出异常 | 是否适合大规模并发 | 灵活性 | 复杂性 |
---|---|---|---|---|---|
继承 Thread | 否 | 否 | 较差 | 较差 | 简单 |
实现 Runnable | 否 | 否 | 较好 | 好 | 简单 |
实现 Callable | 是 | 是 | 最佳 | 最好 | 复杂 |
三种创建线程的方式(继承 Thread
类、实现 Runnable
接口、实现 Callable
接口)的底层实现机制涉及到线程的启动、调度和执行方式有所不同。以下是它们的底层实现及区别:
1. 继承 Thread
类的底层实现
当你继承 Thread
类并创建线程时,主要的底层工作是在 Thread
类内部完成的。
- 底层实现要点:
Thread
类实现了Runnable
接口,并且有一个run()
方法,该方法是线程的入口点,包含线程需要执行的任务代码。- 当调用
start()
方法启动线程时,实际上是调用了native
方法start0()
,这个方法由 JVM 提供,它会启动一个新的操作系统线程。 - JVM 会调用操作系统的相关接口来创建和启动新的线程,操作系统负责线程的调度和执行。
- 区别:
- 线程的任务逻辑与线程管理代码紧密耦合,无法将任务逻辑与线程控制分开。
- 因为 Java 是单继承的,继承了
Thread
类后无法再继承其他类,限制了类的灵活性。 - 使用
Thread
类时,线程的生命周期和执行逻辑都由Thread
类管理,不易与其他任务或线程池集成。
2. 实现 Runnable
接口的底层实现
通过实现 Runnable
接口并将其作为参数传递给 Thread
对象来创建线程。
- 底层实现要点:
Runnable
接口包含一个run()
方法,定义了线程的任务逻辑。- 创建
Thread
对象时,可以将实现了Runnable
接口的实例作为参数传递给Thread
的构造函数。 - 当调用
start()
方法时,Thread
类内部会调用传入的Runnable
对象的run()
方法,启动线程执行任务。 - 线程的创建和调度仍由 JVM 和操作系统完成,与继承
Thread
类创建线程的方式类似。
- 区别:
- 使用
Runnable
接口实现线程,可以将任务逻辑与线程管理分离,提高了代码的灵活性和可维护性。 - 同一个
Runnable
实例可以被多个线程共享,提高了线程的复用性。 - 更适合通过线程池管理大量线程,避免频繁创建和销毁线程的开销。
- 使用
3. 实现 Callable
接口的底层实现
Callable
接口与 Runnable
接口类似,但它可以返回结果并抛出受检异常。
- 底层实现要点:
Callable
接口定义了一个call()
方法,与Runnable
的run()
方法类似,但可以返回一个泛型类型的结果。- 结合
FutureTask
或ExecutorService
使用时,可以通过Future
对象获取线程执行的结果。 FutureTask
类实现了RunnableFuture
接口,它实际上是Runnable
和Future
的结合体,可以被线程执行,并且可以获取线程执行的结果或取消任务。
- 区别:
Callable
接口允许线程任务返回结果,可以通过Future.get()
方法获取任务执行的结果,这在某些需要获取结果的并发任务场景中很有用。Callable
接口的call()
方法允许抛出受检异常,比Runnable
接口更适合处理需要异常处理的复杂任务。- 使用
Callable
和FutureTask
结合,可以更灵活地管理任务执行的状态和结果,适合复杂的并发场景。
总结
- 继承
Thread
适用于简单的线程任务,但不推荐在实际开发中使用,因为它的灵活性和可维护性较差。 - 实现
Runnable
接口是最常见和推荐的创建线程的方式,它将任务逻辑与线程管理分开,提高了代码的清晰度和灵活性。 - 实现
Callable
接口适用于需要获取任务执行结果或处理任务异常的复杂场景,提供了比Runnable
更多的控制和功能。
三、 线程的生命周期
线程的生命周期包括以下状态:
- 新建(New): 线程对象被创建,但尚未调用
start()
方法。 - 就绪(Runnable): 线程对象调用了
start()
方法,线程进入就绪状态,等待JVM调度。 - 运行(Running): 线程获得CPU时间片并执行
run()
方法中的代码。 - 阻塞(Blocked): 线程在等待获取资源时(例如,等待I/O操作完成)。
- 等待(Waiting): 线程调用
wait()
方法或join()
方法,线程会进入等待状态。 - 死亡(Terminated): 线程执行完毕或因异常终止。
四、线程调度
线程调度是操作系统或Java虚拟机(JVM)管理线程执行的过程,决定了线程的执行顺序和时间分配。有效的线程调度可以提高程序的并发性和响应性。
线程调度的几个关键方面:
调度策略、线程优先级、时间片管理。
控制线程调度的常用方法
方 法 | 说 明 |
---|---|
void setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程 |
boolean isAlive() | 测试线程是否处于活动状态 |
线程优先级
- 线程优先级由1~10表示,1最低,默认优先级为5
- 优先级高的线程获得CPU资源的概率较大
设置线程优先级
方法: setPriority(int priority)
获取线程优先级
方法:getpriority()
import java.util.concurrent.Callable;
/**
* @author Advancer
* @version 1.0.0
* @ClassName Test7.java
* @Description
* 显示主线程、子线程默认优先级
* 将主线程设置为最高优先级、子线程设置为最低优先级并显示
* @createTime 2024年08月23日 09:48
*/
public class Test7 {
public static void main(String[] args) {
System.out.println("*******显示默认优先级*******");
Mythread5 mythread5 = new Mythread5();
Thread thread = new Thread(mythread5);
System.out.println("主线程名:"+Thread.currentThread().getName()+",优先级"+Thread.currentThread().getPriority());
System.out.println("子线程名:"+thread.getName()+",优先级"+thread.getPriority());
System.out.println("*******修改默认优先级后*******");
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);
System.out.println("主线程名:"+Thread.currentThread().getName()+",优先级"+Thread.currentThread().getPriority());
System.out.println("子线程名:"+thread.getName()+",优先级"+thread.getPriority());
}
}
class Mythread5 implements Runnable{
public void run(){
}
}
线程休眠
- 让线程暂时睡眠指定时长,线程进入阻塞状态
- 睡眠时间过后线程会再进入可运行状态
Thread.sleep(1000);
package test8;
public class SleepTest {
public static void main(String[] args) {
// 初始化队列
Thread specialThread = new Thread(new SpecialQueue());
Thread normalThread = new Thread(new NormalQueue());
// 开始叫号
specialThread.start();
normalThread.start();
}
}
class SpecialQueue implements Runnable {
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println("特需号 " + i + " 正在看诊...");
try {
Thread.sleep(200); // 特需号看病时间是普通号的2倍
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("特需号看诊完毕!");
Thread.interrupted();
}
}
class NormalQueue implements Runnable {
public void run() {
for (int i = 1; i <= 50; i++) {
if (i == 11) {
// 等待特需号完成
try {
Thread.sleep(1000); // 稍作等待,确保特需号完成
} catch (InterruptedException e) {
}
}
System.out.println("普通号 " + i + " 正在看诊...");
try {
Thread.sleep(100); // 普通号看病时间
} catch (InterruptedException e) {
}
}
System.out.println("普通号看诊完毕!");
}
}
线程强制运行
join()
主线程等待 被调用的线程执行完成后 主线程才能运行
package test;
public class JoinExample extends Thread {
public void run() {
for(int i = 1; i <= 5; i++) {
try {
Thread.sleep(500);
} catch(InterruptedException e) {
System.out.println(e);
}
if (Thread.currentThread().getName().equals("线程1")){
System.out.print("线程1运行中.....");
}
System.out.println(i);
}
}
public static void main(String[] args) {
JoinExample t1 = new JoinExample();
JoinExample t2 = new JoinExample();
JoinExample t3 = new JoinExample();
t1.setName("线程1");
t1.start();
try {
t1.join();
} catch(InterruptedException e) {
System.out.println(e);
}
t2.start();
t3.start();
}
}
join()
方法让调用线程进入等待状态,直到目标线程完成或者被中断。
在上述代码中,主线程启动了 JoinExample 线程 t1,并调用了 t1.join(),这使得主线程在 t1 完成之前不会继续执行。当 t1 完成后,主线程才会继续启动 t2 和 t3。
如果**join()**方法在线程实例中被调用,当前运行的线程会被堵塞,直到线程实例运行完成。
例如线程a中调用线程b的join方法,这时线程a就会进入阻塞状态,直到线程b执行完成。这样就可以使并行的线程串行化的执行。
线程礼让
yield()
- 暂停当前线程,允许其他具有相同优先级的线程获得运行机会
- 该线程处于就绪状态,不转为阻塞状态
- 只是提供一种可能,但是不能保证一定会实现礼让
- 大多数情况下,yield()将导致线程从运行状态转到可运行状态,但很有可能没有效果
五、synchronized
多个线程操作同一共享资源时,将引发数据不安全问题
同步方法
使用synchronized修饰的方法控制对类成员变量的访问
访问修饰符 synchronized 返回类型 方法名(参数列表){……}
或者
synchronized 访问修饰符 返回类型 方法名(参数列表){……}
public class TicketThread implements Runnable{
private static int ticket = 50;
private static int num = 1;
@Override
public void run() {
while (true){
if (extracted()) break;
}
}
public synchronized boolean extracted() {
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张票");
num++;
ticket--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}else{
return true;
}
return false;
}
}
public class Test9 {
public static void main(String[] args) {
Thread th1 = new Thread(new TicketThread());
Thread th2 = new Thread(new TicketThread());
th1.setName("A");
th2.setName("B");
th1.start();
th2.start();
}
}
同步代码块
使用synchronized关键字修饰的代码块
synchronized(syncObject){ //需要同步的代码}
- syncObject为需同步的对象,通常写线程类的字节码对象
- 效果与同步方法相同
多个并发线程访问同一资源的同步代码块时
- 同一时刻只能有一个线程进入synchronized(this)同步代码块
- 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
- 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
package test10;
/**
* @ClassName RunThread.java
* @author Advancer
* @version 1.0.0
* @Description
* 多人参加1000米接力跑
* 每人跑100米,换下个选手
* 每跑10米显示信息
* @createTime 2024年08月23日 15:24
*/
public class RunThread implements Runnable{
public static int distance = 1000;
@Override
public void run() {
while (true){
synchronized (RunThread.class){
if (distance>0){
System.out.println(Thread.currentThread().getName()+"号选手拿到了接力棒");
for (int i = 10;i <= 100; i+=10){
System.out.println(Thread.currentThread().getName()+"号选手跑了"+i+"米");
distance-=10;
}
if(distance<=0){
System.out.println(Thread.currentThread().getName()+"号选手到达终点线!!!");
}
}
if (distance>0){
System.out.println("还剩"+distance+"米");
}else {
break;
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test10 {
public static void main(String[] args) {
RunThread runThread1 = new RunThread();
RunThread runThread2 = new RunThread();
RunThread runThread3 = new RunThread();
Thread th1 = new Thread(runThread1);
th1.setName("1");
Thread th2 = new Thread(runThread2);
th2.setName("2");
Thread th3 = new Thread(runThread3);
th3.setName("3");
th1.start();
th2.start();
th3.start();
}
}
六、线程安全的类型
方法是否同步 | 效率比较 | 适合场景 | |
---|---|---|---|
线程安全 | 是 | 低 | 多线程并发共享资源 |
非线程安全 | 否 | 高 | 单线程 |
例如 StringBuffer 和 StringBuilder
StringBuffer 的方法加了synchronized 线程安全 效率低
StringBuilder 的方法没加 synchronized 线程不安全 效率高