Java 多线程
Java 多线程
Java多线程是指在同一程序中同时执行多个任务的能力。每个任务在程序中被称为一个线程(Thread)。在Java中,多线程可以帮助我们更高效地利用计算机的多核处理能力,提升程序的并发性和响应性。
1. 线程的基本概念
线程是程序中的一个执行单元,每个线程都有自己的程序计数器、栈和局部变量。多线程允许程序在同一时间执行多个操作,而这些操作共享同一个内存空间。
联想操作系统中线程PCB
2. 创建线程的方式
1) 继承 Thread
类
最直接的方式是创建一个新的类,继承 Thread
类并重写 run()
方法。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程正在运行...");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread(); // 创建线程对象
thread.start(); // 启动线程
}
}
start()
方法:启动线程,调用run()
方法。run()
方法:线程的执行代码,包含线程需要完成的任务。
2) 实现 Runnable
接口
实现 Runnable
接口比继承 Thread
类更灵活,因为它允许多个线程共享同一个 Runnable
对象。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程正在运行...");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable); // 创建线程对象并传入 Runnable 实现类
thread.start(); // 启动线程
}
}
3) lambda表达式简写
可以在创建线程时不必显式地定义一个类或实现接口,只需用一个表达式来表示线程执行的任务。
Thread thread = new Thread(() -> {
System.out.println("线程正在执行");
});
thread.start();
3. 线程生命周期
线程在执行过程中会经历不同的状态:
- 新建(NEW): 使用
new
创建线程对象。 - 就绪(RUNNABLE): 调用
start()
方法。 - 运行(RUNNING): CPU 调度线程时开始执行
run()
。 - 阻塞(BLOCKED): 等待资源时进入阻塞状态。
- 终止(TERMINATED): 线程执行结束或被中断。
4. 常见的多线程方法
1. sleep(long millis)
- 作用:使当前线程暂停执行指定的时间(毫秒),但不会释放占用的资源(如锁)。线程处于休眠状态后,过了指定时间自动回到就绪状态,等待 CPU 调度。
- 用法:静态方法,需要捕获
InterruptedException
。 - 注意:
sleep()
方法不会释放锁,因此在同步方法中使用时要小心死锁。
try {
Thread.sleep(1000); // 当前线程休眠 1 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
2. interrupt()
- 作用:请求中断线程的执行。调用该方法后,线程会在适当的时机终止运行。需要在线程内部通过
isInterrupted()
或InterruptedException
判断是否中断。 - 用法:如果线程在
sleep()
、wait()
或join()
等方法中被阻塞,通过调用interrupt()
方法来中断线程。 - 注意:调用
interrupt()
并不一定会立即停止线程,具体行为取决于线程的状态。
Thread thread = new Thread();
thread.start();
thread.interrupt(); // 请求中断线程
3. join()
- 作用:使当前线程等待调用该方法的线程执行完毕后再继续执行。
join()
是一种让一个线程等待另一个线程完成的机制。 - 用法:调用
join()
后,当前线程会等待目标线程完成执行。 - 注意:如果线程是
join()
的目标线程,当前线程将被阻塞直到目标线程终止。
Thread thread = new Thread();
thread.start();
try {
thread.join(); // 当前线程等待 thread 执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
4. yield()
- 作用:让当前线程暂停并允许同优先级的线程有机会执行。调用
yield()
并不会阻塞当前线程,而是将当前线程从运行状态转到就绪状态,具体是否能让其他线程执行,由线程调度器决定。 - 用法:通常用于调度线程的优先级。
Thread.yield(); // 暂停当前线程,允许同优先级的线程执行
wait()和sleep()区别
特性 | wait() | sleep() |
---|---|---|
类 | Object 类的方法 | Thread 类的方法 |
作用 | 使线程进入等待状态,释放锁,等待其他线程唤醒 | 使当前线程暂停指定时间 |
锁的释放 | 释放当前对象的锁 | 不释放锁 |
使用场景 | 线程间通信,等待条件满足 | 使线程休眠一段时间,用于暂停执行 |
是否中断 | 会响应 interrupt() 中断 | 通过抛出 InterruptedException 响应中断 |
超时 | 可以指定等待时间,并在超时后自动唤醒 | 只能指定固定时间,时间到后自动唤醒 |
必须同步代码块 | 必须在同步代码块(synchronized )中使用 | 不需要同步代码块 |
5. 线程同步
当多个线程访问共享资源时,可能会出现资源竞争的情况,从而导致数据不一致。这时需要进行线程同步来保证线程安全。
1) 使用 synchronized
关键字
synchronized
可以用来修饰方法或代码块,保证在同一时间只有一个线程可以执行被同步的代码。
修饰方法
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
// 创建多个线程
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
}
}
- 通过在
increment()
方法上加上synchronized
,保证了线程安全,即在同一时刻,只有一个线程能执行该方法。
修饰代码块
synchronized
用作代码块时,通常是在方法内部的一段代码,只有持有相应锁的线程才能执行这一段代码。
synchronized (lockObject) {
// 被同步控制的代码块
}
lockObject
是用于同步的对象,表示当前线程需要持有该对象的锁才能执行同步代码块。- 被
synchronized
包裹的代码块是临界区,只有获得锁的线程可以进入执行。
三种锁对象
实例对象锁:如果多个线程需要访问同一类的不同实例,则可以选择实例对象作为锁对象(
this
)。public class Counter { private int count = 0; // 线程安全的增值方法 public void increment() { synchronized (this) { // 锁定当前对象(实例) count++; } } public int getCount() { return count; } }
类锁(静态锁):如果多个线程访问的是类的静态成员(方法或变量),则可以选择类的 Class 对象作为锁(
ClassName.class
)。public class Counter { private static int count = 0; // 静态方法,访问的是类的静态变量 public static void increment() { synchronized (Counter.class) { // 锁定类的 Class 对象 count++; } } public static int getCount() { return count; } }
自定义锁对象:可以定义一个专门的对象作为锁来确保线程同步。
synchronized
代码块的执行过程
当一个线程进入同步代码块时,它会尝试获得锁对象的锁。如果其他线程已经持有锁,则当前线程将会被阻塞,直到获得锁。锁的获取和释放是自动的,当线程离开同步代码块时,锁会被释放。
- 获取锁:当线程进入同步代码块时,它首先会尝试获得
lockObject
的锁。只有当它获得锁后,才能执行代码块中的内容。 - 释放锁:当线程执行完同步代码块后,锁会被自动释放,这时其他等待的线程就可以获取锁并继续执行。
2) Lock
接口
Lock
是一种更灵活的同步机制,比 synchronized
提供了更多的功能,例如尝试锁定、超时等待等。
java复制代码Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}
考试不要求
6. 进程通信
7. 常见的多线程问题
- 死锁:多个线程互相等待对方释放资源,导致程序无法继续执行。
- 竞态条件:多个线程同时操作共享数据,可能会导致程序产生不可预测的结果。
8. 使用 ExecutorService
管理线程池
在实际应用中,直接创建和管理线程可能会导致性能问题。使用线程池可以有效管理和重用线程。Java 提供了 ExecutorService
来简化线程的管理。
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2); // 创建固定大小的线程池
executorService.submit(() -> {
System.out.println("任务1");
});
executorService.submit(() -> {
System.out.println("任务2");
});
executorService.shutdown(); // 关闭线程池
}
}
ExecutorService
提供了一个高效的方式来执行异步任务,并且能够在任务完成后进行清理和关闭线程池。