线程

区分程序,线程和进程

项目 定义 特点 示例
程序Program 是一组指令的集合,描述了完成特定任务的步骤和操作,通常以源代码的形式编写。是静态的,本身不具备执行能力。 不占用运行时系统资源(内存,CPU等)。没有生命周期的概念,只要存储介质存在就能长期保存。不能被操作系统直接调度执行。 一个用Java编写的Hello World程序,保存为.java后缀的文件,在未运行时就是程序。
进程Process 是计算机中正在运行的程序的实例,是程序在其自身地址空间中的一次执行活动。当程序被加载到内存并由操作系统分配资源后开始执行,就形成了进程。 资源分配:是资源申请、调度和独立运行的单位,操作系统为其分配内存空间、CPU时间片、文件描述符等系统资源。例如每个进程都有自己独立的虚拟地址空间,用于存放代码、数据等。
动态性:有生命周期,从创建(程序加载执行时创建)开始,到终止(程序执行完毕或异常结束)为止,期间会经历不同状态,如就绪、运行、阻塞。
独立性:不同进程的工作相互独立,拥有各自独立的资源,进程间通常不能直接访问彼此资源。
开销较大:创建、撤销进程以及进程间上下文切换时,系统开销相对较大。
运行一个Java程序,操作系统会为其创建一个进程,该进程有自己独立的内存区域,执行程序中的代码逻辑。
线程Thread 是进程中的执行单元,也可看作是进程中独立的控制流。一个进程可以包含多个线程,它们共享进程的内存空间和系统资源,是操作系统调度的基本单位。 共享资源:同一进程内的线程共享进程的资源,如代码段、数据段、堆内存等,但每个线程有自己独立的栈空间,用于存放局部变量、方法调用等信息。
轻量级:相较于进程,线程创建、撤销和切换的开销小很多,因为线程不需要像进程那样分配大量独立资源。
并发性:多个线程可并发执行,提高程序执行效率。例如一个浏览器进程中,可同时存在负责页面渲染、网络请求、用户交互等不同功能的多个线程。
受进程制约:线程不能独立存在,必须属于某个进程,一个进程中的线程崩溃可能导致整个进程受影响。
在Java多线程编程中,通过继承Thread类或实现Runnable接口创建多个线程,在一个进程内并发执行不同任务。

创建线程

在Java中,创建线程常见有以下几种方式:

继承Thread类

通过继承Thread类并重写其run()方法来定义线程执行的任务。步骤如下:

  1. 定义Thread类的子类,并重写run()方法,run()方法中的代码即为线程执行体。
  2. 创建Thread子类的实例,即线程对象。
  3. 调用线程对象的start()方法启动线程,使其进入就绪状态,等待CPU调度执行run()方法。
    示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
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();
}
}

优点是简单直观,适合初学者理解线程原理;缺点是Java单继承特性限制了该类再继承其他类,扩展性受限。

实现Runnable接口

实现Runnable接口来定义线程任务,再借助Thread类启动线程。步骤为:

  1. 定义实现Runnable接口的类,重写run()方法编写任务逻辑。
  2. 创建Runnable实现类的实例。
  3. 以该实例为参数创建Thread对象,该Thread对象才是实际执行的线程。
  4. 调用Thread对象的start()方法启动线程。
1
2
3
4
5
6
7
8
9
10
11
12
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程正在运行");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}

此方式优势在于任务逻辑和线程对象解耦,避免单继承限制,可继承其他类,还能让多个线程共享同一Runnable实例,适合多线程处理同一份资源;不足是需额外创建Thread对象。

实现Callable接口

Callable接口在Java 5引入,类似Runnable,但call()方法可返回值和抛出异常。创建步骤:

  1. 创建实现Callable接口的类,实现call()方法定义线程执行体及返回值。
  2. 创建Callable实现类的实例。
  3. FutureTask类包装Callable对象,FutureTask实现了FutureRunnable接口,能获取call()方法返回值。
  4. FutureTask对象为参数创建Thread对象并启动。
  5. 调用FutureTask对象的get()方法获取线程执行结束后的返回值,调用时可能阻塞,直到线程执行完毕。
    示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行完成";
}
}
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}

