👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者
📕系列专栏:多线程及高并发系列
📕其他专栏:微服务框架系列、MySQL系列、Redis系列、Leetcode算法系列、GraphQL系列
📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦❤️
✨时间是条环形跑道,万物终将归零,亦得以圆全完美
线程基础及线程中断同步
- 进程/线程/虚拟线程
- 线程状态
- 线程使用方式
- 继承 Thread
- 实现 Runnable 接口
- 实现 Callable 接口
- 线程机制
- 基础方法
- setDaemon
- yield
- sleep
- 线程中断
- interrupt
- interrupted
- 线程同步
- synchronized
- ReentranLock
- 线程协作
多线程及高并发系列
- 【多线程及高并发 一】内存模型及理论基础
线程(Thread):线程是进程内的执行单元,它是操作系统调度的最小单位。一个进程可以包含多个线程,它们共享进程的资源。线程之间可以并发执行,共享内存空间,因此可以更高效地完成多个任务。Java 中的线程由 Java 虚拟机(JVM)进行管理和调度
多线程的作用:
- 提高程序性能:通过利用多核处理器或多处理器系统的并行性,多线程可以同时执行多个任务,从而提高程序的处理能力和执行效率
- 提升用户体验:在需要进行耗时操作的情况下,使用多线程可以避免长时间的阻塞,保持用户界面的响应性
- 实现异步编程:多线程可以实现异步编程模型,通过在后台执行任务,提供更好的用户体验和系统响应能力
- 支持并发处理:在服务器端应用程序中,多线程可以帮助同时处理多个客户端请求,提高系统的并发性和吞吐量
- 充分利用资源:多线程可以充分利用计算机的硬件资源,例如 CPU、内存和磁盘等
多线程编程也带来了一些挑战,例如线程安全性、数据同步和共享资源管理等问题。Java 提供了丰富的并发编程工具和库,如线程类(Thread)、线程池(ThreadPoolExecutor)、锁(Lock)、原子类(Atomic)等,帮助开发人员更方便地进行多线程编程。
进程/线程/虚拟线程
- 进程(Process):进程是计算机系统中运行的程序的实例。它是资源分配的最小单位,包括内存空间、文件句柄、打开的网络连接等
- 操作系统线程(OS Thread):线程由操作系统管理,是进程内的执行单元,它是操作系统调度的最小单位
- 平台线程(Platform Thread):
Java.Lang.Thread
类的每个实例,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射 - 虚拟线程(Virtual Thread):一种轻量级,由 JVM 管理的线程。对应的实例
java.lang.VirtualThread
这个类 - 载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程
虚拟线程(Virtual Thread)它不与特定的操作系统线程相绑定。它在平台线程上运行 Java 代码,但在代码的整个生命周期内不独占平台线程。**这意味着许多虚拟线程可以在同一个平台线程上运行他们的 Java 代码,共享同一个平台线程。详见虚拟线程原理及性能分析
线程状态
- 新建状态(New):当线程对象被创建时,它处于新建状态。此时线程尚未启动,还未调用start()方法。
- 运行状态(Runnable):在线程启动后,它进入运行状态。线程可以在多个线程中竞争处理器资源,但具体的执行顺序由调度器决定。
- 阻塞状态(Blocked):线程在某些条件下暂停执行,进入阻塞状态。常见的情况包括等待获取锁、等待I/O操作完成、等待其他线程的通知等。当条件满足时,线程会重新进入就绪状态。
- 等待状态(Waiting):线程进入等待状态是出于某些条件的需要,线程会主动停止执行,直到满足特定的条件才会被唤醒。线程可以通过调用Object.wait()、Thread.join()或LockSupport.park()等方法进入等待状态。
- 超时等待状态(Timed Waiting):线程在特定的时间范围内等待,如果在指定的时间内未满足条件,线程会自动唤醒。线程可以通过调用Thread.sleep()、Object.wait(timeout)、Thread.join(timeout)或LockSupport.parkNanos()等方法进入超时等待状态。
- 终止状态(Terminated):线程执行完任务或者由于异常等原因终止执行,进入终止状态。一旦线程进入终止状态,它将不再执行
线程使用方式
Java中有三种常见的线程使用方式:
- 继承 Thread 类:适合简单的线程任务,不需要额外的线程控制
- 实现 Runnable 接口:适合于需要执行的任务不需要返回结果的情况
- 实现 Callable 接口:适合需要执行任务并且获取返回结果的情况
实现
Runnable
和Callable
接口的类实际上是任务,最后还需要通过Thread
来执行
继承 Thread
创建一个继承自Thread
类的子类,重写其run()
方法。然后可以创建该子类的实例,并调用start()
方法来启动线程
这种方式适合简单的线程任务,不需要额外的线程控制
public class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
实现 Runnable 接口
创建一个实现了Runnable
接口的类,重写其run()
方法。然后可以创建一个Thread
对象,将该实现类的实例作为参数传递给Thread
构造函数。最后调用Thread
对象的start()
方法来启动线程
这种方式适合于需要执行的任务不需要返回结果的情况
public class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
实现 Callable 接口
创建一个实现了Callable
接口的类,重写其call()
方法。然后可以创建一个FutureTask
对象,将该实现类的实例作为参数传递给FutureTask
构造函数,最后通过FutureTask
对象可以获取任务执行的结果
这种方式适合需要执行任务并且获取返回结果的情况
public class MyCallable implements Callable<String> {
public String call() {
// 线程执行的代码
return "Hello, World!";
}
}
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
线程机制
在 Java 中,Thread 类提供了方法及工具来控制线程的行为
- 基础方法:
setDaemon(boolean on)
、yield()
和sleep(long millis)
- 线程中断:
interrupted()
- 线程同步:
synchronized
、ReentranLock
- 线程协助:
join()
、wait()
、notify()
和notifyAll()
基础方法
setDaemon
作用:将线程设置为守护线程(daemon thread)或用户线程(user thread)
- 守护线程是在后台提供服务的线程。当所有的用户线程结束时,守护线程也会自动结束。典型的守护线程包括垃圾回收线程(Garbage Collector)
- 用户线程是在前台执行的线程,不会影响 JVM 的关闭。守护线程的存在并不会阻止 JVM 退出
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon Thread is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
System.out.println("Main Thread is exiting");
}
}
守护线程
daemonThread
,它会不停地输出一条消息。直到主线程(即 main 方法)退出时,守护线程也会随之自动结束
yield
作用:提示调度器当前线程愿意放弃当前的 CPU 时间片,让其他具有相同优先级的线程执行。
调用yield()
方法不会导致线程进入阻塞状态,而是将线程从运行状态转换为就绪状态
yield()
方法是对线程调度器的一个建议,它在一定程度上提高了线程之间的公平性
public class YieldExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread 1: " + i);
Thread.yield();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread 2: " + i);
Thread.yield();
}
});
thread1.start();
thread2.start();
}
}
尽可能交替执行输出,但不保证,因为
yield()
的线程也可能又被CPU调度
sleep
作用:使当前线程暂停执行指定的时间,进入阻塞状态。
参数:millis
指定线程休眠的时间(以毫秒为单位)
sleep()
方法会暂时释放 CPU,使得其他线程有机会执行。在指定的时间过去后,线程会重新进入就绪状态,等待重新获得 CPU 执行
public class SleepExample {
public static void main(String[] args) {
System.out.println("Before sleep");
try {
Thread.sleep(3000); // 休眠 3 秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("After sleep");
}
}
线程中断
interrupt
Java 提供了 Thread 类的interrupt()
方法来中断线程的执行。
调用interrupt()
方法会将线程的中断标志位设置为true
- 如果线程处于阻塞状态(如调用了
sleep()
、wait()
或join()
方法),会抛出InterruptedException
异常并清除中断标志位 - 如果线程处于非阻塞状态,在适当的时机需要检查线程的中断标志位,并采取相应的处理逻辑
interrupted
作用:静态方法,检查当前线程的中断状态,并重置中断标志位。通常在当前线程需要处理中断状态时使用
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run() {
while (!interrupted()) {
// ..
}
System.out.println("Thread end");
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new MyThread2();
thread2.start();
thread2.interrupt();
}
}
如果一个线程的run()
方法执行一个无限循环,并且没有执行sleep()
等会抛出 InterruptedException 的操作,那么调用线程的interrupt()
方法就无法使线程提前结束。此时需要interrupted()
方法来判断线程是否处于中断状态,从而提前结束线程
线程同步
synchronized
Java 中的synchronized
关键字用于实现线程同步,确保在同一时间只有一个线程可以进入被 synchronized 修饰的方法或代码块,从而防止多个线程同时访问共享资源,避免出现数据竞争和并发访问的问题
synchronized
原理分析、锁位置、锁状态及锁升级详解见【多线程及高并发 二】volatile & synchorized 详解
- 修饰实例方法,为当前实例加锁,进入同步方法前要获得当前实例的锁
- 修饰静态方法,为当前类对象加锁,进入同步方法前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
synchronized
关键字在JDK 1.5 前本质上是一把悲观锁。JDK 1.5 之后进行了优化,引入了锁升级的概念。在多线程竞争不激烈的情况下,锁会从无锁状态逐渐升级为偏向锁、轻量级锁,最后升级为重量级锁,以提高性能
ReentranLock
ReentrantLock
是 Java 提供的可重入锁(Reentrant Lock)实现,它是在java.util.concurrent.locks
包中的一个类。与传统的synchronized
关键字相比,ReentrantLock
提供了更多可编程的灵活性和功能,例如可重入性、公平性、条件变量和更精细的线程控制
- 可重入性:ReentrantLock 是可重入锁,意味着同一个线程可以多次获取同一个锁而不会造成死锁
- 加锁和解锁:使用 ReentrantLock,可以使用
lock()
方法进行加锁,使用unlock()
方法进行解锁 - 公平性和非公平性:ReentrantLock 可以构造为公平锁或非公平锁。公平锁会按照线程请求锁的顺序分配锁,而非公平锁则允许插队,可能会导致某些线程长时间等待
Condition
条件变量:ReentrantLock 提供了与 CONDITION 相关联的条件变量,用于实现更复杂的线程通信和同步
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private int count = 0;
public void increment() {
lock.lock(); // 加锁
try {
count++;
System.out.println("Incremented: " + count);
condition.signalAll(); // 唤醒等待的线程
} finally {
lock.unlock(); // 解锁
}
}
public void decrement() throws InterruptedException {
lock.lock(); // 加锁
try {
while (count == 0) {
condition.await(); // 等待条件满足
}
count--;
System.out.println("Decremented: " + count);
} finally {
lock.unlock(); // 解锁
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Thread incrementThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
});
Thread decrementThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
example.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
incrementThread.start();
decrementThread.start();
}
}
在上述示例中创建了一个ReentrantLock
对象lock
和一个与之关联的Condition
对象condition
increment()
方法使用lock
加锁,递增count
的值,并唤醒等待的线程decrement()
方法使用lock
加锁,当count
为 0 时,调用condition.await()
方法等待条件满足,否则递减count
的值
线程协作
在Java中,Thread
类提供了几个用于线程间协作的方法,包括join()
、wait()
、notify()
和notifyAll()
。这些方法用于实现线程的等待、唤醒和协调操作
Thread 类的
wait()
、notify()
和notifyAll()
方法是与对象的监视器(monitor)相关联的,而不是直接与 Thread 类相关联。这些方法是基于对象的锁机制实现的,用于线程间的协调和通信
join()
方法用于等待调用线程完成其执行,然后再继续执行当前线程
- 调用某个线程的
join()
方法会使当前线程进入阻塞状态,直到被调用线程执行完毕 - 如果在
join()
方法中指定了超时时间,当前线程最多会等待指定的时间,然后继续执行 join()
方法通常与多线程的任务分割和结果合并中使用
wait()
、notify()
和notifyAll()
这三个方法是用于线程间的等待和唤醒机制,需要在同步代码块或同步方法中使用。被唤醒的线程会重新竞争对象的锁,一旦获得锁,就可以继续执行
wait()
方法使当前线程进入等待状态,放弃对象的锁,并等待其他线程调用相同对象的notify()
或notifyAll()
方法来唤醒它notify()
方法唤醒在相同对象上调用wait()
方法并进入等待状态的单个线程。notifyAll()
方法唤醒在相同对象上调用wait()
方法并进入等待状态的所有线程
wait()
方法在调用前需要先获得锁,否则会抛出IllegalMonitorStateException
异常
public class ThreadCooperationExample {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread 1 starts");
Thread.sleep(2000);
System.out.println("Thread 1 notifies");
lock.notify(); // 唤醒等待的线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread 2 starts");
lock.wait(); // 等待被唤醒
System.out.println("Thread 2 continues");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
thread1.join(); // 等待 thread1 执行完毕
thread2.join(); // 等待 thread2 执行完毕
System.out.println("Main thread finishes");
}
}
在上述示例中创建了两个线程 thread1 和 thread2。thread1 在执行过程中调用lock.notify()
方法唤醒等待的线程,而 thread2 在执行过程中调用lock.wait()
方法等待被唤醒。main 线程使用join()
方法等待 thread1 和 thread2 执行完毕后再继续执行
参考资料:
- Java 并发编程实战
- 虚拟线程原理及性能分析