Java并发编程-多线程篇
Java并发编程-多线程篇
xiaoyanJava作为一门广泛使用的编程语言,提供了丰富的多线程编程接口和工具,使得开发者能够轻松地创建和管理线程。然而,多线程编程并非易事,它涉及到线程的创建、启动、关闭、同步等多个复杂问题。本文将深入探讨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提供了多种机制来保证线程安全,主要包括原子性、可见性和有序性。
- 原子性
原子性是指一个操作要么全部执行,要么全部不执行,不存在中间状态。在多线程环境中,原子性确保了同一时间只有一个线程能够对共享数据进行操作,从而避免了数据竞争。
实现
java.util.concurrent.atomic
包:提供了多种原子类(如AtomicInteger
、AtomicLong
等),这些类通过CAS(Compare-And-Swap)操作实现原子性。synchronized
关键字:通过监视器锁(monitor lock)确保方法或代码块在同一时间只能被一个线程执行。
- 可见性
可见性确保一个线程对数据的修改对其他线程是可见的。在多线程环境中,由于CPU缓存和编译器优化,可能会导致一个线程的修改对其他线程不可见。
实现
volatile
关键字:确保变量的修改立即对所有线程可见,禁止CPU缓存和编译器优化。synchronized
关键字:在进入和退出同步块时,确保变量的可见性。
3. 有序性
有序性确保指令按照程序的顺序执行。在多线程环境中,由于编译器和CPU的指令重排序优化,可能会导致指令的执行顺序与程序代码的顺序不一致。
实现
volatile
关键字:禁止指令重排序,确保变量的读写操作按顺序执行。synchronized
关键字:确保同步块内的代码按顺序执行。- 使用了
happens-before
原则来确保有序性。
Java中保证数据一致性的科学方案
在Java应用中,确保数据一致性是关键任务之一。以下是几种科学且专业的方案,旨在帮助读者理解并实施有效的数据一致性策略:
事务管理
事务管理是保证数据一致性的基础手段。通过数据库事务,可以确保一系列数据操作要么全部成功提交,要么全部失败回滚。这一机制依赖于事务的ACID属性:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,不存在部分执行的情况。
- 一致性(Consistency):事务执行前后,数据库从一个一致状态转变到另一个一致状态。
- 隔离性(Isolation):并发执行的事务之间相互隔离,一个事务的执行不会影响其他事务。
- 持久性(Durability):一旦事务提交,其结果将永久保存在数据库中,即使系统发生故障。
锁机制
锁机制是另一种重要的数据一致性保障手段。通过锁,可以实现对数据的互斥访问,确保同一时间只有一个事务能够对特定数据进行操作。常见的锁类型包括:
- 共享锁(Shared Lock):允许多个事务同时读取同一数据,但阻止写操作。
- 排他锁(Exclusive Lock):阻止其他事务读取或写入同一数据,确保数据修改的独占性。
版本控制
版本控制是一种乐观的并发控制策略,通过记录数据的版本信息来保证数据一致性。具体实现方式如下:
- 乐观锁(Optimistic Locking):在更新数据时,检查数据的版本号。如果版本号与预期一致,则允许更新并递增版本号;否则,拒绝更新操作。
这种策略适用于读多写少的场景,能够减少锁竞争,提高系统并发性能。
如何创建线程
在 Java 中,创建线程主要有以下几种方式:
- 继承
Thread
类
通过继承 Thread
类并重写 run()
方法来创建线程。
1 | class MyThread extends Thread { |
优点:简单直接,易于理解和实现。
缺点:由于 Java 不支持多重继承,继承 Thread
类后无法再继承其他类。线程管理和复用性较差。
- 实现
Runnable
接口
通过实现 Runnable
接口并重写 run()
方法来创建线程。
1 | class MyRunnable implements Runnable { |
优点:避免了单继承的限制,可以继承其他类。代码更加灵活,适用于需要实现多线程的场景。
缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
- 实现
Callable
和FutureTask
通过实现 Callable
接口并使用 FutureTask
来创建线程。Callable
接口允许线程返回结果,并且可以抛出异常。要执行Callable任务,需将它包装进一个FutureTask,因为Thread类的构造器只接受Runnable参数,而FutureTask实现了Runnable接囗。
1 | import java.util.concurrent.Callable; |
优点:允许线程返回结果,并且可以抛出异常。适用于需要获取线程执行结果的场景。
缺点:代码相对复杂,需要处理 FutureTask
和 Callable
。
- 使用线程池(Executor)
通过使用 Executor
框架来创建和管理线程池。Executor
框架提供了多种线程池实现,如 FixedThreadPool
、CachedThreadPool
、SingleThreadExecutor
等。
1 | import java.util.concurrent.ExecutorService; |
优点:提高线程的复用性和管理性,减少线程创建和销毁的开销。适用于需要管理多个线程的场景。
缺点:代码相对复杂,需要理解线程池的概念和配置。
如何启动/关闭线程?
- 启动线程
启动线程通过 Thread
类的 start()
方法。
1 | MyThread t = new MyThread(); |
- 关闭线程
关闭线程主要有以下几种方法:
- 异常停止:通过抛出异常来停止线程。
1 | class MyThread extends Thread { |
优点:优雅地停止线程,避免资源泄漏。
缺点:需要在线程代码中处理异常。
- 在沉睡中停止:通过在
sleep()
方法中抛出InterruptedException
来停止线程。
1 | class MyThread extends Thread { |
优点:适用于线程在沉睡中停止的场景。
缺点:需要在线程代码中处理异常。
stop()
暴力停止:使用Thread
类的stop()
方法来暴力停止线程。
1 | class MyThread extends Thread { |
优点:简单直接,易于使用。
缺点:不安全,可能导致资源泄漏或数据不一致。stop()
方法已被弃用,不推荐使用。
- 使用
return
停止:通过在run()
方法中使用return
语句来停止线程。
1 | class MyThread extends Thread { |
优点:优雅地停止线程,避免资源泄漏。适用于需要控制线程停止的场景。
缺点:需要在线程代码中添加停止标志。
调用 interrupt
如何让线程抛出异常
在Java中,线程的中断机制是通过一个布尔属性来表示线程的中断状态。初始状态下,线程的中断状态为 false
。当一个线程被其他线程调用 Thread.interrupt()
方法中断时,会根据线程当前的执行状态做出不同的响应。
中断机制的工作原理
中断状态的设置:当调用
Thread.interrupt()
方法时,目标线程的中断状态会被设置为true
。响应中断:
- 如果目标线程当前正在执行低级别的可中断方法(如
Thread.sleep()
、Thread.join()
或Object.wait()
),这些方法会检查线程的中断状态。 - 如果发现中断状态为
true
,这些方法会立即解除阻塞,并抛出InterruptedException
异常。
- 如果目标线程当前正在执行低级别的可中断方法(如
轮询中断状态:
- 如果目标线程当前没有执行可中断方法,
Thread.interrupt()
方法仅会设置线程的中断状态为true
。 - 被中断的线程可以通过轮询中断状态(使用
Thread.currentThread().isInterrupted()
方法)来决定是否停止当前正在执行的任务。
- 如果目标线程当前没有执行可中断方法,
示例代码
以下是一个示例,展示了如何通过 interrupt
方法让线程抛出 InterruptedException
异常:
1 | public class InterruptExample implements Runnable { |
关键点解析
- 可中断方法:
Thread.sleep()
、Thread.join()
和Object.wait()
等方法是可中断的。当线程在这些方法中被中断时,会立即抛出InterruptedException
异常。 - 中断状态的轮询:如果线程没有执行可中断方法,可以通过
Thread.currentThread().isInterrupted()
方法轮询中断状态,以决定是否停止当前任务。 - 重新设置中断状态:在捕获
InterruptedException
异常后,通常需要重新设置中断状态(使用Thread.currentThread().interrupt()
),以确保中断状态不会丢失。
通过调用 Thread.interrupt()
方法,可以设置线程的中断状态,并根据线程当前的执行状态做出不同的响应。如果线程正在执行可中断方法,会立即抛出 InterruptedException
异常;否则,线程可以通过轮询中断状态来决定是否停止当前任务。这种机制确保了线程能够优雅地响应中断请求,避免资源泄露和不一致状态。
Java 线程状态详解
状态
在Java中,线程的生命周期可以通过其状态来描述。线程的状态反映了线程当前的执行情况和行为。Java线程的状态由 Thread.State
枚举类定义,主要包括以下几种状态:
- NEW(新建)
- 描述:线程对象已经被创建,但尚未调用
start()
方法启动。 - 状态转换:从
NEW
状态调用start()
方法后,线程进入RUNNABLE
状态。
- RUNNABLE(可运行)
- 描述:线程正在Java虚拟机中执行,但它可能正在等待操作系统的其他资源(如CPU时间片)。
- 状态转换:
- 从
NEW
状态调用start()
方法后进入RUNNABLE
状态。刚调用start()
等待系统调度时是READY
状态,当执行任务时为RUNNING
状态。 - 从
BLOCKED
、WAITING
、TIMED_WAITING
状态恢复后进入RUNNABLE
状态。
- 从
- BLOCKED(阻塞)
- 描述:线程正在等待获取监视器锁(monitor lock)以进入同步代码块或方法。
- 状态转换:
- 当线程尝试进入同步代码块或方法但锁已被其他线程占用时,进入
BLOCKED
状态。 - 当获取到锁后,线程从
BLOCKED
状态进入RUNNABLE
状态。
- 当线程尝试进入同步代码块或方法但锁已被其他线程占用时,进入
- WAITING(等待)
- 描述:线程正在无限期地等待另一个线程执行特定操作(如
Object.wait()
、Thread.join()
或LockSupport.park()
)。 - 状态转换:
- 通过调用
Object.wait()
、Thread.join()
或LockSupport.park()
方法进入WAITING
状态。 - 当其他线程调用
Object.notify()
、Object.notifyAll()
或LockSupport.unpark()
方法时,线程从WAITING
状态进入RUNNABLE
状态。
- 通过调用
- 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
状态。
- 通过调用
- TERMINATED(终止)
- 描述:线程已经执行完毕,
run()
方法正常退出或抛出未捕获的异常。 - 状态转换:
- 当线程的
run()
方法执行完毕或抛出未捕获的异常时,线程进入TERMINATED
状态。
- 当线程的
notify 和 notifyAll 的区别
在Java中,notify
和 notifyAll
都是用于唤醒等待在对象监视器上的线程,但它们的行为有所不同。
notify
- 功能:
notify
方法会随机唤醒一个正在等待该对象监视器的线程。 - 特点:只唤醒一个线程,其他线程仍处于
WAITING
状态。如果其他线程没有被再次调用notify
或notifyAll
,它们可能会一直等待,直到超时或被中断。
notifyAll
- 功能:
notifyAll
方法会唤醒所有正在等待该对象监视器的线程。 - 特点:所有线程都会被唤醒,但只有一个线程能获得锁,其他线程会继续竞争锁。所有线程都会从
WAITING
状态进入RUNNABLE
状态,开始竞争锁。
以下是一个示例代码,展示了 notify
和 notifyAll
的区别:
1 | public class NotifyExample { |
wait 和 sleep 的区别
wait
和 sleep
都是用于线程的等待操作,但它们的行为和使用场景有所不同。
wait
- 所属类:
Object
类的方法。 - 功能:使当前线程进入等待状态,直到被唤醒(通过
notify
或notifyAll
)或超时。 - 特点:
- 必须在同步代码块(
synchronized
)中调用。 - 调用
wait
方法会释放对象的监视器锁(monitor lock)。 - 线程进入
WAITING
或TIMED_WAITING
状态。
- 必须在同步代码块(
sleep
- 所属类:
Thread
类的方法。 - 功能:使当前线程暂停执行指定的时间。
- 特点:
- 可以在任何地方调用,不需要在同步代码块中。
- 调用
sleep
方法不会释放对象的监视器锁。 - 线程进入
TIMED_WAITING
状态。
以下是一个示例代码,展示了 wait
和 sleep
的区别:
1 | public class WaitSleepExample { |
notify会唤醒哪个线程?
查看源码注释:
大致的意思是:
唤醒一个正在等待该对象监视器的单个线程。如果有任何线程正在等待该对象,其中一个线程将被选择并唤醒。选择是任意的,并由实现自行决定。线程通过调用其中一个 wait
方法来等待对象的监视器。
也就是说,依赖于JVM的具体实现。JVM有很多实现,比较流行的就是hotspot,hotspot对notofy0的实现并不是我们以为的随机唤醒,而是“先进先出”的顺序唤醒。