Java并发编程-多线程篇

Java作为一门广泛使用的编程语言,提供了丰富的多线程编程接口和工具,使得开发者能够轻松地创建和管理线程。然而,多线程编程并非易事,它涉及到线程的创建、启动、关闭、同步等多个复杂问题。本文将深入探讨Java中的多线程编程,涵盖线程的基本概念、线程与操作系统的关系、线程的创建和管理方法。

Java中的多线程与操作系统中的多线程

在探讨Java中的多线程与操作系统中的多线程之间的关系时,我们首先需要理解两者在底层实现上的联系。尽管Java语言本身提供了丰富的多线程编程接口,但其底层实现依赖于操作系统的线程机制。

Java语言通过java.lang.Thread类提供了多线程编程的支持。然而,Java虚拟机(JVM)在实现这些线程时,依赖于操作系统的线程机制。具体来说,JVM在大多数操作系统上使用POSIX线程(pthread)库来创建和管理线程。

在Linux系统中,JVM通过调用pthread_create函数来创建线程。pthread_create是POSIX线程库中的一个函数,用于在操作系统级别创建一个新的线程。因此,从底层实现的角度来看,Java中的线程实际上是操作系统线程的封装。

Java线程与操作系统线程之间的关系通常被称为“1对1线程模型”。在这种模型中,每个Java线程都直接映射到一个操作系统线程。这意味着:

  • 资源管理:操作系统负责管理线程的资源分配,如CPU时间、内存等。
  • 调度:操作系统的调度器负责决定哪个线程在何时执行。
  • 上下文切换:线程的上下文切换由操作系统内核完成。

由于Java线程直接映射到操作系统线程,因此Java线程的行为和性能特征在很大程度上受到操作系统线程机制的影响。

使用多线程需要注意的问题

在多线程编程中,确保线程安全是至关重要的。线程安全指的是在多线程环境下,程序能够正确地处理共享数据,避免数据竞争和不一致性问题。Java提供了多种机制来保证线程安全,主要包括原子性、可见性和有序性。

  1. 原子性

原子性是指一个操作要么全部执行,要么全部不执行,不存在中间状态。在多线程环境中,原子性确保了同一时间只有一个线程能够对共享数据进行操作,从而避免了数据竞争。

实现

  • java.util.concurrent.atomic:提供了多种原子类(如AtomicIntegerAtomicLong等),这些类通过CAS(Compare-And-Swap)操作实现原子性。
  • synchronized关键字:通过监视器锁(monitor lock)确保方法或代码块在同一时间只能被一个线程执行。
  1. 可见性

可见性确保一个线程对数据的修改对其他线程是可见的。在多线程环境中,由于CPU缓存和编译器优化,可能会导致一个线程的修改对其他线程不可见。

实现

  • volatile关键字:确保变量的修改立即对所有线程可见,禁止CPU缓存和编译器优化。
  • synchronized关键字:在进入和退出同步块时,确保变量的可见性。

3. 有序性

有序性确保指令按照程序的顺序执行。在多线程环境中,由于编译器和CPU的指令重排序优化,可能会导致指令的执行顺序与程序代码的顺序不一致。

实现

  • volatile关键字:禁止指令重排序,确保变量的读写操作按顺序执行。
  • synchronized关键字:确保同步块内的代码按顺序执行。
  • 使用了happens-before原则来确保有序性。

Java中保证数据一致性的科学方案

在Java应用中,确保数据一致性是关键任务之一。以下是几种科学且专业的方案,旨在帮助读者理解并实施有效的数据一致性策略:

事务管理

事务管理是保证数据一致性的基础手段。通过数据库事务,可以确保一系列数据操作要么全部成功提交,要么全部失败回滚。这一机制依赖于事务的ACID属性:

  • 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,不存在部分执行的情况。
  • 一致性(Consistency):事务执行前后,数据库从一个一致状态转变到另一个一致状态。
  • 隔离性(Isolation):并发执行的事务之间相互隔离,一个事务的执行不会影响其他事务。
  • 持久性(Durability):一旦事务提交,其结果将永久保存在数据库中,即使系统发生故障。

锁机制

锁机制是另一种重要的数据一致性保障手段。通过锁,可以实现对数据的互斥访问,确保同一时间只有一个事务能够对特定数据进行操作。常见的锁类型包括:

  • 共享锁(Shared Lock):允许多个事务同时读取同一数据,但阻止写操作。
  • 排他锁(Exclusive Lock):阻止其他事务读取或写入同一数据,确保数据修改的独占性。

