进程和线程
进程
进程是计算机中正在运行的程序的实例。每个进程都有自己的地址空间、内存和系统资源(如文件句柄、设备状态等)。进程是独立的实体,可由操作系统进行创建、调度和终止。每个进程都在自己的地址空间中执行,彼此之间不共享内存。进程之间通常通过进程间通信机制如管道、消息队列、共享内存等来进行数据交换和同步。
线程
线程是进程中的一个执行单元,也被称为轻量级进程。一个进程可以包含多个线程,这些线程共享相同的地址空间和系统资源。线程在同一进程中的各个线程之间共享内存,可以直接访问进程的全局变量和堆内存。线程之间的切换比进程之间的切换要快速,因为它们共享相同的上下文。线程间通信直接读写共享变量,更加简单直接。线程间也可以通过锁、信号量等机制进行同步,避免竞态条件和数据不一致问题。
并发和并行
并发
并发指的是多个任务可以在同一时间段内被启动和执行,它们共享同一个CPU处理器,通过轮流使用CPU时间片来模拟同时进行的效果。并发可以在单个处理器上实现。
并行
并行指多个任务同时执行在不同的处理器上,每个任务都有自己的处理器来执行它们。并行需要一些特定的硬件和软件支持,如多核处理器、分布式系统和并行编程框架等。
线程的类型
在Java中,线程主要有两种类型:守护线程(Daemon Thread)和用户线程(User Thread)。
守护线程
守护线程是一种后台线程,它为其他前台线程提供服务。典型的守护线程包括垃圾回收器和JIT(即时)编译器等系统级任务。这些线程的生命期依赖于用户线程的存在;一旦所有非守护线程(即用户线程)终止,JVM就会退出,而不论守护线程是否仍在运行。因此,如果需要在线程结束前执行一些清理工作,比如关闭文件或网络连接,那么不应该使用守护线程,因为它们可能会被强制终止,没有机会执行finally
块中的代码,从而导致资源泄露。
为了将一个线程指定为守护线程,可以在调用start()
方法之前通过Thread.setDaemon(true)
来设置。值得注意的是,这个属性必须在启动线程之前设定,一旦线程开始运行,它的守护状态就不能再改变。
用户线程
相比之下,用户线程代表了程序的主要逻辑和任务。它们是Java应用程序的核心组成部分,大多数情况下,开发者创建的线程都是用户线程。当所有的用户线程完成其工作并终止后,如果没有其他用户线程在运行,JVM将会结束进程。默认情况下,通过Thread
类或其子类实例化的新线程都自动作为用户线程启动。
用户线程享有完整的生命周期管理,确保了即使在程序即将结束时也能正确地执行清理操作。因此,对于需要保证一定顺序或需要执行重要收尾工作的任务,应该总是使用用户线程。
线程的实现
线程的实现方式
Java线程的实现方式主要有三种:内核线程实现、用户线程实现以及两者的混合实现。
内核线程实现(1:1实现)
内核线程(Kernel Level Thread, KLT)是由操作系统内核直接支持的线程。它们通过操作系统的调度器进行调度,并将任务映射到处理器上。
- 轻量级进程(LWP):通常,程序不会直接使用内核线程,而是使用轻量级进程作为高级接口。轻量级进程与内核线程之间是一对一的关系。
- 优点:利用了操作系统的线程调度,能够有效利用多核处理器。大多数现代操作系统使用此模型。
- 缺点:线程操作(创建、销毁、同步)需要系统调用,导致用户态与内核态之间的切换,消耗较大。
用户线程实现(1:N实现)
用户线程(User Thread, UT)完全在用户空间中实现,不需要内核的支持,其创建、调度和销毁完全在用户态完成。
- 优点:线程操作无需系统调用,避免了用户态与内核态之间的切换,因此速度快,消耗低,可以支持大量线程。
- 缺点:没有内核支持,线程间的同步和通信较复杂。当某个线程阻塞时,整个进程都会被阻塞。
混合实现(N:M实现)
混合实现结合了用户线程和轻量级进程的优点,通过多个用户线程映射到一个或多个内核线程。
- 实现机制:用户线程在用户空间中创建和调度,轻量级进程作为桥梁与内核线程交互。
- 优点:既保留了用户线程的低消耗和高并发,又利用了内核的线程调度和处理器映射,适用于需要大量线程并发的应用场景。
Java线程的实现
Java线程的具体实现并不受《Java虚拟机规范》的约束,因此不同的Java虚拟机可能有不同的线程实现方式。
其实现方式取决于运行Java应用程序的虚拟机。Java线程在不同版本的JVM中有不同的实现方式。
- 早期Java线程实现:在JDK 1.2之前,Java线程是通过用户线程实现的,称为“绿色线程”。
- 现代Java线程实现:从JDK 1.3开始,大多数商用Java虚拟机采用了基于操作系统原生线程的1:1模型。
这意味着每个Java线程实际上对应着一个操作系统的线程资源。在Windows操作系统中,创建一个新线程时,操作系统会为该线程分配一定的内存用于栈空间。这个栈空间大小是可配置的,但有一个默认值。通常,默认的线程栈大小大约为1MB。这意味着每当创建一个新的线程时,至少会有1MB的虚拟地址空间被保留给这个线程作为其栈空间。因此,在拥有2GB物理内存或可用虚拟地址空间的系统中,理论上最多可以创建约2048个线程(2GB / 1MB = 2048)。
线程调度
线程调度是指系统为线程分配处理器使用权的过程。Java线程调度主要有两种方式:协同式和抢占式。
协同式线程调度
线程的执行时间由线程本身控制,线程完成工作后需主动通知系统切换到其他线程。
- 优点:实现简单,没有线程同步问题。
- 缺点:线程执行时间不可控,可能导致系统阻塞。
抢占式线程调度
系统为每个线程分配执行时间,线程切换不由线程本身决定。
- 优点:线程执行时间可控,不会因单个线程问题导致系统阻塞。
- 缺点:无法主动获取执行时间。
Java线程采用的是抢占式线程调度,这种方式的线程调度是系统自动完成的,但是我们可以给操作系统一些建议,例如设置线程优先级。Java语言一共设置了10个级别的线程优先级,优先级越高的线程越容易被系统选择执行。
线程的设置
设置线程名称:可以使用
setName(String name)
方法来设置线程的名称。线程名称可以帮助我们更好地识别线程,特别是在多线程环境下。设置线程优先级:可以使用
setPriority(int priority)
方法来设置线程的优先级。线程的优先级越高,越有可能被系统调度执行。Java中线程的优先级范围是1~10,默认值为5。设置线程为守护线程:可以使用
setDaemon(boolean on)
方法来设置线程是否为守护线程。当一个Java程序的所有非守护线程都结束时,程序就会退出。如果将线程设置为守护线程,那么当程序中只有守护线程时,程序就会自动退出。设置未捕获异常处理器:通
setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
方法可以为线程设置未捕获异常处理器。设置线程组:通过
Thread
类的构造函数可以指定线程所属的线程组。通过getThreadGroup()
方法获取线程所属的线程组。
查看代码
public class ThreadDemo extends Thread {
public ThreadDemo(String name) {
super(name);
}
@Override
public void run() {
System.out.println("线程 " + getName() + " 正在执行...");
}
public static void main(String[] args) {
ThreadDemo thread1 = new ThreadDemo("线程1");
thread1.setPriority(Thread.MAX_PRIORITY); // 设置线程优先级为最高
thread1.start();
ThreadDemo thread2 = new ThreadDemo("线程2");
thread2.setDaemon(true); // 将线程设置为守护线程
thread2.start();
}
}
线程的创建
Java中有三种方式来创建多线程,分别为继承Thread类、实现Runnable接口和使用Callable和Future。
继承Thread类
定义一个类,继承Thread类,并重写run方法,run方法中定义需要执行的代码。然后创建该类的对象,调用start方法启动该线程。
查看代码
public class MyThread extends Thread{
@Override
public void run(){
//需要执行的代码
}
}
//创建并启动新的线程
MyThread myThread = new MyThread();
myThread.start();
实现Runnable接口
定义一个类,实现Runnable接口,并实现run方法,run方法中定义需要执行的代码。然后创建Thread对象,将该类的对象作为参数传入Thread构造函数中,调用start方法启动该线程。
查看代码
public class MyRunnable implements Runnable{
@Override
public void run(){
//需要执行的代码
}
}
//创建并启动新的线程
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
在实际开发中,推荐使用实现Runnable接口的方式来创建多线程,因为Java只支持单继承,如果使用继承Thread类的方式,就无法再继承其他的类。而实现Runnable接口则可以避免由于单继承所带来的限制,并且多个线程可以共享同一个Runnable对象,从而实现线程间通信。
使用Callable和Future
Callable是一个泛型接口,它可以作为线程执行体,并且可以返回执行结果。Future则是用来获取异步计算结果的接口。接口源码如下
查看代码
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
首先定义一个实现Callable接口的类,需要实现call方法,并在其中编写需要执行的代码,并返回一个结果。
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 需要执行的代码
return 42;
}
}
然后,创建一个ExecutorService对象,可以通过Executors工厂类来创建不同类型的线程池。使用submit方法提交Callable任务,并得到一个Future对象。
查看代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
// 创建一个线程池
ExecutorService executor = Executors.newFixedThreadPool(1);
// 提交Callable任务,并得到Future对象
Future<Integer> future = executor.submit(new MyCallable());
// 获取异步计算结果
int result = future.get();
System.out.println("计算结果:" + result);
// 关闭线程池
executor.shutdown();
}
}
run()和start的区别
run()
是一个普通的方法调用,它会按照顺序在当前线程中执行代码。当我们调用一个线程对象的 run()
方法时,它会在当前线程中同步执行线程的代码块。这意味着,代码将按照顺序执行,并且不会创建新的线程。
start()
方法用于启动一个新的线程。当一个线程的start()
方法被调用时,Java虚拟机将创建一个新的线程,并在这个新线程中执行该线程的run()
方法。调用start()
方法后,原来的线程(调用start()
方法的线程)和新创建的线程将并发执行。线程一旦启动,其run()
方法将在新线程中执行,而start()
方法会立即返回。
start()
方法开始时会检查线程的状态(threadStatus
),如果状态不是0
(即线程不是新建状态),则抛出IllegalThreadStateException
异常。这是因为在Java中,一个线程一旦开始执行就不能再次启动。
线程的状态
Java线程的状态定义在Thread类的State枚举中,可以用Thread类中的getState()方法获取,返回一个Thread.State枚举类型的值。Java线程的状态包括以下几种:
- NEW(新建):线程对象已经创建,但还没有调用start()方法。
- RUNNABLE(可运行):线程正在Java虚拟机中执行,可能正在等待操作系统资源,如处理器。在Java中RUNNABLE包括了操作系统中的就绪和运行两种状态。
- BLOCKED(阻塞):线程正在等待获取监视器锁,以进入同步块/方法或在调用Object.wait()之后重新进入同步块/方法。
- WAITING(等待):线程正在等待另一个线程执行特定操作。例如,调用了Object.wait()的线程正在等待另一个线程调用Object.notify()或Object.notifyAll(),或者调用了Thread.join()的线程正在等待指定的线程终止。
- TIMED_WAITING(定时等待):线程正在具有指定等待时间的等待状态。这种状态是通过调用具有指定超时时间的方法引起的,例如Thread.sleep()、Object.wait(long)、Thread.join(long)等。
- TERMINATED(终止):线程已完成执行,不再运行。
下面是源码
查看代码
/**
* 一个线程在某一时刻只能处于一种状态。
* 这些状态是虚拟机状态,不反映任何操作系统线程状态。
*/
public enum State {
// 尚未启动的线程状态。
NEW,
// 可运行线程的状态。处于可运行状态的线程在Java虚拟机中执行,但可能正在等待来自操作系统的其他资源,例如处理器。
RUNNABLE,
// 等待获取监视器锁的线程的状态。
// 处于阻塞状态的线程正在等待获取监视器锁,以进入同步块/方法或在调用Object.wait()之后重新进入同步块/方法。
BLOCKED,
// 等待中的线程的状态。
// 由于调用了以下方法之一,线程处于等待状态:
// Object.wait()(没有超时)
// Thread.join()(没有超时)
// LockSupport.park()
//
// 处于等待状态的线程正在等待另一个线程执行特定操作。
//
// 例如,调用了Object.wait()的线程正在等待另一个线程调用Object.notify()或Object.notifyAll()。
// 调用了Thread.join()的线程正在等待指定的线程终止。
WAITING,
// 具有指定等待时间的等待线程的状态。
// 由于调用了以下具有指定正等待时间的方法之一,线程处于定时等待状态:
// Thread.sleep
// Object.wait(long)(超时)
// Thread.join(long)(超时)
// LockSupport.parkNanos
// LockSupport.parkUntil
TIMED_WAITING,
// 终止的线程的状态。
// 线程已完成执行。
TERMINATED;
}
线程的状态转换
在Java中,线程在其生命周期内可以处于多种状态,这些状态反映了线程从创建到终止的整个执行过程。
新建到可运行
(NEW)→(RUNNABLE)
当使用new
关键字创建一个线程对象后,线程处于NEW
状态。在这个状态下,线程尚未启动(即其start()
方法未被调用)。调用start()
方法会使线程进入RUNNABLE
状态。此时,线程准备好执行,等待被线程调度器选中获取CPU时间。
可运行到阻塞
(RUNNABLE)→(BLOCKED)
处于RUNNABLE
状态的线程可能因为等待获取一个同步锁而进入BLOCKED
状态。当线程试图进入一个对象的方法或代码块,并且该对象锁已经被另一个线程持有时,线程就会进入阻塞状态。一旦线程获取到所需的锁,它就会从BLOCKED
状态转换回RUNNABLE
状态。
可运行到等待
(RUNNABLE)→(WAITING)
线程在执行过程中可能会调用Object.wait()
、Thread.join()
或LockSupport.park()
方法,这将使线程进入WAITING
状态。在WAITING
状态下,线程等待其他线程执行特定操作,例如:
- 调用
Object.wait()
的线程等待其他线程调用Object.notify()
或Object.notifyAll()
。 - 调用
Thread.join()
的线程等待指定的线程终止。 线程将保持WAITING
状态直到被其他线程唤醒。
可运行到定时等待
(RUNNABLE)→(TIMED_WAITING)
线程调用某些带有超时参数的方法会使其进入TIMED_WAITING
状态。这些方法包括Thread.sleep(long millis)
、Object.wait(long timeout)
、Thread.join(long millis)
、LockSupport.parkNanos()
和LockSupport.parkUntil()
等。线程将在指定的时间间隔过去后自动回到RUNNABLE
状态。
等待/定时等待到可运行
(WAITING)/(TIMED_WAITING)→(RUNNABLE)
处于WAITING
或TIMED_WAITING
状态的线程可以通过以下方式被唤醒并回到RUNNABLE
状态:
- 另一个线程调用了
notify()
或notifyAll()
方法。 - 对于
Thread.join()
,被等待的线程已经终止。 - 超时,对于
TIMED_WAITING
状态。 - 不通过锁或条件,而是通过
LockSupport.unpark(Thread thread)
方法。
可运行到终止
(RUNNABLE)→(TERMINATED)
线程执行完成后,即run()
方法执行完毕后,线程将进入TERMINATED
状态。线程一旦终止,就不能再次启动。
特殊状态转换
在某些情况下,线程的状态转换可能不遵循上述规则,例如:
- 线程在
RUNNABLE
状态时,如果CPU时间用尽或线程调度器选择其他线程运行,线程可能会从运行状态回到就绪状态,但这在Java线程状态中仍然是RUNNABLE
。 - 如果线程在
WAITING
或TIMED_WAITING
状态下被中断,它会抛出InterruptedException
并回到RUNNABLE
状态。
线程的执行流程
在Java程序中,执行流程从JVM启动main
线程开始。main
线程可以创建并启动其他线程。当所有线程执行完毕后,JVM进程才会结束。若有一个线程未结束,JVM进程就不会终止。因此,要确保所有线程都能适时结束,但对于那些设计为无限循环的线程,如定时任务触发线程,则需要特别注意。
Java虚拟机启动时,通常会有一个非守护线程(通常调用main方法)。JVM会继续执行线程,直到以下任一情况发生:
- 调用了
Runtime
类的exit
方法,并且安全管理器允许退出操作。 - 所有非守护线程都已停止运行,这包括从run方法返回或传播到run方法之外的异常。
守护线程是服务于其他线程的线程。当JVM中所有非守护线程执行完毕后,不论守护线程是否结束,虚拟机都会自动退出。因此,在JVM退出时,不必关心守护线程的状态。
终止线程
在Java中,终止线程有几种方法,但需要注意正确和安全地终止线程,以避免资源泄漏或数据不一致。
使用标志位
定义一个布尔类型的变量作为线程终止的标志,线程在运行过程中会定期检查这个标志。
查看代码
public class MyThread extends Thread {
private volatile boolean exit = false;
public void run() {
while (!exit) {
// 执行任务
}
}
public void stopThread() {
exit = true;
}
}
使用中断(Interrupt)
调用线程的 interrupt()
方法可以中断线程。线程可以通过调用 isInterrupted()
方法来检查自己是否被中断。
查看代码
public class MyThread extends Thread {
public void run() {
while (!isInterrupted()) {
// 执行任务
}
}
public void stopThread() {
interrupt();
}
}
线程在阻塞状态(如 sleep()
、wait()
、join()
等)时被中断,会抛出 InterruptedException
,可以在捕获异常后结束线程。
public void run() {
try {
while (!isInterrupted()) {
// 执行任务,可能会阻塞
}
} catch (InterruptedException e) {
// 处理中断
}
}
使用 Thread.stop()
Thread.stop()
方法可以立即终止线程,但这种方法是不安全的,因为它可能会破坏线程的同步块,导致数据不一致,因此不推荐使用。
使用 System.exit(int status)
调用 System.exit(int status)
会终止当前运行的Java虚拟机,从而终止所有线程。这通常用于程序出错时终止程序。
常用的线程方法
方法 | 功能 |
---|---|
start() | 启动线程,使线程进入RUNNABLE状态 |
run() | 线程运行时执行的代码,需要在线程子类中重写实现 |
join() | 等待线程终止,即在当前线程中等待另一个线程执行完毕后再继续执行 |
sleep() | 线程休眠指定时间(单位是毫秒),让出CPU资源给其他线程 |
interrupt() | 中断线程,发送中断信号给线程,并不一定能成功终止线程 |
isInterrupted() | 判断线程是否被中断 |
setPriority() | 设置线程的优先级,数值越大优先级越高 |
yield() | 让出CPU资源,让其他线程有机会运行 |
wait() | 使当前线程进入WAITING状态,等待其他线程调用notify()/notifyAll()方法唤醒 |
notify()/notifyAll() | 唤醒正在等待的线程 |
sleep和wait的区别
sleep
和 wait
是Java中用于控制线程执行的两个不同方法,它们之间有几个关键的区别:
定义位置和用途
sleep
是Thread
类的一个静态方法,用于使当前线程暂停执行指定的时间。wait
是Object
类的一个实例方法,用于使当前线程暂停执行,直到另一个线程调用同一对象的notify()
或notifyAll()
方法。
锁的处理
sleep
不会释放当前线程所持有的任何监视器(锁)。wait
必须在同步块或同步方法中调用,在调用时会释放当前线程所持有的监视器。
异常处理
sleep
需要处理InterruptedException
,这是当其他线程中断当前线程时抛出的。wait
在被唤醒时不会抛出异常,但如果在等待期间线程被中断,则会抛出InterruptedException
。
使用场景
sleep
通常用于让线程暂停执行一段时间,比如在执行定时任务时。wait
通常用于线程间的通信,当一个线程需要等待另一个线程完成某个操作或某个条件成立时。
线程安全
在多线程环境中,线程间不恰当的数据访问或操作,可能会导致数据不一致、数据竞争或死锁等问题。具体来说,当多个线程同时访问同一数据或资源,并且至少有一个线程对此数据或资源进行了写操作时,如果没有适当的同步措施,就可能会发生线程安全问题。
竞态条件
竞态条件(Race Conditions):当多个线程同时访问同一数据,并且至少有一个线程对数据进行了修改,而其他线程依赖于该数据的原始值时,可能会发生竞态条件。这可能导致程序的行为变得不可预测。
数据不一致
数据不一致(Data Inconsistency):当多个线程在没有适当同步的情况下修改同一数据时,可能会导致数据不一致。例如,一个线程正在修改一个对象,而另一个线程可能正在读取该对象,此时读取到的对象可能处于不一致的状态。
死锁
死锁(Deadlock):当多个线程相互等待对方持有的资源而无法继续执行时,就会发生死锁。这通常发生在每个线程都持有至少一个资源并且等待获取其他线程持有的资源时。
要形成死锁,必须同时满足以下四个条件:
- 互斥条件:某些资源是独占的,即同一时间只能由一个线程占有和使用。
- 持有并等待条件:线程已经持有至少一个资源,同时还在等待获取额外的资源,而这些资源又被其他线程持有。
- 非抢占条件:已经分配给一个线程的资源在该线程完成任务前不能被其他线程强行剥夺。
- 循环等待条件:存在一个线程链,每个线程都在等待下一个线程持有的资源,形成一个闭环。
这四个条件是死锁发生的充分条件,即当这四个条件都满足时,死锁就可能会发生。如果任何一个条件不被满足,死锁就不会发生。
活锁
活锁(Livelock):与死锁类似,活锁是指线程虽然没有被阻塞,但仍然无法向前推进,因为它们不断重复相同的操作,而这些操作总是失败。
避免线程安全问题
避免线程安全问题通常需要采取一系列的措施来确保当多个线程访问共享资源或数据时不会出现不一致或错误。以下是一些常见的方法:
使用同步机制
synchronized
关键字:在方法或代码块上使用 synchronized
关键字可以确保同一时间只有一个线程能够执行该段代码。
ReentrantLock
:java.util.concurrent.locks.ReentrantLock
类提供了比 synchronized
更灵活的锁定机制。
Semaphore
、CountDownLatch
、CyclicBarrier
等其他同步辅助类。
避免共享状态
使用不可变对象:不可变对象天生就是线程安全的,因为它们的状态在创建后就不能改变。
局部变量:尽量使用局部变量代替共享变量,因为局部变量是线程安全的。
使用线程局部变量
ThreadLocal
类提供了线程局部变量,每个线程都有自己独立的变量副本,从而避免了共享。
避免死锁
确保锁的获取顺序一致,避免循环等待。
使用尝试锁定(tryLock)而不是无限期等待锁定。
使用线程安全的数据结构
Vector
是 Java 的一个同步列表类,它类似于 ArrayList
,但所有操作都是线程安全的。这意味着在多线程环境中,不需要额外的同步措施就能保证线程安全。
Hashtable
是 Java 的一个同步哈希表类,类似于 HashMap
,但它所有公共方法都是同步的,因此可以在多线程环境中安全地使用。
Java 提供了专门为高并发设计的一些线程安全集合Concurrent Collections
,这些集合在并发读写时能够提供更好的性能。
StringBuffer
是线程安全的可变的字符序列。
Java 提供了原子变量类,如 AtomicInteger
、AtomicLong
和 AtomicReference
,它们使用CAS(Compare and Swap)操作来提供无锁的线程安全访问。
线程同步
线程同步主要是指控制多个线程访问共享资源的机制。
线程同步的目的是避免多个线程同时修改共享数据而引发的数据不一致或错误。
互斥是实现同步的一种手段,确保共享数据在同一时刻只被一个线程使用。
线程同步的方法主要包括一下几种:
- 同步方法(Synchronized Method):使用
synchronized
关键字修饰的方法。每个对象都有一个监视器锁,当线程调用对象的同步方法时,它会自动获取这个对象的锁,其他线程将无法获取该锁,直到锁被释放。 - 同步代码块(Synchronized Block):使用
synchronized
关键字创建一个同步代码块,可以指定一个锁对象。进入同步代码块之前,线程必须获得指定锁对象的锁。 - 使用
ReentrantLock
:java.util.concurrent.locks.ReentrantLock
类提供了比synchronized
关键字更灵活的锁定机制。它允许尝试带有超时的锁定、尝试非阻塞地获取锁以及公平锁或非公平锁等。 - 使用
Semaphore
、CountDownLatch
、CyclicBarrier
等并发工具类:Java并发包java.util.concurrent
提供了这些高级同步类,用于更复杂的同步需求。
线程通信
线程通信主要是指线程之间交换信息。Java提供了几个重要的方法来支持线程通信:
wait()
: 当一个线程调用一个共享对象的wait()
方法时,该线程会暂停执行,并等待其他线程调用该对象的notify()
或notifyAll()
方法。notify()
: 随机唤醒在此对象监视器上等待的单个线程。notifyAll()
: 唤醒在此对象监视器上等待的所有线程。
这三个方法只能用在同步方法或同步代码块中,因为它们依赖于java.lang.Object
类的固有监视器锁。
除了以上方法,还有Thread.join()
方法允许线程等待另一个线程的终止。调用 join()
的线程会暂停执行,直到指定的线程终止为止。
线程本地变量
ThreadLocal 是 Java 中一个非常重要的并发工具类,它提供了一种让每个线程拥有自己独立的变量副本的能力。在多线程环境下,尽管所有的线程共享同一个变量,但每个线程都可以通过 ThreadLocal 实例来访问自己独立的变量副本,从而隔离了线程间的数据,避免了线程安全问题。
使用方法
ThreadLocal 使用起来非常简单,通常有以下几个操作:
- 创建 ThreadLocal 对象:通过
ThreadLocal<String> localVar = new ThreadLocal<String>();
这样的语句来创建一个 ThreadLocal 对象。 - 设置值:通过
localVar.set("value");
来为当前线程设置一个值。 - 获取值:通过
String value = localVar.get();
来获取当前线程的副本中的值。 - 移除值:通过
localVar.remove();
来移除当前线程的副本中的值。
实现原理
ThreadLocal 的实现原理是基于 Thread 类的一个内部类 ThreadLocalMap。每个线程都有一个自己的 ThreadLocalMap,用于存储线程的副本变量。ThreadLocalMap 中的键是 ThreadLocal 对象,值是副本变量。 当线程调用 ThreadLocal 的 set 方法时,实际上是将值存储在当前线程的 ThreadLocalMap 中。当调用 get 方法时,就是从当前线程的 ThreadLocalMap 中取出值。
注意事项
- 内存泄漏:由于 ThreadLocalMap 的生命周期和线程相同,如果 ThreadLocal 对象被回收,而线程长时间运行不结束,那么 ThreadLocalMap 中的键值对就无法被回收,可能会导致内存泄漏。因此,在使用完 ThreadLocal 后,应该调用 remove 方法来清除线程的副本值。
- 继承性:ThreadLocal 不支持继承性,即子线程不能访问父线程的 ThreadLocal 值。如果需要传递值,可以使用 InheritableThreadLocal。