适用于线程需返回结果场景,如数据查询、复杂计算;缺点是代码复杂度比Runnable略高。

使用匿名内部类

可使用匿名内部类创建线程,代码简洁,常用于线程逻辑简单时。

继承Thread类方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AnonymousInnerClassThreadExample {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("匿名内部类线程正在运行,当前计数: " + i);
}
}
};
thread.start();
//主线程也可以有自己的逻辑
for (int i = 0; i < 3; i++) {
System.out.println("主线程正在运行,当前计数: " + i);
}
}
}

实现Runnable接口方式

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程正在运行");
}
});
thread.start();
}
}

使用Lambda表达式(Java 8及以上版本)

Java 8及后续版本可用Lambda表达式简化线程创建,语法更简洁。

实现Runnable接口

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("线程正在运行"));
thread.start();
}
}

使用线程池结合Lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable task = () -> System.out.println("线程正在执行任务");
for (int i = 0; i < 5; i++) {
executorService.execute(task);
}
executorService.shutdown();
}
}

使用线程池

线程池是管理线程的机制,可复用线程,减少创建和销毁开销,提高性能和资源利用率。Java通过Executor框架及ThreadPoolExecutor类实现线程池创建与管理。以Executors工具类创建常见线程池示例:

创建固定大小线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在执行任务"));
}
executorService.shutdown();
}
}

创建可缓存线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在执行任务"));
}
executorService.shutdown();
}
}

创建单线程线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + " 正在执行任务"));
}
executorService.shutdown();
}
}

创建支持定时及周期性任务执行的线程池

1
2
3
4
5
6
7
8
9
10
11
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Main {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
executorService.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + " 执行周期性任务"), 0, 1, TimeUnit.SECONDS);
}
}

线程生命周期

线程生命周期

  1. 新建状态(New):当使用 new 关键字创建一个 Thread 对象时,线程就进入了新建状态。此时线程仅仅是在内存中被分配了空间并初始化了一些基本信息,但还没有开始执行。
  2. 就绪状态(Runnable):调用线程的 start() 方法后,线程进入就绪状态。在这个状态下,线程已经准备好执行,等待操作系统的线程调度器分配 CPU 时间片。
  3. 运行状态(Running):当线程调度器为处于就绪状态的线程分配了 CPU 时间片后,线程就进入运行状态,开始执行 run() 方法中的代码。
  4. 阻塞状态(Blocked):在运行过程中,线程可能会因为某些原因(如等待 I/O 操作完成、等待获取对象锁、调用 sleep() 或 wait() 方法等)进入阻塞状态。在阻塞状态下,线程暂时停止执行,直到阻塞条件解除。
  5. 死亡状态(Terminated):线程执行完 run() 方法中的代码,或者因为异常退出 run() 方法,线程就进入死亡状态。一旦线程进入死亡状态,就不能再重新启动。

多线程

多线程是指在一个程序中同时运行多个线程,每个线程都可以独立执行不同的任务,从而提高程序的执行效率和响应能力。

基本概念

  • 线程:是程序执行的最小单位,是进程中的一个执行路径。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等。
  • 多线程:允许在一个程序中并发执行多个线程,每个线程都有自己的执行上下文,包括程序计数器、栈和局部变量等。

优势

  • 提高程序的执行效率:可以将一个复杂的任务分解为多个子任务,分别由不同的线程来执行,充分利用多核处理器的并行性,从而加快整个任务的完成速度。
  • 增强程序的响应能力:在图形界面应用程序中,使用多线程可以避免界面在执行长时间任务时出现卡顿现象。例如,将文件下载、数据处理等耗时操作放在单独的线程中执行,而主线程则可以继续处理用户界面的交互,提高用户体验。
  • 更好地利用系统资源:多线程可以使程序在等待某些操作完成(如I/O操作)时,利用等待时间执行其他任务,从而提高系统资源的利用率。