版本控制

版本控制是一种乐观的并发控制策略,通过记录数据的版本信息来保证数据一致性。具体实现方式如下:

  • 乐观锁(Optimistic Locking):在更新数据时,检查数据的版本号。如果版本号与预期一致,则允许更新并递增版本号;否则,拒绝更新操作。

这种策略适用于读多写少的场景,能够减少锁竞争,提高系统并发性能。

如何创建线程

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

  1. 继承 Thread

通过继承 Thread 类并重写 run() 方法来创建线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyThread extends Thread {
@Override
public void run() {
// 编写线程执行逻辑
System.out.println("Thread is running by extending Thread class");
}
}

public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}

优点:简单直接,易于理解和实现。

缺点:由于 Java 不支持多重继承,继承 Thread 类后无法再继承其他类。线程管理和复用性较差。

  1. 实现 Runnable 接口

通过实现 Runnable 接口并重写 run() 方法来创建线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyRunnable implements Runnable {
@Override
public void run() {
// 编写线程执行逻辑
System.out.println("Thread is running by implementing Runnable interface");
}
}

public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}

优点:避免了单继承的限制,可以继承其他类。代码更加灵活,适用于需要实现多线程的场景。

缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

  1. 实现 CallableFutureTask

通过实现 Callable 接口并使用 FutureTask 来创建线程。Callable 接口允许线程返回结果,并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接囗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Thread is running by implementing Callable interface";
}
}

public class Main {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get()); // 获取线程执行结果
}
}

优点:允许线程返回结果,并且可以抛出异常。适用于需要获取线程执行结果的场景。

缺点:代码相对复杂,需要处理 FutureTaskCallable

  1. 使用线程池(Executor)

通过使用 Executor 框架来创建和管理线程池。Executor 框架提供了多种线程池实现,如 FixedThreadPoolCachedThreadPoolSingleThreadExecutor 等。

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(2);//规定线程池容量

executorService.submit(//此处可以是实现了Runnable接口的类
() -> {System.out.println("Thread is running using ExecutorService");}
);
executorService.shutdown();
}
}

优点:提高线程的复用性和管理性,减少线程创建和销毁的开销。适用于需要管理多个线程的场景。

缺点:代码相对复杂,需要理解线程池的概念和配置。

如何启动/关闭线程?

  1. 启动线程

启动线程通过 Thread 类的 start() 方法。

1
2
MyThread t = new MyThread();
t.start();
  1. 关闭线程

