文章目录
Java 多线程技术详解
目录
引言
Java多线程是Java平台的一个核心特性,它为开发人员提供了一种在单个程序中同时执行多个任务的能力。这种并发执行的能力不仅极大地提高了程序的执行效率,还允许软件更好地利用现代多核处理器的硬件资源,从而实现高性能和高响应性的应用。
多线程编程的核心优势在于它能够实现任务的并行处理,即在同一时间处理多个任务,这对于处理I/O密集型或计算密集型的工作负载特别有效。例如,一个服务器应用程序可以同时处理多个客户端请求,或者一个数据分析程序可以在不同的数据集上并行运行算法。
然而,多线程编程也带来了复杂性,特别是当涉及到线程之间的数据共享和同步时。如果不恰当地管理,多线程程序可能会遭受竞态条件、死锁、活锁、饥饿和资源泄露等问题。因此,理解线程生命周期、线程状态、线程调度、线程安全、线程间通信以及如何有效地使用线程池和其他高级同步机制,对于成功开发多线程应用程序至关重要。
Java标准库提供了丰富的API来支持多线程编程,包括Thread
类、Runnable
和Callable
接口、ExecutorService
框架、synchronized
关键字、Lock
和ReentrantLock
接口、Condition
接口、ThreadLocal
类以及各种线程池类型。熟练掌握这些工具和技术,是成为高效Java多线程程序员的基础。
多线程的概念
在计算机科学中,多线程是指从软件或者硬件第一级(编程语言层面,操作系统层面,硬件层面)支持执行多个线程的操作。在Java中,多线程是指在一个单一的Java虚拟机(JVM)中,同时运行多个执行路径,即多个线程。每个线程都是操作系统进程中的一个执行单元,具有自己的程序计数器、堆栈和局部变量,但它们共享进程的全局变量和资源。
为什么使用多线程?
- 资源利用率:多线程可以提高CPU的利用率,特别是在多核处理器系统中,能够同时
并行
处理多个任务(这里说的是在多核cpu中同一时间并行执行而不是在同一时间间隔内交替执行
),从而提高系统的整体性能。 - 响应性:在图形用户界面(GUI)应用程序中,多线程确实可以极大地提高应用程序的响应性。
- 模块化:多线程可以增强程序的模块化,使得大型或复杂的程序更容易管理和扩展。在多线程编程中,每个线程通常负责执行一个特定的任务或一组相关任务,这可以看作是将程序分解成多个独立运行的组件或模块。
- 并发执行:多线程可以实现并发执行,这对于处理大量数据或执行长时间运行的任务非常有用。
多线程的特征
- 并发性:多个线程可以同时运行,尽管在单核处理器上可以表现为交替执行。
- 共享资源:线程之间共享相同的内存空间,这意味着它们可以访问和修改相同的变量,但这也可能导致数据不一致的问题,除非采取适当的同步措施。
- 上下文切换:操作系统在多个线程之间切换执行,这称为上下文切换,会带来一定的开销。
- 独立性:每个线程都有自己的执行流,它们可以独立于其他线程执行,但也可以通过线程间通信机制协作。
多线程的挑战
- 同步问题:当多个线程访问和修改共享资源时,必须采取措施防止竞态条件和死锁。
- 死锁:当两个或多个线程无限期地等待彼此持有的资源时发生。
- 资源竞争:多个线程对同一资源的访问可能需要排队,导致性能下降。
- 调试困难:多线程程序的错误往往难以重现和诊断,因为线程的执行顺序可能在每次运行时都不同。
多线程的实现方式
在Java中,实现多线程主要有四种常见的方式:继承Thread
类、实现Runnable
接口、使用Executor
框架以及使用Callable
和Future
接口。每种方式都有其适用场景和优缺点。
3.1 继承 Thread
类
继承Thread
类是最直接的实现多线程的方式。你需要创建一个Thread
类的子类,并重写run()
方法,其中包含线程要执行的代码。当通过子类实例调用start()
方法时,run()
方法会被系统调用,从而开始线程的执行。
示例代码:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello from " + this.getName());
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
优点:
- 直接使用
Thread
类的方法,如start()
,join()
,interrupt()
等。 - 可以访问和修改线程的一些属性,如线程名称、优先级。
缺点:
- Java不允许多重继承,因此,如果需要继承其他类,就不能再继承
Thread
类。 - 不能直接使用
Thread
类的其他成员变量。
3.2 实现 Runnable
接口
实现Runnable
接口是更常用的多线程实现方式,因为它避免了Java单继承的限制。你需要创建一个实现了Runnable
接口的类,并实现run()
方法。之后,创建一个Thread
对象,将你的Runnable
对象作为参数传入,然后调用start()
方法开始线程。
示例代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread myThread = new Thread(new MyRunnable(), "My Runnable Thread");
myThread.start();
}
}
优点:
- 不影响类的继承链,可以继承其他类。
- 更加灵活,适合复杂的业务逻辑。
缺点:
- 需要额外的
Thread
对象来启动线程。
3.3 使用 Executor
框架
Executor
框架是Java并发工具包(java.util.concurrent
)的一部分,提供了更高级别的抽象来管理线程。ExecutorService
接口是Executor
框架的核心,它提供了一系列的线程管理方法,如submit()
, execute()
, shutdown()
, isTerminated()
等。使用ExecutorService
可以创建线程池,有效地复用线程,避免频繁创建和销毁线程的开销。
示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread(i);
executor.execute(worker);
}
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有线程完成
}
}
}
class WorkerThread implements Runnable {
private int id;
public WorkerThread(int id) { this.id = id; }
@Override
public void run() {
System.out.println("Hello from WorkerThread " + id);
}
}
优点:
- 更好的资源管理,通过线程池可以控制线程数量,避免过多线程导致的系统资源浪费。
- 提供了更丰富的线程控制方法,如定时执行、批量提交任务等。
缺点:
- 相对于直接使用
Thread
和Runnable
,实现起来稍微复杂一些。
3.4 使用 Callable
和 Future
Callable
接口类似于Runnable
,但是它允许线程执行后返回一个结果,并且可以抛出异常。Future
接口用于获取Callable
执行的结果,或取消任务的执行。
示例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
int result = future.get(); // 阻塞直到得到结果
System.out.println("Result: " + result);
executor.shutdown();
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 42; // 假设这是一个计算结果
}
}
优点:
- 允许线程执行后返回结果,适合需要返回值的任务。
- 可以抛出异常,提供更完整的错误处理机制。
缺点:
- 相较于
Runnable
,实现起来稍微复杂,因为需要处理Future
和可能的异常。 Future.get()
方法会阻塞,直到任务完成,需要注意避免在主线程中调用,以免造成UI冻结或其他性能问题。
线程的生命周期
在Java中,一个线程从创建到结束,会经历一系列的状态变化,这些状态构成了线程的生命周期。理解线程生命周期对于正确管理和控制线程非常重要,尤其在处理线程的启动、终止、同步和调度时。
线程状态
新建状态(New)
线程的生命周期始于新建状态,这时线程对象已经被创建,但是start()
方法还没有被调用。在这个状态下,线程还没有开始执行任何代码。
就绪状态(Runnable)
当线程对象的start()
方法被调用后,线程进入就绪状态。此时,线程已经准备好运行,但是尚未被调度器选中获取CPU时间片。处于就绪状态的线程由操作系统管理,等待CPU资源以便开始执行。
运行状态(Running)
一旦线程被调度器选中并分配到CPU时间片,线程开始执行其run()
方法中的代码,此时线程处于运行状态。在运行状态中,线程可能会因为各种原因而暂停执行,如执行完一个时间片、等待I/O操作、等待其他线程释放锁、响应中断或执行sleep()
方法等。
阻塞状态(Blocked / Waiting / Timed Waiting)
在执行过程中,线程可能会进入阻塞状态,这通常发生在以下几种情况下:
- 等待锁:当线程试图获取一个已被其他线程锁定的资源时,它将被阻塞,直到锁被释放。
- 等待通知:线程调用
Object.wait()
方法,等待其他线程的notify()
或notifyAll()
通知。 - 等待定时事件:线程调用
Thread.sleep(long millis)
或Object.wait(long timeout)
,在指定的时间段内不会被调度。
终止状态(Terminated)
当线程的run()
方法执行完毕,或者线程抛出了未捕获的异常,线程将进入终止状态。一旦线程终止,它将不再参与调度,也不能再次启动。线程对象仍然存在于内存中,直到垃圾回收器将其回收。
线程状态转换
线程状态的转换是由Java虚拟机和操作系统共同管理的。以下是一些常见的状态转换:
- 新建 → 就绪:当
start()
方法被调用后,线程从新建状态变为就绪状态。 - 就绪 → 运行:当线程被调度器选中并分配到CPU资源时,从就绪状态变为运行状态。
- 运行状态 → 就绪状态:当线程的时间片用尽,或者主动让出CPU(如调用
yield()
方法),它会从运行状态变回就绪状态,等待下一次调度。 - 运行 → 阻塞:当线程遇到阻塞条件,如等待锁、I/O操作或执行
wait()
时,从运行状态变为阻塞状态。 - 阻塞 → 就绪:当阻塞条件解除,如锁被释放、等待时间到期或收到通知,线程从阻塞状态回到就绪状态。
- 运行 → 终止:当线程的
run()
方法执行完毕或抛出未捕获异常,线程从运行状态变为终止状态。
线程调度
线程调度是操作系统的一项核心功能,负责确定哪些线程应该在什么时候运行以及运行多长时间。在Java中,线程调度由Java虚拟机(JVM)和底层操作系统协同完成,主要依据线程的优先级和系统的调度策略。
调度策略
操作系统通常采用以下几种调度策略:
- 先来先服务(First-Come, First-Served, FCFS):按照线程到达的先后顺序进行调度。
- 时间片轮转(Round Robin, RR):将CPU时间分成相等的时间片,每个就绪状态的线程轮流获得一个时间片。
- 优先级调度(Priority Scheduling):根据线程的优先级高低进行调度,优先级高的线程优先执行。在Java中,线程的优先级可以通过
Thread
类的setPriority()
方法设置。 - 最短作业优先(Shortest Job First, SJF):优先执行预计执行时间最短的线程。
Java线程调度
在Java中,线程调度遵循优先级调度原则,但实际的调度细节取决于底层操作系统。Java虚拟机并不保证线程的优先级一定会直接影响线程的执行顺序,而是尽力按照优先级来调度线程。此外,线程优先级的范围是1(最低)到10(最高),默认优先级为5。
public class PriorityDemo {
public static void main(String[] args) {
// 创建低优先级线程
Thread lowPriorityThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Low priority thread is running.");
}
});
//通过Thread类的setPriority()方法来设置线程的优先级。线程的优先级是一个整数值,范围从Thread.MIN_PRIORITY(常量值为1,代表最低优先级)到Thread.MAX_PRIORITY(常量值为10,代表最高优先级)。默认的优先级是Thread.NORM_PRIORITY(常量值为5)
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
// 创建高优先级线程
Thread highPriorityThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("High priority thread is running.");
}
});
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
// 启动线程
lowPriorityThread.start();
highPriorityThread.start();
}
}
线程调度的重要概念包括:
- 抢占式调度:在Java中,线程调度是抢占式的,这意味着高优先级的线程可以打断低优先级线程的执行,一旦高优先级的线程可用,它将立即获得CPU时间片。
- 时间片:每个线程在运行时会获得一个时间片,时间片结束后,线程会回到就绪状态,等待下一轮调度。
- 线程让步:线程可以通过调用
Thread.yield()
方法主动放弃剩余的时间片,将CPU让给同优先级或更高优先级的线程。
影响线程调度的因素
- 线程优先级:高优先级的线程有更大的机会被调度。
- 线程状态:只有处于就绪状态的线程才能被调度。
- 系统负载:系统中的线程数量和CPU核心数量会影响线程调度的效率。
- 操作系统调度策略:底层操作系统的调度策略会对Java线程的调度产生影响。
- 线程交互:线程间的同步和通信操作,如等待锁或条件变量,会影响线程的调度时机。
调度的不可预测性
Java线程调度的具体行为在不同操作系统和不同JVM实现中可能会有所不同,因此开发者不能完全依赖于线程优先级来保证线程的执行顺序。在设计多线程应用程序时,应考虑到调度的不确定性和不可预测性,避免过度依赖线程调度来实现同步或定时任务。
线程安全
synchronized
关键字是Java中用于实现线程安全的基本同步机制之一。它确保了在多线程环境中,任何时刻只有一个线程可以执行被synchronized
关键字保护的代码段。这种机制通过内部的互斥锁(也称为监视器锁或内置锁)来实现,该锁与Java对象关联。
synchronized
关键字的使用
synchronized
关键字可以应用于两种情况:方法和代码块。
-
synchronized
方法
当你声明一个方法为synchronized
时,该方法成为一个同步方法。在该方法执行期间,任何其他线程都不能调用这个对象上的任何synchronized
方法。这意味着对象的锁将被持有直到该方法执行完毕。public class Counter { private int count = 0; public synchronized void increment() { count++; } }
-
synchronized
代码块
你也可以使用synchronized
关键字来同步代码块,这允许更细粒度的控制。你必须指定一个对象作为锁,通常是this
对象或一个类的静态字段。public class Counter { private int count = 0; private final Object lock = new Object(); public void increment() { synchronized (lock) { count++; } } }
synchronized
关键字的特点
- 排他性:在任意时刻,只有一个线程能够执行被
synchronized
保护的代码。 - 有序性:由于
synchronized
关键字的锁是基于对象的,所以它强制执行了变量读取和写入的有序性,避免了指令重排序带来的问题。 - 可见性:当一个线程更改了共享变量的值,然后释放了锁,另一个线程在获取该锁时能够看到前一个线程所做的更改。
synchronized
的局限性
- 性能开销:由于
synchronized
需要维护锁的所有权和等待队列,因此在高并发的情况下可能会成为性能瓶颈。 - 死锁风险:如果多个
synchronized
代码块或方法没有正确的加锁顺序,可能会导致死锁。
如何优化synchronized
虽然synchronized
关键字是实现线程安全的一种简单方式,但在高并发场景下可能不是最优的选择。以下是一些优化策略:
- 减少锁的范围:只在必要的时候使用
synchronized
,尽量减小同步代码块的大小。 - 使用锁分离:如果可能,将共享资源分割,每个资源有自己的锁,这样可以减少锁的竞争。
- 使用更高效的锁:如
java.util.concurrent
包中的ReentrantLock
,它提供了比synchronized
更灵活的锁定机制,如可重入、公平性和条件变量。
示例代码
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
在上面的例子中,Counter
类使用一个私有的锁对象来同步对count
字段的访问。这确保了increment
和getCount
方法在多线程环境下的线程安全性。
7.2 Lock 接口
Lock
接口是java.util.concurrent.locks
包的一部分,它提供了一种更高级别的锁定机制,比synchronized
关键字更灵活、更强大。Lock
接口定义了以下主要方法:
void lock()
: 获取锁。如果锁已被另一个线程持有,则当前线程将一直等待,直到锁被释放。void unlock()
: 释放锁。boolean tryLock()
: 尝试获取锁。如果锁不可用,则立即返回false
,不会阻塞线程。boolean tryLock(long time, TimeUnit unit)
: 尝试获取锁。如果锁不可用,则等待一定的时间,如果在等待时间内锁仍未被释放,则返回false
。Condition newCondition()
: 返回一个Condition
对象,可以用来实现更复杂的线程间同步。
ReentrantLock 类
ReentrantLock
是Lock
接口的一个实现,它提供了一个可重入的互斥锁。ReentrantLock
有两个构造函数,分别用于创建公平锁和非公平锁:
- 公平锁:线程按照请求锁的顺序获取锁,这样可以减少线程的饥饿现象,但是性能通常不如非公平锁。
- 非公平锁:线程获取锁时没有固定的顺序,可能会导致后请求锁的线程在某些情况下先于前面的线程获取锁,这种情况下,锁的获取可能更偏向于当前正在运行的线程,从而提高性能。
ReentrantLock
还提供了以下额外的控制功能:
- 可中断的锁获取:
lockInterruptibly()
允许线程在等待锁时响应中断。 - 锁的公平性控制:通过构造函数的布尔参数来决定锁是否为公平锁。
- 锁的重入次数:
ReentrantLock
允许同一个线程多次获取同一个锁,而不会造成死锁。
Condition 接口
Condition
接口也是java.util.concurrent.locks
包的一部分,它与Lock
接口一起使用,提供了一种比Object
类的wait()
和notify()
方法更高级的线程等待和唤醒机制。Condition
接口允许线程等待某个条件满足,而不仅仅是在对象监视器上等待。
Condition
接口的主要方法包括:
void await()
: 释放锁并使当前线程等待,直到其他线程调用与此Condition
相关的signal()
或signalAll()
方法。void signal()
: 唤醒一个等待此Condition
的线程。void signalAll()
: 唤醒所有等待此Condition
的线程。
示例代码
下面是一个使用ReentrantLock
和Condition
的示例,展示如何实现一个简单的生产者-消费者模式:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Buffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items;
private int putIndex, takeIndex, count;
public Buffer(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("Capacity must be greater than 0.");
}
items = new Object[capacity];
}
public void put(Object item) throws InterruptedException {
lock.lock();
try {
// 如果缓冲区满,则等待
while ((count == items.length) || (putIndex == takeIndex)) {
long nanos = 1000 * 1000 * 1000; // 1秒
nanos = notFull.awaitNanos(nanos);
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
++count;
// 唤醒等待的消费者线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
// 如果缓冲区空,则等待
long nanos = 1000 * 1000 * 1000; // 1秒
while (count == 0 && nanos > 0) {
nanos = notEmpty.awaitNanos(nanos);
}
if (count != 0) {
Object x = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
--count;
// 唤醒等待的生产者线程
notFull.signal();
return x;
} else {
return null; // 缓冲区仍然为空,返回null
}
} finally {
lock.unlock();
}
}
}
详细解释
构造函数
构造函数接收一个整数参数capacity
,用于指定缓冲区的大小。如果传递的容量小于或等于0,将抛出IllegalArgumentException
。构造函数还初始化了items
数组,并设置了putIndex
、takeIndex
和count
变量的初始值。
put() 方法
生产者线程调用put()
方法将对象放入缓冲区。该方法的主要逻辑如下:
-
获取锁:
- 使用
lock.lock()
获取锁。
- 使用
-
检查缓冲区是否已满:
- 使用
while (count == items.length)
检查缓冲区是否已满。 - 如果缓冲区已满,线程将调用
notFull.awaitNanos(nanos)
等待,其中nanos
是最大等待时间(以纳秒为单位)。 awaitNanos()
方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
- 使用
-
放入对象:
- 如果缓冲区未满,生产者将对象放入
items
数组中。 - 更新
putIndex
和count
变量。
- 如果缓冲区未满,生产者将对象放入
-
唤醒消费者线程:
- 调用
notEmpty.signal()
来唤醒等待的消费者线程。
- 调用
-
释放锁:
- 使用
lock.unlock()
释放锁。
- 使用
take() 方法
消费者线程调用take()
方法从缓冲区取出对象。该方法的主要逻辑如下:
-
获取锁:
- 使用
lock.lock()
获取锁。
- 使用
-
检查缓冲区是否为空:
- 使用
while (count == 0)
检查缓冲区是否为空。 - 如果缓冲区为空,线程将调用
notEmpty.awaitNanos(nanos)
等待,其中nanos
是最大等待时间(以纳秒为单位)。 awaitNanos()
方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
- 使用
-
取出对象:
- 如果缓冲区不为空,消费者将对象从
items
数组中取出。 - 更新
takeIndex
和count
变量。
- 如果缓冲区不为空,消费者将对象从
-
唤醒生产者线程:
- 调用
notFull.signal()
来唤醒等待的生产者线程。
- 调用
-
释放锁:
- 使用
lock.unlock()
释放锁。
- 使用
使用awaitNanos()
方法的原因
使用awaitNanos()
方法而不是普通的await()
方法有几个好处:
-
避免忙等:
awaitNanos()
方法允许线程在等待期间释放锁,并在指定的时间后自动恢复,从而避免了忙等的情况。
-
节省CPU资源:
- 通过限制等待时间,可以减少CPU的使用率,避免不必要的循环检查。
-
提高系统响应性:
- 如果条件在等待时间内没有满足,线程将退出等待状态并继续执行,这有助于提高系统的响应性。
总结
Lock
接口和ReentrantLock
类提供了更高级、更灵活的锁控制机制,适用于需要更细粒度控制的场景。使用Lock
接口和ReentrantLock
时,需要注意锁的获取和释放必须配对,否则会导致死锁或资源泄露。同时,Condition
接口提供了更灵活的线程间同步方式,有助于实现更复杂的同步逻辑。
线程间通信
线程间通信概述
线程间通信是指在一个多线程环境中,不同线程之间共享信息和协调行为的过程。这对于确保程序的正确执行和提高效率至关重要。线程间通信通常涉及以下几种机制:
-
共享内存:
- 多个线程共享相同的内存空间,通过读写共享变量来通信。
- 必须注意同步访问,以防止竞态条件。
-
信号量和条件变量:
- 信号量用于管理资源的访问权限。
- 条件变量允许线程等待特定条件的满足。
-
消息队列:
- 一种基于队列的数据结构,线程可以向队列发送消息,其他线程可以从队列中读取消息。
-
管道(Pipes):
- 允许线程或进程之间通过管道进行通信。
-
事件对象:
- 用于信号通知,可以用来同步线程。
Java 中的线程间通信
在Java中,最常用的线程间通信机制包括使用Object
类的wait()
和notify()
方法,以及使用java.util.concurrent
包中的Lock
接口和Condition
接口。
使用Object
的wait()
和notify()
方法
这是Java中最基本的线程间通信方式之一。wait()
方法使当前线程等待,直到另一个线程调用notify()
或notifyAll()
方法。这些方法必须在同步块内调用,通常在synchronized
块或方法中。
使用Lock
接口和Condition
接口
Lock
接口提供了更高级的锁定机制,而Condition
接口则提供了更灵活的线程等待和唤醒机制。这些接口位于java.util.concurrent.locks
包中。
示例代码
下面是一个使用ReentrantLock
和Condition
的示例,展示如何实现一个简单的生产者-消费者模式:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Buffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items;
private int putIndex, takeIndex, count;
public Buffer(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("Capacity must be greater than 0.");
}
items = new Object[capacity];
}
public void put(Object item) throws InterruptedException {
lock.lock();
try {
// 如果缓冲区满,则等待
while ((count == items.length) || (putIndex == takeIndex)) {
long nanos = 1000 * 1000 * 1000; // 1秒
nanos = notFull.awaitNanos(nanos);
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
++count;
// 唤醒等待的消费者线程
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
// 如果缓冲区空,则等待
long nanos = 1000 * 1000 * 1000; // 1秒
while (count == 0 && nanos > 0) {
nanos = notEmpty.awaitNanos(nanos);
}
if (count != 0) {
Object x = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
--count;
// 唤醒等待的生产者线程
notFull.signal();
return x;
} else {
return null; // 缓冲区仍然为空,返回null
}
} finally {
lock.unlock();
}
}
}
详细解释
构造函数
构造函数接收一个整数参数capacity
,用于指定缓冲区的大小。如果传递的容量小于或等于0,将抛出IllegalArgumentException
。构造函数还初始化了items
数组,并设置了putIndex
、takeIndex
和count
变量的初始值。
put() 方法
生产者线程调用put()
方法将对象放入缓冲区。该方法的主要逻辑如下:
-
获取锁:
- 使用
lock.lock()
获取锁。
- 使用
-
检查缓冲区是否已满:
- 使用
while ((count == items.length) || (putIndex == takeIndex))
检查缓冲区是否已满。 - 如果缓冲区已满,线程将调用
notFull.awaitNanos(nanos)
等待,其中nanos
是最大等待时间(以纳秒为单位)。 awaitNanos()
方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
- 使用
-
放入对象:
- 如果缓冲区未满,生产者将对象放入
items
数组中。 - 更新
putIndex
和count
变量。
- 如果缓冲区未满,生产者将对象放入
-
唤醒消费者线程:
- 调用
notEmpty.signal()
来唤醒等待的消费者线程。
- 调用
-
释放锁:
- 使用
lock.unlock()
释放锁。
- 使用
take() 方法
消费者线程调用take()
方法从缓冲区取出对象。该方法的主要逻辑如下:
-
获取锁:
- 使用
lock.lock()
获取锁。
- 使用
-
检查缓冲区是否为空:
- 使用
while (count == 0 && nanos > 0)
检查缓冲区是否为空。 - 如果缓冲区为空,线程将调用
notEmpty.awaitNanos(nanos)
等待,其中nanos
是最大等待时间(以纳秒为单位)。 awaitNanos()
方法允许线程等待一个特定的时间长度,并返回剩余的等待时间。如果条件未在指定时间内满足,线程将继续执行下一个循环迭代。
- 使用
-
取出对象:
- 如果缓冲区不为空,消费者将对象从
items
数组中取出。 - 更新
takeIndex
和count
变量。
- 如果缓冲区不为空,消费者将对象从
-
唤醒生产者线程:
- 调用
notFull.signal()
来唤醒等待的生产者线程。
- 调用
-
释放锁:
- 使用
lock.unlock()
释放锁。
- 使用
使用awaitNanos()
方法的原因
使用awaitNanos()
方法而不是普通的await()
方法有几个好处:
-
避免忙等:
awaitNanos()
方法允许线程在等待期间释放锁,并在指定的时间后自动恢复,从而避免了忙等的情况。
-
节省CPU资源:
- 通过限制等待时间,可以减少CPU的使用率,避免不必要的循环检查。
-
提高系统响应性:
- 如果条件在等待时间内没有满足,线程将退出等待状态并继续执行,这有助于提高系统的响应性。
总结
线程间通信是多线程编程的关键部分,它确保了线程之间的协作和数据一致性。使用Lock
接口和Condition
接口可以实现更高级、更灵活的同步机制,帮助开发者更好地管理线程间的交互。
避免死锁
避免死锁是多线程编程中的一个重要方面,尤其是在Java中。死锁是一种特殊情况下的资源竞争问题,其中一个或多个线程永久阻塞,因为每个线程都在等待另一个线程持有的锁。为了帮助你完善关于如何避免死锁的内容,我将提供一些关键点和建议。
死锁的四个必要条件
死锁通常由以下四个必要条件引起:
- 互斥条件:至少有一个资源必须处于非共享模式,即一次只能被一个进程使用。
- 占有并等待:进程已保持至少一个资源,但又等待新的资源。
- 非抢占:资源不能被抢占,只能由拥有进程自愿释放。
- 循环等待:存在一种进程-资源的循环链,每个进程已占用的资源被下一个进程所期望。
如何避免死锁
为了避免死锁的发生,可以采取以下策略:
1. 破坏互斥条件
- 资源共享:尽可能使资源可共享,减少独占资源的需求。
- 避免使用锁:如果可能的话,重新设计代码以避免使用锁。
2. 破坏占有并等待条件
- 一次性获取所有资源:确保线程在开始执行之前获取所有必需的锁。
- 按顺序获取锁:如果多个线程需要获取多个锁,则让它们按照固定的顺序获取锁,这样可以避免形成循环等待。
3. 破坏非抢占条件
- 超时机制:为锁请求添加超时机制,如果超过一定时间无法获得锁,则释放已经持有的锁并稍后再重试。
- 使用tryLock:使用
ReentrantLock
的tryLock()
方法来尝试获取锁,如果锁不可用,则不会阻塞线程。
4. 破坏循环等待条件
- 锁顺序:如果一个线程需要多个锁,确保总是按照相同的顺序获取这些锁。
- 死锁检测:定期检查是否有可能出现死锁的情况,如果检测到潜在的死锁,则释放锁并重试。
线程池
10.1 ExecutorService
线程池是Java多线程编程中的一个重要概念,它可以有效地管理线程的创建和销毁过程,减少系统资源的消耗,并提供了一种更灵活的方式来管理并发任务。下面是关于线程池的一些详细信息,可以帮助你更好地理解和使用线程池。
线程池的基本概念
线程池是在Java中管理线程的一种机制,它预先创建一定数量的线程,这些线程处于等待状态,当有新的任务到来时,线程池会分配一个线程来执行这个任务。当任务完成后,线程并不会被销毁,而是返回到线程池中等待下一个任务。这种方式可以避免频繁创建和销毁线程带来的开销,提高程序的执行效率。
Java中的线程池
Java中线程池的主要接口是ExecutorService
,它是java.util.concurrent
包的一部分。ExecutorService
提供了一些重要的方法来控制线程池的生命周期,如submit()
、execute()
、shutdown()
和awaitTermination()
等。
创建线程池
线程池可以通过Executors
工厂类来创建,该类提供了几个静态方法来创建不同类型的线程池。以下是几种常见的线程池类型:
-
Fixed Thread Pool (
newFixedThreadPool(nThreads)
):- 固定大小的线程池,线程数量固定。
- 如果提交的任务数量超过了线程池的大小,这些任务会被放入一个队列中等待执行。
- 适用于任务数量未知的情况,尤其是处理大量短期异步任务时。
-
Cached Thread Pool (
newCachedThreadPool()
):- 可缓存的线程池,线程数量动态调整。
- 当没有任务时,多余的空闲线程会被销毁。
- 适用于执行大量的短期异步任务。
-
Scheduled Thread Pool (
newScheduledThreadPool(nThreads)
):- 定时线程池,用于执行周期性或定时任务。
- 支持延迟执行任务和周期性执行任务。
-
Single Thread Executor (
newSingleThreadExecutor()
):- 单一线程池,只包含一个线程。
- 适用于需要保证任务按顺序执行的场合。
线程池参数
-
核心线程数 (
corePoolSize
):- 表示线程池中的最小线程数量。即使没有任务执行,线程池也会维护这些线程。
- 这些线程通常不会被终止,除非调用了
allowCoreThreadTimeOut(true)
。
-
最大线程数 (
maximumPoolSize
):- 表示线程池中可以创建的最大线程数量。
- 当任务队列满时,线程池会继续创建新线程,直到达到最大线程数。
-
空闲线程存活时间 (
keepAliveTime
):- 指定了线程空闲时可以存活的时间长度。
- 对于超过核心线程数的线程,如果它们空闲了指定的时间长度,就会被终止。
- 对于核心线程,默认情况下,如果设置了
allowCoreThreadTimeOut(true)
,核心线程也会遵守这个时间限制。
-
时间单位 (
TimeUnit
):- 用于指定
keepAliveTime
的时间单位,例如秒(SECONDS
)、毫秒(MILLISECONDS
)等。
- 用于指定
-
工作队列 (
workQueue
):- 用于存放等待执行的任务的队列。
- 通常使用
ArrayBlockingQueue
,LinkedBlockingQueue
或SynchronousQueue
等。 - 当线程池中的线程数达到最大线程数时,新来的任务将会被放入此队列等待执行。
-
拒绝策略 (
handler
):- 当任务队列已满并且线程池已经达到最大线程数时,如果还有新的任务提交,那么线程池会采取拒绝策略来处理这些任务。
- 常见的拒绝策略包括:
AbortPolicy
: 抛出RejectedExecutionException
异常。CallerRunsPolicy
: 由调用者所在的线程来执行该任务。DiscardPolicy
: 不处理该任务(也就是将其丢弃)。DiscardOldestPolicy
: 丢弃队列中最旧的任务,然后重试执行当前任务。
示例代码
1. Fixed Thread Pool
import java.util.concurrent.*;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 设置线程池参数
int corePoolSize = 3; // 核心线程数
int maximumPoolSize = 3; // 最大线程数
long keepAliveTime = 60L; // 空闲线程存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 工作队列
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略
// 创建线程池
ExecutorService fixedThreadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
handler
);
for (int i = 0; i < 10; i++) {
final int taskId = i;
fixedThreadPool.submit(() -> {
System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
fixedThreadPool.shutdown();
while (!fixedThreadPool.isTerminated()) {
// 等待所有任务完成
}
System.out.println("All tasks completed.");
}
}
2. Cached Thread Pool
import java.util.concurrent.*;
public class CachedThreadPoolExample {
public static void main(String[] args) {
// 设置线程池参数
int corePoolSize = 0; // 核心线程数
int maximumPoolSize = Integer.MAX_VALUE; // 最大线程数
long keepAliveTime = 60L; // 空闲线程存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new SynchronousQueue<>(); // 工作队列
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略
// 创建线程池
ExecutorService cachedThreadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
handler
);
for (int i = 0; i < 10; i++) {
final int taskId = i;
cachedThreadPool.submit(() -> {
System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
cachedThreadPool.shutdown();
while (!cachedThreadPool.isTerminated()) {
// 等待所有任务完成
}
System.out.println("All tasks completed.");
}
}
3. Scheduled Thread Pool
import java.util.concurrent.*;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
// 设置线程池参数
int corePoolSize = 2; // 核心线程数
int maximumPoolSize = 2; // 最大线程数
long keepAliveTime = 60L; // 空闲线程存活时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 工作队列
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略
// 创建线程池
ScheduledExecutorService scheduledThreadPool = new ScheduledThreadPoolExecutor(
corePoolSize,
handler
);
for (int i = 0; i < 10; i++) {
final int taskId = i;
scheduledThreadPool.scheduleAtFixedRate(() -> {
System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 0, 2, TimeUnit.SECONDS);
}
// 关闭线程池
scheduledThreadPool.shutdown();
while (!scheduledThreadPool.isTerminated()) {
// 等待所有任务完成
}
System.out.println("All tasks completed.");
}
}
4. Single Thread Executor
import java.util.concurrent.*;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
// 设置线程池参数
int corePoolSize = 1; // 核心线程数
int maximumPoolSize = 1; // 最大线程数
long keepAliveTime = 0L; // 空闲线程存活时间
TimeUnit unit = TimeUnit.MILLISECONDS; // 时间单位
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); // 工作队列
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // 拒绝策略
// 创建线程池
ExecutorService singleThreadExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
handler
);
for (int i = 0; i < 10; i++) {
final int taskId = i;
singleThreadExecutor.submit(() -> {
System.out.println("Task ID: " + taskId + " is running by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
singleThreadExecutor.shutdown();
while (!singleThreadExecutor.isTerminated()) {
// 等待所有任务完成
}
System.out.println("All tasks completed.");
}
}
在上面的每个示例中,我使用ThreadPoolExecutor
构造函数显式地设置了线程池参数,并根据每种线程池的特性和用途配置了合适的参数。例如,在FixedThreadPool
示例中,核心线程数与最大线程数相同,而在CachedThreadPool
示例中,核心线程数为0,最大线程数为Integer.MAX_VALUE
,以适应动态任务需求。对于ScheduledThreadPool
,虽然它也有核心线程数和最大线程数的参数,但通常我们使用ScheduledThreadPoolExecutor
的构造函数来创建定时线程池,而不是直接使用ThreadPoolExecutor
。
线程中断
线程中断是Java多线程编程中的一个重要特性,它允许一个线程请求另一个线程停止执行。当一个线程被中断时,它会抛出InterruptedException
,这通常发生在阻塞操作中,比如Thread.sleep()
, Object.wait()
, 或者LockSupport.park()
等。
如何中断一个线程
要中断一个线程,可以调用线程对象的interrupt()
方法。这会设置线程的中断标志,并且如果线程正在执行一个阻塞操作,它会抛出InterruptedException
。
检查线程中断状态
每个线程都有一个内部中断标志,可以通过以下方法来检查或清除这个标志:
Thread.interrupted()
: 返回当前线程的中断状态,并清除中断标志。Thread.isInterrupted()
: 返回当前线程或给定线程的中断状态,但不会清除中断标志。
示例代码
下面是一个简单的示例,演示如何中断一个线程:
public class InterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("Thread interrupted");
return;
}
try {
Thread.sleep(1000);
System.out.println("Thread is running");
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置中断标志
System.out.println("Thread interrupted during sleep");
return;
}
}
});
thread.start();
Thread.sleep(5000); // 等待5秒后中断线程
thread.interrupt();
thread.join();
System.out.println("Main thread finished.");
}
}
在这个例子中,主线程创建了一个新线程并启动它。新线程在无限循环中每秒打印一条消息,并检查是否被中断。如果被中断,它会退出循环并结束线程。主线程等待5秒后中断新线程,并等待新线程结束。
注意事项
- 中断标志: 中断标志是一个线程级别的标志,当线程被中断时,这个标志被设置。当线程抛出
InterruptedException
时,中断标志会被清除。因此,在捕获InterruptedException
后,通常需要重新设置中断标志,以保持中断状态的一致性。 - 非阻塞操作: 如果线程在非阻塞操作中被中断,它不会抛出
InterruptedException
。因此,线程应定期检查它的中断状态。 - 资源清理: 在线程中处理中断时,不要忘记清理任何打开的资源或进行必要的清理操作。
守护线程
守护线程(Daemon Threads)是Java多线程编程中的一个重要概念。它们通常用于执行后台任务,如垃圾收集、日志记录、心跳检测等,这些任务对于程序的正常运行是辅助性的。当程序中的所有用户线程(非守护线程)都结束时,守护线程会自动结束,不需要显式地关闭它们。
守护线程的特点
- 自动结束:当Java程序中没有非守护线程在运行时,所有的守护线程都会自动结束,即使它们仍在执行。
- 辅助性:守护线程通常用于执行后台任务,这些任务不是程序的主要业务逻辑,但对程序的运行是有益的。
- 生命周期:守护线程的生命周期与其他线程相同,但它们的行为受到程序中其他线程的影响。
创建守护线程
要创建一个守护线程,需要在调用Thread.start()
方法之前,通过调用Thread.setDaemon(true)
方法将线程标记为守护线程。
示例代码
下面是一个简单的示例,展示了如何创建一个守护线程:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon thread running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
// 设置线程为守护线程
daemonThread.setDaemon(true);
// 启动守护线程
daemonThread.start();
// 主线程睡眠一段时间后结束
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Main thread finished.");
}
}
在这个示例中,守护线程每隔一秒打印一条消息。主线程睡眠5秒后结束。由于守护线程是作为守护线程创建的,所以当主线程结束时,守护线程也会自动结束。
注意事项
- 守护线程的启动:守护线程必须在调用
start()
方法之前设置为守护线程,一旦线程开始运行,就不能改变它的守护状态。 - 资源释放:如果守护线程持有资源(如文件句柄、网络连接等),则应在主线程结束前确保这些资源被妥善释放,否则可能导致资源泄露。
- 异常处理:守护线程通常不应该抛出未捕获的异常,因为这可能导致程序异常终止。因此,最好在守护线程中捕获异常并妥善处理。
线程组
线程组(Thread Group)是Java多线程编程中的一个概念,它用于组织和管理一组线程。线程组提供了一种将线程分组的方式,使得可以对这些线程进行统一的管理和控制。线程组可以嵌套,也就是说,一个线程组可以包含其他的线程组,形成层次结构。
线程组的作用
- 组织线程:线程组提供了一种将线程按照功能或逻辑进行分类的方法。
- 管理线程:可以通过线程组来启动、挂起、恢复或终止线程。
- 线程安全:线程组提供了一种机制来限制哪些线程可以访问或控制其他线程。
创建线程组
要创建一个线程组,可以使用ThreadGroup
类的构造函数。通常,线程组会在创建线程时指定。每个线程默认属于其创建者的线程组,如果没有指定线程组,则属于系统的默认线程组。
示例代码
下面是一个简单的示例,展示了如何创建线程组和向其中添加线程:
public class ThreadGroupExample {
public static void main(String[] args) {
// 创建线程组
ThreadGroup group = new ThreadGroup("MyGroup");
// 创建线程并将其加入到线程组中
Thread thread = new Thread(group, () -> {
System.out.println("Hello from " + Thread.currentThread().getName());
}, "ThreadInGroup");
// 启动线程
thread.start();
// 输出线程组的信息
System.out.println("Thread Group Name: " + group.getName());
System.out.println("Active Count: " + group.activeCount());
// 等待线程结束
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
在这个示例中,我们首先创建了一个名为"MyGroup"的线程组。接着,创建了一个线程,并将其加入到这个线程组中。然后启动了这个线程,并输出了线程组的名称和活动线程的数量。
线程组的方法
ThreadGroup
类提供了多种方法来管理和控制线程组内的线程:
void destroy()
: 销毁线程组及其所有子线程和子线程组(仅当线程组中没有任何活动线程时才可用)。int activeCount()
: 返回线程组中当前活动线程的数量。void enumerate(Thread[] threads)
: 将线程组中当前活动的线程复制到数组中。void checkAccess()
: 检查当前线程是否有权限访问该线程组。void stop()
: 请求线程组中的所有线程停止执行(不推荐使用,因为这可能会导致资源泄露)。void suspend()
: 暂停线程组中的所有线程。void resume()
: 恢复线程组中所有被暂停的线程。
注意事项
- 安全性:线程组提供了一种安全机制,只有创建线程组的线程才能访问和控制该线程组中的线程。这有助于保护线程不受未经授权的线程的干扰。
- 资源管理:线程组可以帮助管理线程的生命周期,比如通过
destroy()
方法来销毁整个线程组,这在某些情况下可能是有用的。 - 局限性:尽管线程组提供了一定程度的管理能力,但在现代Java并发编程中,线程池和
ExecutorService
等更高级的工具通常被视为更高效和更灵活的选择。线程组主要用于早期版本的Java,现在更多的是作为一种历史遗留的概念。
线程本地存储
线程本地存储(Thread Local Storage, TLS)是Java多线程编程中的一个重要概念,它提供了一种机制,使得每个线程都可以拥有自己独立的变量副本。ThreadLocal
类是Java标准库中用于实现这一特性的工具。
什么是线程本地存储?
在多线程环境中,多个线程可能会共享同一个对象或变量。当这些线程试图同时修改这些共享变量时,就需要考虑同步问题,以避免竞态条件和数据不一致。然而,在某些情况下,我们希望每个线程都能拥有自己的变量副本,而不必担心线程之间的同步问题。这就是线程本地存储的目的。
ThreadLocal
类的使用
ThreadLocal
类提供了一种简单而有效的机制来实现线程本地存储。使用ThreadLocal
类时,每个线程都可以拥有一个与该线程绑定的变量副本。这些副本是相互独立的,一个线程对它的副本所做的修改不会影响到其他线程。
创建ThreadLocal
实例
创建ThreadLocal
实例非常简单,只需要创建一个ThreadLocal
对象即可。你可以选择在构造函数中提供一个初始值,或者在需要的时候再设置值。
示例代码
下面是一个简单的示例,展示了如何使用ThreadLocal
:
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocal.set(100);
System.out.println("Thread 1: " + threadLocal.get());
});
Thread thread2 = new Thread(() -> {
threadLocal.set(200);
System.out.println("Thread 2: " + threadLocal.get());
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Main thread: " + threadLocal.get());
}
}
在这个示例中,我们定义了一个ThreadLocal
变量threadLocal
,并为其提供了一个初始值0
。接着,我们在两个不同的线程中分别设置不同的值,并打印出这些值。注意,每个线程中的输出都是独立的,不受其他线程的影响。
ThreadLocal
类的方法
ThreadLocal
类提供了以下主要方法:
get()
: 获取当前线程中变量的副本。set(T value)
: 设置当前线程中变量的副本。remove()
: 移除当前线程中的变量副本。initialValue()
: 可选方法,返回当前线程中变量的初始值。
注意事项
- 内存泄漏:当不再需要某个
ThreadLocal
变量时,应该调用remove()
方法来移除当前线程中的变量副本。否则,即使线程结束了,ThreadLocal
变量的副本仍会被保留,这可能导致内存泄漏。 - 初始化:默认情况下,
ThreadLocal
变量的初始值为null
。如果需要设置特定的初始值,可以通过覆盖initialValue()
方法来实现。 - 性能考虑:虽然
ThreadLocal
可以简化多线程编程,但频繁地调用get()
和set()
方法可能会对性能产生影响,尤其是当线程频繁创建和销毁时。因此,在可能的情况下,尽量减少ThreadLocal
的使用频率。
总结
Java多线程技术是Java平台的核心特性之一,它允许开发人员构建高度并发的应用程序,充分利用现代多核处理器的硬件资源。多线程编程虽然强大但也带来了诸多挑战,如竞态条件、死锁、资源竞争等问题。下面是对Java多线程技术的总结和完善:
Java多线程的核心概念
- 并发性:多个线程可以同时运行,尽管在单核处理器上可以表现为交替执行。
- 共享资源:线程之间共享相同的内存空间,这意味着它们可以访问和修改相同的变量,但这也可能导致数据不一致的问题,除非采取适当的同步措施。
- 上下文切换:操作系统在多个线程之间切换执行,这称为上下文切换,会带来一定的开销。
- 独立性:每个线程都有自己的执行流,它们可以独立于其他线程执行,但也可以通过线程间通信机制协作。
实现多线程的方式
- 继承
Thread
类:创建Thread
类的子类,并重写run()
方法。 - 实现
Runnable
接口:创建实现了Runnable
接口的类,并实现run()
方法。 - 使用
Executor
框架:通过ExecutorService
接口创建线程池来管理线程。 - 使用
Callable
和Future
接口:创建实现了Callable
接口的类,可以返回结果,并使用Future
来获取结果。
线程间通信
- 共享内存:多个线程共享相同的内存空间,通过读写共享变量来通信。必须注意同步访问,以防止竞态条件。
- 信号量和条件变量:信号量用于管理资源的访问权限。条件变量允许线程等待特定条件的满足。
- 消息队列:线程可以向队列发送消息,其他线程可以从队列中读取消息。
- 管道(Pipes):允许线程或进程之间通过管道进行通信。
- 事件对象:用于信号通知,可以用来同步线程。
使用Object
的wait()
和notify()
方法
这是Java中最基本的线程间通信方式之一。wait()
方法使当前线程等待,直到另一个线程调用notify()
或notifyAll()
方法。这些方法必须在同步块内调用,通常在synchronized
块或方法中。
使用Lock
接口和Condition
接口
Lock
接口提供了更高级的锁定机制,而Condition
接口则提供了更灵活的线程等待和唤醒机制。这些接口位于java.util.concurrent.locks
包中。
避免死锁
- 破坏互斥条件:尽可能使资源可共享,减少独占资源的需求;避免使用锁。
- 破坏占有并等待条件:确保线程在开始执行之前获取所有必需的锁;按顺序获取锁。
- 破坏非抢占条件:为锁请求添加超时机制;使用
ReentrantLock
的tryLock()
方法。 - 破坏循环等待条件:如果一个线程需要多个锁,确保总是按照相同的顺序获取这些锁;定期检查是否有可能出现死锁的情况。
线程池
- 固定线程池:通过
Executors.newFixedThreadPool()
创建固定大小的线程池。 - 缓存线程池:通过
Executors.newCachedThreadPool()
创建可以缓存线程的线程池。 - 单线程执行器:通过
Executors.newSingleThreadExecutor()
创建只包含一个线程的线程池。 - 定时线程池:通过
Executors.newScheduledThreadPool()
创建可以安排任务的线程池。
线程中断
线程可以被中断,以请求线程提前结束。线程可以通过调用Thread.interrupt()
方法被中断。当一个线程被中断时,它会抛出InterruptedException
,这通常发生在阻塞操作中。
守护线程
守护线程是那些在后台运行,为其他线程服务的线程,当所有非守护线程结束时,守护线程自动结束。守护线程通常用于执行后台任务,如垃圾收集、日志记录等。
线程组
线程组用于组织和管理一组线程。通过ThreadGroup
类可以创建线程组,并将线程加入到线程组中,从而方便地管理和控制线程。
线程本地存储
ThreadLocal
类提供了一个线程本地变量,每个线程都有自己的副本。ThreadLocal
可以用来存储线程特定的数据,避免了线程之间的数据共享和同步问题。