线程
线程(Thread)是计算机中最小的执行单元。在操作系统中,一个进程可以包含多个线程,这些线程共享进程的资源,但每个线程都有自己的执行流程。
以下是一些与线程相关的基本概念:
-
进程(Process): 进程是计算机中的一个程序在执行时所占用的内存空间、系统资源等的总称。一个进程可以包含多个线程,进程之间相互独立。
-
线程(Thread): 线程是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。一个进程可以包含一个或多个线程。线程共享进程的地址空间和资源,但拥有独立的执行流程。
-
多线程(Multithreading): 多线程是指在一个进程中同时运行多个线程。多线程可以提高程序的并发性,使得多个任务可以并行执行,提高系统资源利用率。
-
并发(Concurrency): 并发是指在同一时间间隔内执行多个相对独立的任务。在多线程环境中,多个线程可以并发执行,从而提高程序的响应性和效率。
-
互斥(Mutual Exclusion): 互斥是指在同一时刻只允许一个线程访问共享资源。互斥机制的目的是防止多个线程同时对共享资源进行写操作,避免数据的不一致性。
-
同步(Synchronization): 同步是指协调多个线程之间的执行顺序,确保它们按照某种顺序访问共享资源。同步机制的目的是保证数据的一致性。
-
线程安全(Thread-Safe): 线程安全是指在多线程环境中,对共享资源的访问不会引起数据的错误或不一致性。线程安全的设计通常需要考虑互斥、同步等机制。
-
守护线程(Daemon Thread): 守护线程是在程序运行时在后台提供服务的线程。当所有的非守护线程结束时,守护线程会自动结束。典型的守护线程包括垃圾回收线程。
-
线程池(Thread Pool): 线程池是一种管理和复用线程的机制。通过线程池,可以降低线程创建和销毁的开销,提高系统性能。
-
死锁(Deadlock): 死锁是指两个或多个线程互相等待对方释放资源,导致程序无法继续执行的状态。
这些概念涵盖了多线程编程中的基本概念和常见问题。在多线程编程中,正确地处理并发、互斥和同步是至关重要的。
线程调度
线程调度是操作系统中的一个重要概念,它涉及到在多线程环境中决定哪个线程将被执行的过程。操作系统使用调度器来管理和调度系统中的线程,确保它们在处理器上得到合理的执行时间。
线程调度的主要目标包括:
-
公平性: 所有线程应该有平等的机会获得CPU时间,以避免某个线程长时间占用CPU而导致其他线程无法执行。
-
效率: 系统应该以最佳方式利用CPU资源,确保尽可能多的线程能够并行执行。
-
响应时间: 对于需要快速响应的任务,系统应该能够及时调度相应的线程,以满足用户或应用程序的需求。
不同的调度算法可以用来实现这些目标,例如:
-
先来先服务 (FCFS): 按照线程到达的顺序进行调度,先到达的线程先执行。
-
轮转调度 (Round Robin): 每个线程获得一个固定的时间片,在时间片用尽后,调度到下一个线程。
-
优先级调度: 给每个线程分配一个优先级,高优先级的线程先执行。
-
多级反馈队列调度: 将线程分组成多个队列,每个队列有不同的优先级,线程根据其行为在不同队列之间移动。
-
最短作业优先 (SJF): 执行时间最短的线程先执行。
不同的应用场景和系统特性可能需要不同的调度算法。线程调度的设计要考虑到系统的性能、资源利用率和用户体验。
应用场景
多线程的应用场景很广泛,它们可以用于提高程序的并发性和性能。以下是一些常见的多线程应用场景:
-
图形界面(GUI)应用程序: 在图形用户界面应用中,使用多线程可以确保用户界面的响应性,同时执行后台任务,例如文件下载、数据处理等,而不会阻塞用户界面。
-
网络编程: 在网络应用中,多线程可用于处理多个客户端的连接请求。每个客户端连接可以由一个独立的线程处理,以提高并发性。
-
服务器应用程序: 服务器常常需要同时处理多个客户端请求。多线程可用于并发处理这些请求,提高服务器的吞吐量。
-
并行计算: 多线程可以用于并行执行计算密集型任务,充分利用多核处理器的性能,加速程序的执行。
-
数据库操作: 在数据库应用中,可以使用多线程来处理多个数据库查询或事务,提高数据库访问的效率。
-
实时系统: 在需要实时响应的系统中,多线程可以用于及时处理事件、数据采集或控制任务。
-
游戏开发: 在游戏中,通常需要并发处理图形渲染、用户输入、物理模拟等多个任务,多线程可用于提高游戏性能。
-
数据处理和分析: 在数据科学和大数据领域,多线程可以用于同时处理大量数据,加速数据处理和分析任务。
-
多媒体应用: 音视频播放和处理通常需要同时进行多个任务,例如解码、渲染、网络传输等,多线程可以提高多媒体应用的效率。
-
定时任务: 多线程可用于执行定时任务,例如定时备份、定时检查更新等。
总体而言,多线程适用于需要同时执行多个任务、提高系统并发性和响应性的应用场景。然而,需要谨慎设计和管理多线程,以避免竞态条件和死锁等并发编程问题。
线程的创建和使用
在Java中,线程的创建和使用可以通过两种方式实现:继承Thread
类和实现Runnable
接口。以下是详细介绍这两种方式的方法:
1. 继承 Thread
类
通过继承Thread
类,创建一个新的类并重写run
方法,该方法定义了线程的主体逻辑。然后,可以实例化该类并调用start
方法启动线程。
// 定义一个继承Thread类的线程类
class MyThread extends Thread {
public void run() {
// 线程的主体逻辑
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value " + i);
}
}
}
public class ThreadExample {
public static void main(String args[]) {
// 创建线程实例
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 启动线程
thread1.start();
thread2.start();
}
}
-
优点:
- 直观:代码相对简单,直接继承
Thread
类,重写run
方法。 - 方便:对于简单的线程任务,可以更直接地实现。
- 直观:代码相对简单,直接继承
-
缺点:
- 由于Java不支持多重继承,如果已经继承了其他类,则无法再继承
Thread
类。 - 不利于资源共享:线程的代码和线程本身耦合在一起,不够灵活。
- 由于Java不支持多重继承,如果已经继承了其他类,则无法再继承
class MyThread extends Thread {
public void run() {
// 线程的主体逻辑
}
}
2. 实现 Runnable
接口
通过实现Runnable
接口,创建一个实现了run
方法的类,并将该类的实例传递给Thread
类的构造函数。然后,可以实例化Thread
并调用start
方法启动线程。
// 实现Runnable接口的线程类
class MyRunnable implements Runnable {
public void run() {
// 线程的主体逻辑
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value " + i);
}
}
}
public class ThreadExample {
public static void main(String args[]) {
// 创建Runnable实例
MyRunnable myRunnable = new MyRunnable();
// 创建线程并传入Runnable实例
Thread thread1 = new Thread(myRunnable);
Thread thread2 = new Thread(myRunnable);
// 启动线程
thread1.start();
thread2.start();
}
}
-
优点:
- 灵活:实现了
Runnable
接口的类可以继续继承其他类,提高了代码的灵活性。 - 资源共享:多个线程可以共享同一个实现了
Runnable
接口的对象,更容易实现资源共享。
- 灵活:实现了
-
缺点:
- 稍微繁琐:需要创建一个实现
Runnable
接口的类,并将其传递给Thread
类的构造函数。
- 稍微繁琐:需要创建一个实现
class MyRunnable implements Runnable {
public void run() {
// 线程的主体逻辑
}
}
// 创建线程并传入Runnable实例
Thread thread = new Thread(new MyRunnable());
选择方式的依据
-
类继承 vs. 接口实现: 如果已经继承了其他类,或者希望实现多个接口,那么应该选择实现
Runnable
接口的方式。 -
资源共享: 如果多个线程需要共享同一个实例,可以使用实现
Runnable
接口的方式,因为多个线程可以共享同一个Runnable
对象。 -
代码结构: 如果线程的主体逻辑相对简单,而且不需要继承其他类,直接继承
Thread
类可能更为直观。
总体来说,两种方式都可以完成线程的创建和使用,选择取决于具体的需求和代码结构。在实际应用中,更常见的是实现Runnable
接口的方式,因为它更灵活,支持资源共享。
线程生命周期
- 新建状态(New): 当线程对象被创建时,线程处于新建状态。
- 就绪状态(Runnable): 当调用
start
方法后,线程进入就绪状态。在就绪状态下,线程等待CPU调度执行。 - 运行状态(Running): 当CPU开始执行线程的
run
方法时,线程进入运行状态。 - 阻塞状态(Blocked): 当线程被阻塞(例如等待I/O完成、等待锁等)时,线程进入阻塞状态。
- 死亡状态(Dead): 当线程执行完
run
方法或调用stop
方法时,线程进入死亡状态。
线程同步
在多线程环境下,为了避免竞态条件和确保数据的一致性,可能需要使用synchronized
关键字来同步线程访问共享资源。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
这是一个简单的线程安全的计数器类,其中increment
和getCount
方法都被synchronized
修饰,确保对count
的操作是原子的。
以上是Java中线程的基本创建、启动和同步的详细介绍。在实际应用中,需要特别注意线程安全性和避免死锁等问题。
在Java中,线程的创建方式有两种:继承Thread
类和实现Runnable
接口。这两种方式在使用上有一些区别,主要涉及到类继承和接口实现的不同以及对资源共享的处理。
在Java中,可以通过继承Thread
类或实现Runnable
接口创建线程。
设置线程名称:
-
继承
Thread
类的方式:class MyThread extends Thread { public MyThread(String name) { super(name); } public void run() { // 线程的主体逻辑 } } // 创建线程时指定线程名称 Thread thread = new MyThread("MyThread");
-
实现
Runnable
接口的方式:class MyRunnable implements Runnable { public void run() { // 线程的主体逻辑 } } // 创建线程时传入Runnable实例,并指定线程名称 Thread thread = new Thread(new MyRunnable(), "MyThread");
获取线程名称:
通过getName
方法获取线程的名称。
String threadName = thread.getName();
System.out.println("Thread Name: " + threadName);
以上方式可以帮助标识和识别线程,在调试和日志输出中非常有用。
线程的优先级
在Java中,线程的优先级用整数表示,范围从1到10。线程的优先级越高,表示它在竞争CPU时间时更有可能被调度执行。然而,线程优先级的具体影响因平台而异,不同操作系统可能以不同的方式处理线程的优先级。
设置线程优先级
可以使用setPriority
方法设置线程的优先级。优先级分为三个常量:
Thread.MIN_PRIORITY
:表示最低优先级(1)。Thread.NORM_PRIORITY
:表示默认优先级(5)。Thread.MAX_PRIORITY
:表示最高优先级(10)。
Thread thread = new Thread(new MyRunnable());
thread.setPriority(Thread.NORM_PRIORITY); // 设置线程的优先级为默认优先级
获取线程优先级
可以使用getPriority
方法获取线程的优先级。
int priority = thread.getPriority();
System.out.println("Thread Priority: " + priority);
注意事项
-
线程的优先级并不是绝对的: 优先级较高的线程更有可能被调度执行,但并不代表绝对顺序。线程调度是由操作系统的线程调度器决定的。
-
平台差异: 不同操作系统可能以不同的方式处理线程的优先级,因此在跨平台应用中,不应过于依赖线程优先级。
-
避免过度依赖优先级: 过度使用线程优先级可能导致不可预测的行为,应该谨慎使用,并且更多地依赖于合适的线程同步和协调机制。
-
默认优先级: 新创建的线程默认继承其父线程的优先级。主线程的默认优先级通常是
Thread.NORM_PRIORITY
。
在实际应用中,通常情况下不太需要显式地设置线程的优先级,除非确实需要微调线程的执行顺序。更重要的是通过合适的同步和协调机制来确保线程安全和正确性。
线程终止
在Java中,线程的终止可以通过两种主要的机制来实现:正常终止和强制终止。
1. 正常终止
正常终止是指线程执行完其run
方法中的代码,自然结束。线程的正常终止通常是通过run
方法的执行完毕来实现的。
class MyRunnable implements Runnable {
public void run() {
// 线程的主体逻辑
System.out.println("Thread execution completed.");
}
}
// 创建线程并启动
Thread thread = new Thread(new MyRunnable());
thread.start();
// 等待线程执行完毕
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
在上面的例子中,通过join
方法等待线程执行完毕,从而实现线程的正常终止。
2. 强制终止
强制终止是指通过一些手段来强制终止线程的执行。然而,强制终止线程是不安全的,可能导致资源泄漏和数据不一致。在Java中,不建议使用强制终止线程的方式。
一个线程可以调用interrupt
方法中断另一个线程,但被中断的线程需要检查中断标志并自行决定是否终止。例如:
class MyRunnable implements Runnable {
public void run() {
while (!Thread.interrupted()) {
// 线程的主体逻辑
}
System.out.println("Thread interrupted. Exiting.");
}
}
// 创建线程并启动
Thread thread = new Thread(new MyRunnable());
thread.start();
// 在某个时刻中断线程
thread.interrupt();
在上面的例子中,线程通过检查Thread.interrupted()
来判断是否被中断,如果中断标志被设置,线程自行决定终止执行。
注意事项
- 强制终止线程可能导致资源泄漏和不一致的状态,应该谨慎使用。
- 正常终止是更为安全和可控的方式,通过协调线程的执行逻辑来实现。
- 可以使用
Thread.isInterrupted()
方法来检查线程的中断状态。
总体而言,推荐使用正常终止的方式来管理线程的生命周期,通过协调和同步机制来实现线程的安全退出。
线程礼让
线程礼让是指一个线程主动让出CPU资源,使其他线程有更多的机会执行。在Java中,可以使用 Thread.yield()
方法来实现线程的礼让。线程礼让并不是强制性的,仅仅是一种建议,操作系统和线程调度器可以选择是否响应。
使用 Thread.yield()
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " Value " + i);
// 在每次循环中礼让CPU资源
Thread.yield();
}
}
}
public class ThreadYieldExample {
public static void main(String args[]) {
// 创建两个线程
Thread thread1 = new Thread(new MyRunnable(), "Thread-1");
Thread thread2 = new Thread(new MyRunnable(), "Thread-2");
// 启动线程
thread1.start();
thread2.start();
}
}
在上面的例子中,Thread.yield()
被用于在每次循环中让出CPU资源。当一个线程调用 yield
方法时,它会让出自己的CPU时间片,然后重新进入就绪状态,让其他线程有机会获得CPU执行时间。
注意事项
Thread.yield()
方法是一个静态方法,可以通过类名直接调用。- 调用
yield
并不会让线程进入阻塞状态,它仅仅是将线程从运行状态转为就绪状态,让其他就绪状态的线程有机会执行。 - 线程礼让的效果在不同操作系统和JVM实现中可能有差异,不一定能够产生明显的效果。
虽然线程礼让在某些情况下可以提高多线程程序的执行效率,但在实际应用中需要谨慎使用,因为过度的线程礼让可能导致线程执行效率下降。通常情况下,合理的线程同步和协调机制更为重要。
线程安全
关键字Synchronization
在Java中,synchronized
关键字的底层原理涉及到对象头中的锁标志位、监视器(Monitor)以及底层的操作系统的支持。
-
对象头中的锁标志位: 每个Java对象在内存中都有一个对象头,其中包含了用于存储锁信息的标志位。这个标志位用于标识对象的锁状态,以及哪个线程拥有该对象的锁。
-
Monitor: 每个Java对象都与一个Monitor相关联,它负责管理对象的锁。Monitor包含了锁的信息,如拥有锁的线程、等待锁的线程队列等。
-
进入和退出Monitor: 当线程进入一个
synchronized
方法或代码块时,它会尝试获取对象的Monitor。如果对象的锁标志位表示没有其他线程占有锁,当前线程就会获得锁,锁标志位被设置为当前线程的ID。如果对象的锁已经被其他线程占有,当前线程就会被阻塞并加入到等待队列中。当线程退出同步代码块时,它会释放锁,将锁标志位重置为无锁状态。 -
底层操作系统的支持:
synchronized
关键字的实现还依赖于底层操作系统提供的原子性操作,比如CAS(Compare and Swap)。这些原子性操作保证了在多线程环境中,对锁的获取和释放是原子的,不会发生中断。
总体而言,synchronized
关键字的底层原理涉及到对象头的锁标志位、Monitor的管理、线程的阻塞和唤醒,以及底层操作系统提供的原子性操作。这些机制共同保证了多线程环境下的同步访问和线程安全性。
线程安全是指当多个线程同时访问共享资源时,不会发生数据的错误、数据不一致或者其他异常的情况。确保线程安全是多线程编程中一个重要的目标,因为多线程环境下的并发访问可能导致竞态条件、死锁、数据不一致等问题。
关键字ReentrantLock
当一个线程在ReentrantLock
上调用lock
方法时,它实际上是在尝试获取锁。这个获取锁的过程涉及到以下几个重要的步骤:
-
非公平性和公平性选择:
ReentrantLock
可以在创建时选择是公平的还是非公平的。在公平模式下,锁会按照线程请求的顺序分配。在非公平模式下,锁可能会优先分配给当前正在等待的线程,而不考虑其他线程的顺序。 -
尝试CAS操作获取锁: 当一个线程调用
lock
方法时,首先会尝试使用CAS操作来原子地将同步状态(state
)从零增加到一。如果这一步成功,表示该线程成功获取到了锁。如果失败,说明锁已经被其他线程持有,这时将进入队列等待。 -
同步状态的增加和减少: 一旦线程成功获取到锁,同步状态(
state
)会被增加,表示锁被持有的次数。每次成功调用lock
,同步状态加一。当线程调用unlock
方法释放锁时,同步状态减一。只有当同步状态减到零时,锁才被完全释放,其他线程可以争夺锁。 -
等待队列: 如果一个线程在获取锁时失败(即同步状态不为零),它会被加入到等待队列中。等待队列是一个FIFO队列,用于存储等待锁的线程。这些线程在释放锁的时候,会唤醒等待队列中的第一个线程,使其有机会再次尝试获取锁。
-
可重入性:
ReentrantLock
支持线程的可重入性,即同一个线程可以多次获取同一个锁而不会死锁。每次成功获取锁,同步状态加一,每次释放锁,同步状态减一。只有当同步状态减到零时,锁才会完全释放。
整个过程涉及到对同步状态的原子性修改、等待队列的管理、线程的唤醒等复杂的操作,这些都由AbstractQueuedSynchronizer
框架来支持。ReentrantLock
在这个框架上构建了一种可重入、可中断、可定时等高级的锁机制,提供了更多的灵活性和控制力。这使得ReentrantLock
比传统的synchronized
更适用于一些复杂的多线程场景。
以下是确保线程安全的一些常见方式:
1. 互斥锁(Synchronization)
使用 synchronized
关键字来保护共享资源,确保同一时刻只有一个线程能够访问该资源。这样可以避免竞态条件(Race Condition)。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2. 使用 Lock 接口
使用 java.util.concurrent.locks
包中的 Lock
接口及其实现类,例如 ReentrantLock
,来进行更细粒度的锁控制。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
3. 原子操作(Atomic Operations)
使用 java.util.concurrent.atomic
包中的原子类,例如 AtomicInteger
,来执行原子操作,从而避免竞态条件。
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
4. 使用线程安全的集合类
Java提供了一些线程安全的集合类,例如 ConcurrentHashMap
,CopyOnWriteArrayList
,它们本身就提供了线程安全的操作。
import java.util.concurrent.ConcurrentHashMap;
class SharedResource {
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
public void update(String key, int value) {
map.put(key, value);
}
public int getValue(String key) {
return map.getOrDefault(key, 0);
}
}
5. 避免不可变对象
使用不可变对象可以避免线程安全问题,因为不可变对象的状态不会发生改变。例如,使用 String
或 ImmutableList
。
class ImmutableResource {
private final String data;
public ImmutableResource(String data) {
this.data = data;
}
public String getData() {
return data;
}
}
注意事项:
- 了解并发编程的基本原理,避免竞态条件、死锁等问题。
- 尽量使用更高级别的并发工具,例如
java.util.concurrent
包中提供的类。 - 谨慎使用
synchronized
,避免出现性能问题。
确保线程安全是多线程编程中的一项复杂任务,需要综合考虑性能、可维护性和代码清晰度。
线程通讯
线程通讯是指多个线程之间通过特定的机制协调和合作,以完成一些共同的任务。在Java中,常用的线程通讯机制包括使用 wait()
、notify()
、notifyAll()
方法以及 Condition
接口等。
1. 使用 wait()
、notify()
、notifyAll()
这三个方法通常与synchronized
关键字一起使用,用于在不同线程之间进行通讯。
wait()
: 让当前线程等待,并释放对象的锁。notify()
: 唤醒等待队列中的一个线程。notifyAll()
: 唤醒等待队列中的所有线程。
示例:
class SharedResource {
private boolean flag = false;
public synchronized void produce() {
while (flag) {
try {
wait(); // 等待消费者消费完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产操作
System.out.println("Producing...");
flag = true;
notify(); // 唤醒一个消费者线程
}
public synchronized void consume() {
while (!flag) {
try {
wait(); // 等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费操作
System.out.println("Consuming...");
flag = false;
notify(); // 唤醒一个生产者线程
}
}
在上面的例子中,produce
方法负责生产,consume
方法负责消费,通过 flag
控制生产者和消费者的状态。使用 wait()
和 notify()
进行线程通讯,确保在合适的时机唤醒对方线程。
2. 使用 Condition
接口
Condition
接口提供了更灵活和强大的线程通讯机制,通常与 ReentrantLock
结合使用。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class SharedResource {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean flag = false;
public void produce() {
lock.lock();
try {
while (flag) {
try {
condition.await(); // 等待消费者消费完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产操作
System.out.println("Producing...");
flag = true;
condition.signal(); // 唤醒一个消费者线程
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (!flag) {
try {
condition.await(); // 等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费操作
System.out.println("Consuming...");
flag = false;
condition.signal(); // 唤醒一个生产者线程
} finally {
lock.unlock();
}
}
}
在上述例子中,使用 ReentrantLock
和 Condition
接口替代了 synchronized
、wait()
、notify()
的机制。await()
替代 wait()
,signal()
替代 notify()
。这种方式提供了更灵活的线程通讯和更细粒度的锁控制。
无论使用哪种方式,线程通讯的关键是确保线程之间能够在合适的时机进行通知和等待,以协调它们的执行顺序和共享资源的访问。
sleep和wait
sleep()
和 wait()
是在 Java 中用于线程控制的两个方法,它们之间有一些关键的区别:
-
调用方式:
sleep()
方法是Thread
类的静态方法,直接通过Thread.sleep()
调用。wait()
方法则是Object
类的实例方法,需要在某个对象上调用,例如object.wait()
。
-
使用的类别:
sleep()
主要用于线程间的时间控制,即让当前线程休眠一段时间。通常不释放锁。wait()
用于线程间的协调和通信,使得当前线程等待某个条件满足,同时会释放锁。
-
锁的释放:
- 在调用
sleep()
时,线程持有的锁不会被释放。其他线程无法获取这个锁,即使这个锁是共享锁也一样。 - 在调用
wait()
时,线程会释放它持有的锁,允许其他线程获取这个锁并执行。
- 在调用
-
使用场景:
sleep()
通常用于模拟耗时操作,或者在定时任务中执行等待一段时间的操作。wait()
通常用于线程间的协调,例如在多线程环境下,一个线程等待另一个线程完成某个操作后再继续执行。
-
异常处理:
sleep()
可能会抛出InterruptedException
,因为线程在睡眠期间可以被中断。wait()
必须在synchronized
块中调用,且通常会配合notify()
或notifyAll()
使用,同时可能会抛出InterruptedException
。
下面是一个简单的示例,演示了 sleep()
和 wait()
的用法:
class Example {
private final Object lock = new Object();
void sleepExample() {
try {
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void waitExample() {
synchronized (lock) {
try {
lock.wait(); // 等待条件满足
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
总的来说,sleep()
用于线程的休眠,而 wait()
用于线程的等待和协调。在使用它们时需要根据具体的需求选择合适的方法。
线程池
普通
在使用Java线程池时,通常需要考虑以下一些关键的参数和配置:
-
核心线程数(corePoolSize):
- 核心线程数是线程池中一直存活的线程数量。即使它们没有执行任务,它们也会一直存活。线程池会根据需要在核心线程数的基础上创建新的线程。
-
最大线程数(maximumPoolSize):
- 最大线程数是线程池中允许存在的最大线程数量。当队列满了且核心线程都在执行任务时,新的任务会导致创建新的线程,直到达到最大线程数。
-
线程空闲时间(keepAliveTime):
- 线程空闲时间是非核心线程在空闲状态下被保留的时间。当线程空闲时间超过该值时,非核心线程将被终止,直到线程数恢复到核心线程数。
-
任务队列(workQueue):
- 任务队列是用于保存尚未执行的任务的队列。当线程池中的线程数达到核心线程数时,新的任务会被放入任务队列。具体的队列类型可以选择不同的实现,例如
LinkedBlockingQueue
、ArrayBlockingQueue
等。 LinkedBlockingQueue
是一个基于链表实现的阻塞队列。它的容量可以是有限的,也可以是无限的(未指定容量时默认是无限)。当任务被提交到线程池时,如果线程池的线程数未达到核心线程数,任务会直接创建一个新线程执行,否则任务会被放入队列中。ArrayBlockingQueue
是一个基于数组实现的有界阻塞队列。它需要指定一个容量,当任务被提交到线程池时,如果线程池的线程数未达到核心线程数,任务会直接创建一个新线程执行,否则任务会被放入队列中。当队列已满时,新的任务将被拒绝。SynchronousQueue
是一个没有存储元素的阻塞队列。每个插入操作必须等待另一个线程的对应删除操作,反之亦然。这种队列可用于直接将任务传递给工作者线程。
- 任务队列是用于保存尚未执行的任务的队列。当线程池中的线程数达到核心线程数时,新的任务会被放入任务队列。具体的队列类型可以选择不同的实现,例如
-
拒绝策略(RejectedExecutionHandler):
- 拒绝策略定义了当任务无法被接受时的处理方式。常见的策略包括抛出异常、直接丢弃任务、丢弃队列中最老的任务等。
AbortPolicy
是默认的拒绝策略。当任务无法被接受时,它将抛出RejectedExecutionException
异常。CallerRunsPolicy
让提交任务的线程自己执行被拒绝的任务。这种策略可能会降低新任务的提交速度,但可以保证不会丢失任务。DiscardPolicy
直接丢弃被拒绝的任务,不提供任何反馈。DiscardOldestPolicy
丢弃队列中最老的任务,然后尝试重新提交被拒绝的任务。
在创建线程池时,可以通过ThreadPoolExecutor
类的构造函数或Executors
工厂类提供的方法来指定这些参数。以下是一个简单的例子,演示了如何创建一个具有核心线程数为2,最大线程数为5,队列容量为10的线程池:
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
int corePoolSize = 2;
int maximumPoolSize = 5;
long keepAliveTime = 60L;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10);
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ExecutorService executorService = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
handler
);
// 提交任务给线程池
for (int i = 0; i < 8; i++) {
executorService.submit(new Task(i));
}
// 关闭线程池
executorService.shutdown();
}
static class Task implements Runnable {
private final int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
}
}
}
在这个例子中,通过ThreadPoolExecutor
的构造函数指定了核心线程数、最大线程数、线程空闲时间、任务队列等参数。当线程池中的线程数达到核心线程数时,新的任务会被放入队列,当队列满了且线程数未达到最大线程数时,会创建新的线程执行任务。当线程空闲时间超过60秒时,空闲的线程将被终止。任务队列采用了LinkedBlockingQueue
。在任务被拒绝时,采用了AbortPolicy
拒绝策略,该策略会抛出RejectedExecutionException
。
工厂
在Java中,有几个常见的线程池工厂类,它们是Executors
工厂类提供的方法,用于创建不同类型的线程池。以下是其中一些常见的线程池工厂类:
-
Executors.newFixedThreadPool(int nThreads)
:- 创建一个固定大小的线程池,其中包含指定数量的线程。当线程池中的线程数达到核心线程数时,新的任务会被放入任务队列。
-
Executors.newCachedThreadPool()
:- 创建一个可根据需要自动调整大小的线程池。当线程池中的线程数小于核心线程数时,会创建新的线程执行任务,当线程数超过核心线程数且线程空闲时间超过60秒时,空闲的线程将被终止。
-
Executors.newSingleThreadExecutor()
:- 创建一个单线程的线程池,确保所有任务按顺序执行。当线程池中的线程数达到核心线程数时,新的任务会被放入任务队列。
-
Executors.newScheduledThreadPool(int corePoolSize)
:- 创建一个固定大小的线程池,可以在指定的延迟后执行任务,或定期执行任务。适用于需要定时执行任务的场景。
这些工厂方法返回的线程池实例都实现了ExecutorService
接口,具有提交任务、执行控制等功能。需要根据具体的需求选择合适的线程池类型和配置参数。在实际应用中,也可以通过直接使用ThreadPoolExecutor
的构造函数来自定义线程池的配置。
Callable和Runnable接口
Callable
和 Runnable
是 Java 中用于表示多线程任务的两个接口,它们都用于创建可以在独立线程中执行的任务。它们之间的主要区别在于任务执行完成后是否能够返回结果以及是否能够抛出异常。
Runnable 接口:
-
定义:
Runnable
是 Java 中的一个函数式接口,用于表示一个可以在独立线程中执行的任务。它包含一个run()
方法,该方法定义了线程执行的主体。
-
方法:
void run()
:该方法定义了线程的执行主体,但不返回结果。
-
使用场景:
- 适用于那些不需要返回结果的、简单的线程任务。
-
示例:
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable task is running."); } }
Callable 接口:
-
定义:
Callable
是 Java 中的一个泛型接口,用于表示一个可以在独立线程中执行的任务。它包含一个call()
方法,该方法返回一个泛型类型的结果,并可以抛出异常。
-
方法:
V call()
:该方法定义了线程的执行主体,并返回一个泛型类型的结果。
-
使用场景:
- 适用于那些需要返回结果的、可能抛出异常的线程任务。
-
示例:
import java.util.concurrent.Callable; public class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "Callable task is running."; } }
使用方式:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws Exception {
// 使用 Runnable
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
// 使用 Callable
Callable<String> myCallable = new MyCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(myCallable);
String result = future.get();
System.out.println(result);
// 关闭线程池
executorService.shutdown();
}
}
总体来说,如果你的线程任务不需要返回结果或抛出异常,使用 Runnable
就足够了。但如果你需要获取线程任务的执行结果或处理可能抛出的异常,那么就应该使用 Callable
。在实际应用中,Callable
结合 Future
可以更灵活地处理线程任务的执行结果。
生命周期
线程的生命周期包括多个阶段,从创建到终止。Java中的线程生命周期可以分为以下状态:
-
新建状态(New):
- 当线程对象被创建时,它处于新建状态。此时线程并没有启动,仅是一个 Java 对象。
-
就绪状态(Runnable):
- 在新建状态的线程调用
start()
方法后,线程进入就绪状态。在就绪状态中,线程已经准备好运行,一旦得到CPU时间片,就可以开始执行。
- 在新建状态的线程调用
-
运行状态(Running):
- 线程调度器选中了就绪状态的线程,使其进入运行状态。线程正在执行其任务。
-
阻塞状态(Blocked):
- 在运行状态中,可能由于某些原因需要暂时放弃 CPU 时间片,此时线程进入阻塞状态。例如,线程调用了
sleep()
方法、等待 I/O 操作完成、等待获取锁等情况。
- 在运行状态中,可能由于某些原因需要暂时放弃 CPU 时间片,此时线程进入阻塞状态。例如,线程调用了
-
等待状态(Waiting):
- 线程进入等待状态是因为调用了
Object.wait()
、Thread.join()
或者LockSupport.park()
等方法。线程会一直等待某个条件满足。
- 线程进入等待状态是因为调用了
-
超时等待状态(Timed Waiting):
- 线程进入超时等待状态是因为调用了具有超时参数的
sleep()
、Object.wait(long timeout)
、Thread.join(long millis)
或者LockSupport.parkNanos()
等方法。
- 线程进入超时等待状态是因为调用了具有超时参数的
-
终止状态(Terminated):
- 线程执行完任务或者发生了未捕获的异常,线程将进入终止状态。在终止状态后,线程不可再次启动。
线程的状态转换是动态的,线程在运行过程中可能多次在不同状态之间切换。例如,一个线程可以从就绪状态进入运行状态,然后再由运行状态进入阻塞状态。
以下是一个简单的示例,演示线程的生命周期:
public class ThreadLifecycleExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("Thread is in Running state.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 新建状态
System.out.println("Thread is in New state.");
// 启动线程,进入就绪状态
thread.start();
Thread.sleep(100);
// 就绪状态
// 进入运行状态后,等待2秒
Thread.sleep(2000);
// 阻塞状态(sleep)
// 进入运行状态后,等待1秒
Thread.sleep(1000);
// 超时等待状态(sleep)
// 进入等待状态
Object lock = new Object();
synchronized (lock) {
lock.wait();
}
// 进入终止状态
}
}
在这个例子中,线程经历了新建状态、就绪状态、运行状态、阻塞状态、超时等待状态、等待状态和终止状态。实际应用中,线程的状态转换会受到多线程的调度和竞争等因素的影响。
关键字volatile
volatile
是Java中的一个关键字,用于修饰变量。当一个变量被声明为 volatile
时,它具有以下特性:
-
可见性(Visibility):
- 当一个线程修改
volatile
变量的值时,这个新值对其他线程是立即可见的。这是因为每次访问volatile
变量都会从主内存中读取最新的值,而不是使用线程本地的缓存。
- 当一个线程修改
-
禁止指令重排序(Atomicity):
volatile
关键字禁止指令重排序,确保变量的读、写操作是按照代码的顺序执行的。这意味着在一个线程写入volatile
变量之前的所有操作都完成后,其他线程能立即看到最新的值。
-
不保证原子性(Atomicity):
volatile
保证了变量的可见性和禁止指令重排序,但它并不保证对变量操作的原子性。如果一个变量的操作需要保证原子性,应该使用synchronized
或者java.util.concurrent
包提供的原子类。
volatile
通常用于确保多个线程能够正确处理共享变量的值。然而,它并不适用于所有情况,特别是在复合操作(例如递增操作)时,可能需要使用其他的同步机制。
以下是一个简单的示例,演示了 volatile
的用法:
public class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
// 线程1:修改 flag 的值
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Flag set to true.");
}).start();
// 线程2:检查 flag 的值
new Thread(() -> {
while (!flag) {
// 等待 flag 的值变为 true
}
System.out.println("Flag is now true.");
}).start();
}
}
在这个例子中,线程1修改了 flag
的值,而线程2通过不断检查 flag
的值来等待它的变化。由于 flag
是声明为 volatile
,线程2能够立即看到线程1修改的最新值,而不需要其他的同步机制。
原子性
在Java中,原子性是指一个操作是不可中断的。在多线程并发的环境中,如果一个操作是原子的,那么它要么完全执行,要么完全不执行,不存在被中断的情况。Java提供了一些机制来实现原子性操作,主要包括以下几种:
-
synchronized
关键字:- 使用
synchronized
关键字可以确保一段代码块或方法在同一时刻只能被一个线程执行,从而保证操作的原子性。但是,使用synchronized
会引入一些性能开销,并且需要注意避免死锁等问题。
public synchronized void atomicMethod() { // 原子性操作 }
- 使用
-
java.util.concurrent.atomic
包:- Java提供了
java.util.concurrent.atomic
包,其中包含了一些原子性的操作类,如AtomicInteger
、AtomicLong
、AtomicBoolean
等。这些类使用了底层的CAS(Compare and Swap)操作来保证操作的原子性。
import java.util.concurrent.atomic.AtomicInteger; AtomicInteger atomicInteger = new AtomicInteger(0); public void atomicMethod() { atomicInteger.incrementAndGet(); // 原子性操作 }
- Java提供了
-
volatile
关键字:- 在前面提到的
volatile
关键字中,虽然它主要用于保证变量的可见性和禁止指令重排序,但在某些情况下,它也可以提供一定程度的原子性。例如,对于单次的读取和写入操作,volatile
可以确保原子性。
private static volatile int counter = 0; public void atomicMethod() { counter++; // 单次读取和写入操作,可能具有一定的原子性 }
- 在前面提到的
需要注意的是,虽然上述机制可以提供一定程度的原子性,但并不是所有的操作都能通过它们来实现原子性。对于复合操作,例如递增操作,仍然需要使用其他更强大的原子性工具,比如 AtomicInteger
。此外,确保原子性操作还需要考虑线程安全性和并发性能等因素。
在Java中,常用的原子性操作主要涉及到对基本数据类型的一些操作,以及使用 java.util.concurrent.atomic
包提供的原子类。以下是一些常用的原子性操作:
原子性操作基本数据类型:
-
原子性递增和递减:
AtomicInteger atomicInt = new AtomicInteger(0); atomicInt.incrementAndGet(); // 原子性递增 atomicInt.decrementAndGet(); // 原子性递减
-
原子性加法和减法:
AtomicInteger atomicInt = new AtomicInteger(0); atomicInt.addAndGet(5); // 原子性加法 atomicInt.subtractAndGet(3); // 原子性减法
原子性更新:
```java
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.updateAndGet(x -> x * 2); // 使用 Lambda 表达式原子性更新
```
-
java.util.concurrent.atomic
包提供的原子类:-
AtomicInteger
: 用于对整数进行原子操作。AtomicInteger atomicInt = new AtomicInteger(0);
-
AtomicLong
: 用于对长整型进行原子操作。AtomicLong atomicLong = new AtomicLong(0L);
-
AtomicBoolean
: 用于对布尔值进行原子操作。AtomicBoolean atomicBoolean = new AtomicBoolean(true);
-
AtomicReference
: 用于对引用类型进行原子操作。AtomicReference<String> atomicReference = new AtomicReference<>("initialValue");
-
AtomicStampedReference
: 用于对引用类型和整数标记进行原子操作,解决了 ABA 问题。AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("initialValue", 0);
-
AtomicIntegerArray
、AtomicLongArray
、AtomicReferenceArray
: 用于对数组元素进行原子操作。AtomicIntegerArray atomicIntArray = new AtomicIntegerArray(new int[]{1, 2, 3});
-
AtomicLongFieldUpdater
、AtomicIntegerFieldUpdater
、AtomicReferenceFieldUpdater
: 用于对类的字段进行原子操作。AtomicIntegerFieldUpdater<MyClass> updater = AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "fieldName");
-
这些原子类提供了一些基本的原子性操作,可以用于在多线程环境中安全地进行并发操作。需要根据实际需求选择适当的原子类。
并发工具类
Java提供了一些支持并发的集合,它们是在多线程环境中安全使用的集合类。以下是一些常用的支持并发的集合类:
-
java.util.concurrent
包中的并发集合:-
ConcurrentHashMap
:- 线程安全的哈希表实现,适用于多线程并发读写的场景。
-
ConcurrentSkipListMap
:- 线程安全的跳表实现的有序映射。
-
ConcurrentSkipListSet
:- 线程安全的跳表实现的有序集合。
-
CopyOnWriteArrayList
:- 基于写时复制的并发列表,适用于读多写少的场景。
-
CopyOnWriteArraySet
:- 基于写时复制的并发集合,适用于读多写少的场景。
-
BlockingQueue
接口的实现类(例如LinkedBlockingQueue
、ArrayBlockingQueue
):- 用于在多线程环境中安全地进行生产者-消费者模型的数据交换。
-
ConcurrentLinkedQueue
、ConcurrentLinkedDeque
:- 线程安全的非阻塞队列和双端队列。
-
LinkedBlockingDeque
:- 由链接节点支持的、可选有界的阻塞双端队列。
-
ConcurrentLinkedHashMap
(不是标准库中的类):- 一个支持并发访问的 LRU(Least Recently Used)缓存。
-
-
Java 9 引入的
java.util.concurrent
包中的新集合:-
CopyOnWriteArrayList
、CopyOnWriteArraySet
的升级版本:- 在Java 9中,这两个类提供了更多的方法,支持条件删除和替换元素。
-
ConcurrentHashMap
的升级版本:- 引入了更多的方法,如
forEach
、compute
、merge
等。
- 引入了更多的方法,如
-
这些并发集合类提供了线程安全的操作,并能够在多线程环境中高效地进行读写操作。选择合适的并发集合类取决于具体的应用需求和性能要求。