关闭线程主要有以下几种方法:

  • 异常停止:通过抛出异常来停止线程。
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
26
class MyThread extends Thread {
@Override
public void run() {
try {
while (true) {
System.out.println("Thread is running");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}

public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
try {
Thread.sleep(5000);
t.interrupt(); // 中断线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

优点:优雅地停止线程,避免资源泄漏。

缺点:需要在线程代码中处理异常。

  • 在沉睡中停止:通过在 sleep() 方法中抛出 InterruptedException 来停止线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted while sleeping");
}
}
}

public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
try {
Thread.sleep(2000);
t.interrupt(); // 中断线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

优点:适用于线程在沉睡中停止的场景。

缺点:需要在线程代码中处理异常。

  • stop() 暴力停止:使用 Thread 类的 stop() 方法来暴力停止线程。
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
26
class MyThread extends Thread {
@Override
public void run() {
try {
while (true) {
System.out.println("Thread is running");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}

public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
try {
Thread.sleep(5000);
t.stop(); // 暴力停止线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

优点:简单直接,易于使用。

缺点:不安全,可能导致资源泄漏或数据不一致。stop() 方法已被弃用,不推荐使用。

  • 使用 return 停止:通过在 run() 方法中使用 return 语句来停止线程。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
class MyThread extends Thread {
private volatile boolean running = true;

public void stopThread() {
running = false;
}

@Override
public void run() {
while (running) {
System.out.println("Thread is running");
// 检查中断状态
if (Thread.currentThread().isInterrupted()) {
System.out.println("Thread interrupted, stopping...");
return; // 退出 run 方法,停止线程
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
System.out.println("Thread stopped");
}
}

public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
try {
Thread.sleep(5000);
t.stopThread(); // 停止线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

优点:优雅地停止线程,避免资源泄漏。适用于需要控制线程停止的场景。

缺点:需要在线程代码中添加停止标志。

调用 interrupt 如何让线程抛出异常

在Java中,线程的中断机制是通过一个布尔属性来表示线程的中断状态。初始状态下,线程的中断状态为 false。当一个线程被其他线程调用 Thread.interrupt() 方法中断时,会根据线程当前的执行状态做出不同的响应。

中断机制的工作原理

  1. 中断状态的设置:当调用 Thread.interrupt() 方法时,目标线程的中断状态会被设置为 true

  2. 响应中断

    • 如果目标线程当前正在执行低级别的可中断方法(如 Thread.sleep()Thread.join()Object.wait()),这些方法会检查线程的中断状态。
    • 如果发现中断状态为 true,这些方法会立即解除阻塞,并抛出 InterruptedException 异常。
  3. 轮询中断状态

    • 如果目标线程当前没有执行可中断方法,Thread.interrupt() 方法仅会设置线程的中断状态为 true
    • 被中断的线程可以通过轮询中断状态(使用 Thread.currentThread().isInterrupted() 方法)来决定是否停止当前正在执行的任务。

示例代码

以下是一个示例,展示了如何通过 interrupt 方法让线程抛出 InterruptedException 异常:

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
26
27
28
29
30
31
public class InterruptExample implements Runnable {

@Override
public void run() {
try {
while (true) {
System.out.println("Thread is running...");

// 模拟耗时操作
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Thread interrupted, stopping...");
// 重新设置中断状态
Thread.currentThread().interrupt();
}
}

public static void main(String[] args) {
Thread thread = new Thread(new InterruptExample());
thread.start();

try {
Thread.sleep(5000); // 主线程等待 5 秒
} catch (InterruptedException e) {
e.printStackTrace();
}

thread.interrupt(); // 中断线程
}
}

关键点解析

  • 可中断方法Thread.sleep()Thread.join()Object.wait() 等方法是可中断的。当线程在这些方法中被中断时,会立即抛出 InterruptedException 异常。
  • 中断状态的轮询:如果线程没有执行可中断方法,可以通过 Thread.currentThread().isInterrupted() 方法轮询中断状态,以决定是否停止当前任务。
  • 重新设置中断状态:在捕获 InterruptedException 异常后,通常需要重新设置中断状态(使用 Thread.currentThread().interrupt()),以确保中断状态不会丢失。

通过调用 Thread.interrupt() 方法,可以设置线程的中断状态,并根据线程当前的执行状态做出不同的响应。如果线程正在执行可中断方法,会立即抛出 InterruptedException 异常;否则,线程可以通过轮询中断状态来决定是否停止当前任务。这种机制确保了线程能够优雅地响应中断请求,避免资源泄露和不一致状态。

Java 线程状态详解

状态

在Java中,线程的生命周期可以通过其状态来描述。线程的状态反映了线程当前的执行情况和行为。Java线程的状态由 Thread.State 枚举类定义,主要包括以下几种状态:

Java线程状态
  1. NEW(新建)
  • 描述:线程对象已经被创建,但尚未调用 start() 方法启动。
  • 状态转换:从 NEW 状态调用 start() 方法后,线程进入 RUNNABLE 状态。
  1. RUNNABLE(可运行)
  • 描述:线程正在Java虚拟机中执行,但它可能正在等待操作系统的其他资源(如CPU时间片)。
  • 状态转换
    • NEW 状态调用 start() 方法后进入 RUNNABLE 状态。刚调用start()等待系统调度时是READY状态,当执行任务时为RUNNING状态。
    • BLOCKEDWAITINGTIMED_WAITING 状态恢复后进入 RUNNABLE 状态。
  1. BLOCKED(阻塞)
  • 描述:线程正在等待获取监视器锁(monitor lock)以进入同步代码块或方法。
  • 状态转换
    • 当线程尝试进入同步代码块或方法但锁已被其他线程占用时,进入 BLOCKED 状态。
    • 当获取到锁后,线程从 BLOCKED 状态进入 RUNNABLE 状态。
  1. WAITING(等待)
  • 描述:线程正在无限期地等待另一个线程执行特定操作(如 Object.wait()Thread.join()LockSupport.park())。
  • 状态转换
    • 通过调用 Object.wait()Thread.join()LockSupport.park() 方法进入 WAITING 状态。
    • 当其他线程调用 Object.notify()Object.notifyAll()LockSupport.unpark() 方法时,线程从 WAITING 状态进入 RUNNABLE 状态。
  1. TIMED_WAITING(计时等待)
  • 描述:线程正在等待另一个线程执行特定操作,但有一个超时时间(如 Thread.sleep(long)Object.wait(long)Thread.join(long)LockSupport.parkNanos(long))。
  • 状态转换
    • 通过调用 Thread.sleep(long)Object.wait(long)Thread.join(long)LockSupport.parkNanos(long) 方法进入 TIMED_WAITING 状态。
    • 当超时时间到达或被其他线程唤醒时,线程从 TIMED_WAITING 状态进入 RUNNABLE 状态。
  1. TERMINATED(终止)
  • 描述:线程已经执行完毕,run() 方法正常退出或抛出未捕获的异常。
  • 状态转换
    • 当线程的 run() 方法执行完毕或抛出未捕获的异常时,线程进入 TERMINATED 状态。

notify 和 notifyAll 的区别

在Java中,notifynotifyAll 都是用于唤醒等待在对象监视器上的线程,但它们的行为有所不同。

notify

  • 功能notify 方法会随机唤醒一个正在等待该对象监视器的线程。
  • 特点:只唤醒一个线程,其他线程仍处于 WAITING 状态。如果其他线程没有被再次调用 notifynotifyAll,它们可能会一直等待,直到超时或被中断。

notifyAll

  • 功能notifyAll 方法会唤醒所有正在等待该对象监视器的线程。
  • 特点:所有线程都会被唤醒,但只有一个线程能获得锁,其他线程会继续竞争锁。所有线程都会从 WAITING 状态进入 RUNNABLE 状态,开始竞争锁。

以下是一个示例代码,展示了 notifynotifyAll 的区别:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class NotifyExample {

public static void main(String[] args) throws InterruptedException {
Object lock = new Object();

Thread thread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread 1 is waiting...");
lock.wait();
System.out.println("Thread 1 is awake.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread thread2 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Thread 2 is waiting...");
lock.wait();
System.out.println("Thread 2 is awake.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

thread1.start();
thread2.start();

Thread.sleep(1000); // 主线程等待 1 秒

synchronized (lock) {
// lock.notify(); // 只唤醒一个线程
lock.notifyAll(); // 唤醒所有线程
}
}
}

wait 和 sleep 的区别

waitsleep 都是用于线程的等待操作,但它们的行为和使用场景有所不同。

wait

  • 所属类Object 类的方法。
  • 功能:使当前线程进入等待状态,直到被唤醒(通过 notifynotifyAll)或超时。
  • 特点
    • 必须在同步代码块(synchronized)中调用。
    • 调用 wait 方法会释放对象的监视器锁(monitor lock)。
    • 线程进入 WAITINGTIMED_WAITING 状态。

sleep

  • 所属类Thread 类的方法。
  • 功能:使当前线程暂停执行指定的时间。
  • 特点
    • 可以在任何地方调用,不需要在同步代码块中。
    • 调用 sleep 方法不会释放对象的监视器锁。
    • 线程进入 TIMED_WAITING 状态。

以下是一个示例代码,展示了 waitsleep 的区别:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class WaitSleepExample {

public static void main(String[] args) throws InterruptedException {
Object lock = new Object();

Thread waitThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Wait thread is waiting...");
lock.wait(2000); // 进入 WAITING 状态,释放锁
System.out.println("Wait thread is awake.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

Thread sleepThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Sleep thread is sleeping...");
Thread.sleep(2000); // 进入 TIMED_WAITING 状态,不释放锁
System.out.println("Sleep thread is awake.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

waitThread.start();
sleepThread.start();

Thread.sleep(1000); // 主线程等待 1 秒

synchronized (lock) {
lock.notify(); // 唤醒 waitThread
}
}
}

notify会唤醒哪个线程?

查看源码注释:

notify()

大致的意思是:

唤醒一个正在等待该对象监视器的单个线程。如果有任何线程正在等待该对象,其中一个线程将被选择并唤醒。选择是任意的,并由实现自行决定。线程通过调用其中一个 wait 方法来等待对象的监视器。

也就是说,依赖于JVM的具体实现。JVM有很多实现,比较流行的就是hotspot,hotspot对notofy0的实现并不是我们以为的随机唤醒,而是“先进先出”的顺序唤醒。