实现方式

  • 继承Thread类:通过创建一个继承自 Thread 类的子类,并重写 run() 方法来定义线程的执行逻辑。然后创建该子类的实例,并调用 start() 方法启动线程。
  • 实现Runnable接口:定义一个实现 Runnable 接口的类,实现 run() 方法。将该实现类的实例作为参数传递给 Thread 类的构造函数,创建线程对象并启动线程。这种方式比继承 Thread 类更灵活,因为它避免了Java单继承的限制,并且可以让多个线程共享同一个 Runnable 实例。
  • 使用Callable和Future接口Callable 接口类似于 Runnable 接口,但 Callablecall() 方法可以有返回值,并且可以抛出异常。通过 ExecutorService 提交 Callable 任务,会返回一个 Future 对象,用于获取任务的执行结果。

线程同步与互斥

  • 线程同步:当多个线程访问共享资源时,为了保证数据的一致性和完整性,需要对线程的访问进行同步控制。Java中提供了多种线程同步机制,如 synchronized 关键字、ReentrantLock 类等。
  • 线程互斥:是指在同一时刻,只允许一个线程访问某个共享资源,其他线程必须等待。通过使用互斥锁来实现线程互斥,确保共享资源在任何时刻都只能被一个线程访问。

线程通信

  • 多个线程之间可以通过共享变量、等待 - 通知机制等方式进行通信。例如,使用 wait()notify()notifyAll() 方法实现线程之间的等待和通知,使得一个线程可以在满足某些条件时通知其他线程继续执行。

线程同步和锁

在多线程编程里,线程同步和锁机制是保障线程安全、避免数据不一致问题的关键技术。

线程同步

概念

线程同步是指在多线程环境下,对多个线程访问共享资源的操作进行协调,保证在同一时刻只有一个线程能够访问共享资源,以此避免多个线程同时操作共享资源引发的数据竞争和不一致问题。

必要性

当多个线程并发访问和修改共享资源时,可能会出现数据不一致的情况。例如,多个线程同时对一个计数器进行自增操作,若没有同步机制,就可能会出现计数错误。

实现方式

synchronized 关键字:这是 Java 内置的线程同步机制,可用于修饰方法或代码块,在方法声明中使用synchronized关键字,同一时刻仅允许一个线程访问该方法。

1
2
3
4
5
6
7
public class SynchronizedExample {
private int count = 0;

public synchronized void increment() {
count++;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//使用synchronized关键字修饰代码块时需要指定一个对象作为锁
public class SynchronizedBlockExample {
private int count = 0;
private final Object lock = new Object();

public void increment() {
synchronized (lock) {
count++;
}
}
}

锁 Lock

概念

Lock 是 Java 5 引入的一个接口,它提供了比 synchronized 关键字更灵活、更强大的线程同步机制。Lock 接口定义了获取锁和释放锁的方法,允许开发者手动控制锁的获取和释放。

常用实现类

ReentrantLock:可重入锁,这是 Lock 接口的一个常用实现类。它支持与 synchronized 相同的语义,并且提供了更多的功能,如可中断的锁获取、超时锁获取等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
private int count = 0;
private final Lock lock = new ReentrantLock();

public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}

ReadWriteLock:读写锁,它维护了一对锁,一个读锁和一个写锁。多个线程可以同时获取读锁,但在写锁被获取时,其他线程不能获取读锁或写锁。读写锁适用于读多写少的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
private int data = 0;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

public int readData() {
rwLock.readLock().lock();
try {
return data;
} finally {
rwLock.readLock().unlock();
}
}

public void writeData(int newData) {
rwLock.writeLock().lock();
try {
data = newData;
} finally {
rwLock.writeLock().unlock();
}
}
}

Locksynchronized 的比较

  • 灵活性Lock 接口提供了更灵活的锁获取和释放方式,例如可中断的锁获取、超时锁获取等,而 synchronized 关键字的锁获取和释放是隐式的,缺乏这种灵活性。
  • 性能:在高并发场景下,Lock 接口的性能通常优于 synchronized 关键字,因为 Lock 接口可以通过一些优化策略来减少锁的竞争。
  • 可重入性synchronizedReentrantLock 都支持可重入性,但 Lock 接口的可重入性更加灵活,可以设置重入次数等。
  • 锁的公平性ReentrantLock 可以设置为公平锁,即按照线程请求锁的顺序来分配锁,而 synchronized 关键字是非公平锁。