Java多线程
继承Thread
类
优点
- 简单直观:实现简单,直接继承
Thread
类并重写run()
方法即可。 - 封装性强:任务和线程的控制逻辑封装在一起,代码集中。
缺点
- 单继承局限:由于Java不支持多重继承,继承
Thread
类会限制类的继承能力,不能继承其他类。 - 线程和任务紧耦合:任务逻辑和线程控制耦合在一起,不利于重用和灵活性。
- 无返回值和异常处理:
run()
方法不能返回结果,也不能抛出检查异常。
// 自定义线程类,继承Thread类
class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
this.threadName = name;
}
// 重写run()方法,定义线程执行的任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " is running: " + i);
try {
// 让线程睡眠一段时间,模拟处理某些任务
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(threadName + " was interrupted.");
}
}
System.out.println(threadName + " has finished.");
}
}
public class ThreadExample {
public static void main(String[] args) {
// 创建线程对象
MyThread thread1 = new MyThread("Thread 1");
MyThread thread2 = new MyThread("Thread 2");
// 启动线程
thread1.start();
thread2.start();
}
}
- 自定义线程类:
MyThread
类继承自Thread
类。- 在构造函数中接收线程的名称,并将其保存在成员变量
threadName
中。 - 重写
run()
方法,该方法定义了线程执行的任务,即输出线程名称和计数器值,每次循环后让线程睡眠1秒。
- 主程序:
- 在
main
方法中,创建了两个MyThread
对象thread1
和thread2
,分别表示两个线程。 - 调用
start()
方法启动线程,start()
方法内部会调用run()
方法,开始执行线程任务。
- 在
运行结果
运行上述代码时,输出会交替显示两个线程的执行情况,如下所示:
Thread 1 is running: 0
Thread 2 is running: 0
Thread 1 is running: 1
Thread 2 is running: 1
Thread 1 is running: 2
Thread 2 is running: 2
Thread 1 is running: 3
Thread 2 is running: 3
Thread 1 is running: 4
Thread 2 is running: 4
Thread 1 has finished.
Thread 2 has finished.
注意事项
- 线程启动:调用
start()
方法启动线程,不能直接调用run()
方法,否则只是普通的方法调用,没有启动新线程。 - 线程安全:多个线程访问共享资源时,需要考虑线程安全问题,可以使用同步机制(如
synchronized
关键字)解决。 - 线程中断:可以使用
interrupt()
方法中断线程,在run()
方法中需要检查线程的中断状态。 - 线程生命周期:线程从创建到运行到终止,经历了多个状态,需要理解线程的生命周期和状态转换。
通过继承Thread
类实现多线程是一种简单直接的方法,适用于简单的线程任务。在实际开发中,更推荐使用实现Runnable
接口的方式,因为Java不支持多重继承,实现接口的方式更加灵活。
实现Runnable
接口
优点
- 多继承灵活性:实现接口方式允许类继承其他类,提供更大的设计灵活性。
- 任务和线程分离:任务逻辑和线程控制分离,便于任务的复用和灵活性。
- 代码复用:同一个
Runnable
实例可以被多个线程执行,代码更易于重用。
缺点
- 无返回值和异常处理:
run()
方法不能返回结果,也不能抛出检查异常。 - 需手动创建线程:需要手动创建
Thread
对象并传入Runnable
实例。
在Java中,除了通过继承Thread
类来实现多线程,还可以通过实现Runnable
接口来实现多线程。这种方式更加灵活,因为Java类可以实现多个接口,但只能继承一个类。实现Runnable
接口的方式推荐用于实际开发中。下面是详细的代码示例,展示了如何通过实现Runnable
接口来实现多线程。
通过Runnable
接口实现多线程
下面的示例代码创建了一个实现Runnable
接口的自定义类MyRunnable
,并在run()
方法中定义了线程执行的任务。主程序创建多个线程对象并启动它们。
// 自定义类,实现Runnable接口
class MyRunnable implements Runnable {
private String threadName;
public MyRunnable(String name) {
this.threadName = name;
}
// 实现run()方法,定义线程执行的任务
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " is running: " + i);
try {
// 让线程睡眠一段时间,模拟处理某些任务
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(threadName + " was interrupted.");
}
}
System.out.println(threadName + " has finished.");
}
}
public class RunnableExample {
public static void main(String[] args) {
// 创建Runnable对象
MyRunnable runnable1 = new MyRunnable("Thread 1");
MyRunnable runnable2 = new MyRunnable("Thread 2");
// 创建Thread对象并传入Runnable对象
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable2);
// 启动线程
thread1.start();
thread2.start();
}
}
代码解释
- 自定义类:
MyRunnable
类实现了Runnable
接口。- 在构造函数中接收线程的名称,并将其保存在成员变量
threadName
中。 - 实现
run()
方法,该方法定义了线程执行的任务,即输出线程名称和计数器值,每次循环后让线程睡眠1秒。
- 主程序:
- 在
main
方法中,创建了两个MyRunnable
对象runnable1
和runnable2
,分别表示两个线程任务。 - 创建两个
Thread
对象thread1
和thread2
,并将MyRunnable
对象作为参数传递给Thread
对象的构造函数。 - 调用
start()
方法启动线程,start()
方法内部会调用run()
方法,开始执行线程任务。
- 在
运行结果
运行上述代码时,输出会交替显示两个线程的执行情况,如下所示:
Thread 1 is running: 0
Thread 2 is running: 0
Thread 1 is running: 1
Thread 2 is running: 1
Thread 1 is running: 2
Thread 2 is running: 2
Thread 1 is running: 3
Thread 2 is running: 3
Thread 1 is running: 4
Thread 2 is running: 4
Thread 1 has finished.
Thread 2 has finished.
注意事项
- 线程启动:调用
start()
方法启动线程,不能直接调用run()
方法,否则只是普通的方法调用,没有启动新线程。 - 线程安全:多个线程访问共享资源时,需要考虑线程安全问题,可以使用同步机制(如
synchronized
关键字)解决。 - 线程中断:可以使用
interrupt()
方法中断线程,在run()
方法中需要检查线程的中断状态。 - 线程生命周期:线程从创建到运行到终止,经历了多个状态,需要理解线程的生命周期和状态转换。
通过实现Runnable
接口来实现多线程,是一种推荐的实践方法,特别是在需要多重继承或代码复用的场景下。这种方法更符合面向对象设计原则,使代码更加灵活和可维护
Callable
和Future
接口提供了一种更强大的方式来实现多线程任务。Callable
接口与Runnable
接口类似,但Callable
可以返回结果并且可以抛出异常。Future
接口则表示异步计算的结果。结合使用这两个接口,可以实现带返回值和异常处理的多线程任务。
通过Callable
和Future
接口实现多线程
优点
- 有返回值:
call()
方法可以返回结果,适用于需要返回计算结果的任务。 - 异常处理:
call()
方法可以抛出异常,适用于需要处理异常的任务。 - 线程池管理:结合
ExecutorService
可以更好地管理线程,提高性能和资源利用率。
缺点
- 复杂性增加:相比于前两种方式,
Callable
和Future
的实现和使用稍微复杂一些。 - 需手动管理线程池:需要手动创建和管理线程池,使用不当可能导致资源泄漏。
下面的示例代码展示了如何通过实现Callable
接口来定义一个带返回值的任务,并使用Future
接口来获取异步计算的结果。
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;
// 自定义类,实现Callable接口
class MyCallable implements Callable<Integer> {
private String taskName;
public MyCallable(String name) {
this.taskName = name;
}
// 实现call()方法,定义线程执行的任务,并返回结果
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + " is calculating sum: " + i);
sum += i;
try {
// 让线程睡眠一段时间,模拟处理某些任务
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(taskName + " was interrupted.");
}
}
System.out.println(taskName + " has finished calculating. Result: " + sum);
return sum;
}
}
public class CallableFutureExample {
public static void main(String[] args) {
// 创建ExecutorService线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
// 创建Callable对象
MyCallable callable1 = new MyCallable("Task 1");
MyCallable callable2 = new MyCallable("Task 2");
// 提交任务并获取Future对象
Future<Integer> future1 = executor.submit(callable1);
Future<Integer> future2 = executor.submit(callable2);
try {
// 获取任务结果
Integer result1 = future1.get();
Integer result2 = future2.get();
System.out.println("Result of Task 1: " + result1);
System.out.println("Result of Task 2: " + result2);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 关闭线程池
executor.shutdown();
}
}
}
代码解释
- 自定义类:
MyCallable
类实现了Callable<Integer>
接口。- 在构造函数中接收任务的名称,并将其保存在成员变量
taskName
中。 - 实现
call()
方法,该方法定义了线程执行的任务,即计算1到5的和,并返回计算结果。
- 主程序:
- 在
main
方法中,创建了一个ExecutorService
线程池,用于管理和执行线程。 - 创建了两个
MyCallable
对象callable1
和callable2
,分别表示两个线程任务。 - 使用
executor.submit()
方法提交任务,并获取Future<Integer>
对象。 - 调用
future.get()
方法获取任务的结果。该方法是阻塞的,会等待任务完成并返回结果。 - 最后,关闭线程池。
- 在
运行结果
运行上述代码时,输出会显示两个任务的执行情况和计算结果,如下所示:
python复制代码
Task 1 is calculating sum: 1
Task 2 is calculating sum: 1
Task 1 is calculating sum: 2
Task 2 is calculating sum: 2
Task 1 is calculating sum: 3
Task 2 is calculating sum: 3
Task 1 is calculating sum: 4
Task 2 is calculating sum: 4
Task 1 is calculating sum: 5
Task 2 is calculating sum: 5
Task 1 has finished calculating. Result: 15
Task 2 has finished calculating. Result: 15
Result of Task 1: 15
Result of Task 2: 15
注意事项
- 线程池管理:使用
ExecutorService
管理线程,可以更好地控制线程的生命周期,提高性能和资源利用率。 - 结果获取:使用
Future.get()
方法获取任务结果,该方法是阻塞的,会等待任务完成。可以使用isDone()
方法检查任务是否完成。 - 异常处理:
Callable
接口允许抛出异常,可以在call()
方法中处理异常。获取结果时,如果任务抛出异常,Future.get()
方法会抛出ExecutionException
。 - 线程池关闭:使用完线程池后,调用
shutdown()
方法关闭线程池,以释放资源。
通过实现Callable
接口和使用Future
接口,可以实现更加灵活和强大的多线程任务,适用于需要返回结果和异常处理的场景。
三种方式实现多线程对比
特性 | 继承Thread类 | 实现Runnable接口 | 实现Callable接口和使用Future |
---|---|---|---|
实现难度 | 简单 | 简单 | 较复杂 |
是否有返回值 | 否 | 否 | 是 |
是否可抛出异常 | 否 | 否 | 是 |
多继承支持 | 否 | 是 | 是 |
任务和线程耦合性 | 高 | 低 | 低 |
代码复用性 | 低 | 高 | 高 |
线程管理 | 手动管理 | 手动管理 | 使用线程池自动管理 |
根据实际需求选择合适的实现方式:
- 如果任务简单且不需要返回值或抛出异常,可以使用继承
Thread
类或实现Runnable
接口。 - 如果任务需要返回结果或抛出异常,并且希望更灵活地管理线程和任务,建议使用
Callable
接口和Future
。
多线程中常见成员方法
Thread
类是Java多线程编程的核心类,提供了许多常见的成员方法来控制线程的创建、启动、暂停、恢复、中断和状态检查。下面是一些常见的Thread
类成员方法及其简要说明:
1. 创建和启动线程
Thread(Runnable target)
:创建一个新的线程对象并将指定的Runnable
对象作为线程的任务。
void start()
启动线程,线程进入就绪状态并等待CPU调度。
- 注意事项:
- 只能调用一次:调用
start()
方法只能启动线程一次,再次调用会抛出IllegalThreadStateException
。 - 立即返回:
start()
方法会立即返回,并且新线程会异步执行run()
方法。 - 不要直接调用
run()
:直接调用run()
方法不会启动新线程,只是普通的方法调用。
- 只能调用一次:调用
2. 控制线程
void run()
:线程执行的任务代码,通常由用户重写。直接调用此方法不会启动新线程,只是普通方法调用。
注意事项
- 由JVM调用:通常不应该手动调用
run()
方法,它由JVM调用来执行线程的任务。 - 重写:如果继承
Thread
类,需要重写run()
方法。 static void sleep(long millis)
:让当前正在执行的线程休眠指定的毫秒数。
注意事项
- InterruptedException:调用
sleep()
方法时需要处理InterruptedException
,通常在try-catch
块中捕获。 - 不释放锁:
sleep()
方法不会释放持有的锁,因此不会影响同步块的执行。 void interrupt()
:中断线程,使被阻塞的线程抛出InterruptedException
或设置中断状态。
注意事项
- 设置中断状态:
interrupt()
方法会设置线程的中断状态,如果线程正在被阻塞(例如在调用sleep()
、wait()
、join()
等方法时),会抛出InterruptedException
。 - 及时检查中断状态:线程的任务代码应定期检查其中断状态(使用
Thread.interrupted()
或isInterrupted()
),并做出相应处理。 boolean isInterrupted()
:判断线程是否被中断。
注意事项
- 不清除中断状态:
isInterrupted()
方法仅返回线程的中断状态,不会清除该状态。 static boolean interrupted()
:判断当前线程是否被中断,并清除中断状态。
注意事项
- 清除中断状态:
interrupted()
方法不仅返回当前线程的中断状态,还会清除该状态。
public class InterruptibleTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 执行任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 保留中断状态
break; // 退出循环
}
}
}
}
3. 线程状态和信息
long getId()
:返回线程的唯一标识符。
注意事项
- 唯一标识:
getId()
方法返回线程的唯一标识符,该标识符在Java虚拟机中是唯一的。 String getName()
:返回线程的名称。
注意事项
- 获取线程名称:返回线程的名称,默认名称格式为
Thread-N
,其中N
是线程的编号。 void setName(String name)
:设置线程的名称。
注意事项
- 设置线程名称:可以在创建线程时或启动前设置线程名称,有助于调试和日志记录。
- 不能为null:名称不能为
null
,否则会抛出NullPointerException
。 Thread.State getState()
:返回线程的当前状态。
注意事项
- 获取线程状态:返回线程的当前状态(如
NEW
、RUNNABLE
、BLOCKED
、WAITING
等)。 - 仅供参考:线程状态可能在获取后立即变化,因此仅供参考,不应依赖于此状态做出关键决策。
boolean isAlive()
:判断线程是否处于活动状态。
注意事项
- 判断线程是否活跃:如果线程已经启动且尚未终止,则返回
true
,否则返回false
。
4. 线程优先级
int getPriority()
:获取线程的优先级。
注意事项
- 获取线程优先级:返回线程的优先级,范围从
Thread.MIN_PRIORITY
(1)到Thread.MAX_PRIORITY
(10),默认值为Thread.NORM_PRIORITY
(5)。 void setPriority(int newPriority)
:设置线程的优先级,优先级范围从Thread.MIN_PRIORITY
到Thread.MAX_PRIORITY
。
注意事项
- 设置线程优先级:可以在创建线程后、启动前设置优先级。
- 范围限制:优先级应在
Thread.MIN_PRIORITY
到Thread.MAX_PRIORITY
之间,否则会抛出IllegalArgumentException
。 - 平台依赖:线程优先级对线程调度的影响是平台依赖的,不保证一定会按照优先级调度。通俗的来说就是线程优先级高只是CPU优先执行的概率高,而不保证一定按照优先级执行。
5. 线程合并
void join()
:等待线程终止,调用线程会阻塞直到目标线程完成。
注意事项
- 等待线程终止:调用线程会等待目标线程终止,如果目标线程已终止,
join()
方法会立即返回。 - InterruptedException:调用
join()
方法时需要处理InterruptedException
。 void join(long millis)
:等待线程终止的时间最长为指定的毫秒数。
6. 守护线程
boolean isDaemon()
:判断线程是否为守护线程。
注意事项
- 判断是否为守护线程:返回线程是否为守护线程(后台线程)。
void setDaemon(boolean on)
:将线程标记为守护线程或用户线程,必须在启动线程之前调用。
注意事项
- 设置守护线程:必须在启动线程之前调用此方法,否则会抛出
IllegalThreadStateException
。 - 守护线程不会阻止JVM退出:当所有用户线程结束时,守护线程也会终止。
7. 线程组
ThreadGroup getThreadGroup()
:返回线程所属的线程组。
注意事项
- 获取线程组:返回线程所属的线程组,线程组用于批量管理线程。
线程的生命周期
+-----------+ +-----------+
| | start() | |
| New +--------------------->| Runnable |
| | | |
+-----------+ +-----------+
/ \\
/ \\
/ \\
run()/ \\blocked/sleep()/wait()
/ \\
/ \\
V V
+-----------+ +-----------+
| | | |
| Running +--->| Blocked |
| |<---+ |
+-----------+ +-----------+
| |
| notify()/ |
| resume() |
| |
V V
+-----------+ +-----------+
| | | |
| Waiting +--->| Terminated|
| |<---+ |
+-----------+ +-----------+
状态说明:
New(新建):线程对象被创建,但尚未启动。
Runnable(就绪):线程已经启动并等待CPU时间片的分配。此状态下线程可能在运行,也可能正在等待操作系统为其分配处理器。
Running(运行):线程获得了CPU时间片,正在执行代码。
Blocked(阻塞):线程在等待某个监视器锁,以便能够进入同步块或方法。
Waiting(等待):线程等待另一个线程显式地唤醒,通常通过调用Object.wait()方法进入此状态。线程在等待某一特定条件的满足,如调用Thread.join()、LockSupport.park()等。
Terminated(终止):线程已经完成执行,或者因异常退出了run方法,进入此状态。
状态转换说明:
线程从 New 状态调用 start() 方法后进入 Runnable 状态。
Runnable 状态的线程一旦获得CPU时间片,就进入 Running 状态。
在 Running 状态中,线程可能调用 sleep()、wait() 等方法进入 Blocked 或 Waiting 状态。
线程从 Blocked 状态被唤醒后回到 Runnable 状态。
线程从 Waiting 状态被显式唤醒(如调用 notify()、resume())后回到 Runnable 状态。
线程完成运行或因未捕获的异常退出 run() 方法进入 Terminated 状态。
线程安全
线程安全问题通常发生在多个线程同时访问和修改共享数据时。如果没有适当的同步机制,可能会导致数据不一致、死锁、竞态条件等问题。
线程安全问题示例
情景故事
假设有一个小镇,每天早上都会有多位居民去镇中心的公共自来水井取水。这口井由一个自动售水机管理,可以同时为多个人提供水。
- 正常情况:
- 早晨,几位居民同时来到自来水井,每人都需要装满一个水桶。他们看到自动售水机提供了快速、有效的服务,每个人都能及时地装满水桶,然后离开满载而归。
- 高峰时段:
- 有一天,天气特别炎热,更多的居民感到口渴,纷纷前来取水。由于需求激增,有时多人同时插队或者同时操作售水机。
- 自动售水机在高峰时段出现了问题:有时它会在某些居民还没有完全装满水桶之前停止供水,导致部分居民感到沮丧和不满,甚至有些人的水桶只装了一半水就被强行停止了供水。
- 竞争条件:
- 问题的根源在于,自动售水机没有有效地处理多个人同时操作的情况。当多个人同时试图向水桶注水时,售水机无法正确地区分和响应每个人的请求,导致了一些人的需求无法得到满足。
- 解决方案:
- 为了解决这个问题,镇政府决定升级售水机的程序和控制系统,使其能够正确地处理多个人同时操作的情况。他们引入了一个更智能、更有效率的系统,确保每个人都能及时、完整地取到所需的水量,不再出现中途停止供水的情况。
通过这个故事,我们可以看到,线程安全问题就像是管理自来水井的自动售水机遇到的问题一样。在多个人同时访问和操作共享资源(如水井或者数据)时,如果没有正确处理竞争条件和并发访问,就会出现意料之外的结果,导致数据不一致或者程序出现错误。解决这类问题的关键在于设计良好的并发控制策略和机制,确保资源的安全访问和正确操作。
以下是一个典型的线程安全问题示例,展示了多个线程并发修改共享变量 count
时可能出现的数据不一致问题:
public class UnsafeCounter {
// 计数器变量
private int count = 0;
// 增加计数的方法,不是线程安全的
public void increment() {
count++;
}
// 获取当前计数值
public int getCount() {
return count;
}
public static void main(String[] args) {
UnsafeCounter counter = new UnsafeCounter();
// 定义一个任务,每个任务将计数器增加1000次
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
// 创建两个线程来执行任务
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
// 启动线程
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数值
System.out.println("Final count: " + counter.getCount());
}
}
问题描述
上述代码创建了两个线程,每个线程都对共享变量 count
进行了 1000 次递增操作。理论上,最终输出的 count
值应该是 2000。但由于 increment
方法不是线程安全的,最终输出的 count
值可能会小于 2000。这是因为 count++
操作实际上分为三个步骤:读取 count
值、增加 1
、写回 count
。在多个线程并发执行这三步操作时,可能会导致读到的 count
值不一致,从而出现数据错误。
解决线程安全问题
可以通过多种方式来解决线程安全问题,最常见的是使用同步机制(如 synchronized
关键字、显式锁等)来确保同一时间只有一个线程可以访问和修改共享数据。
使用 synchronized
关键字解决示例线程安全问题
synchronized
是 Java 中用于实现线程同步的关键字,它可以应用于方法或代码块,用来确保在同一时刻最多只有一个线程执行被 synchronized
修饰的代码片段。下面是关于 synchronized
关键字的使用细节和关键信息:
1. 使用方式
a. 同步方法
java复制代码
public synchronized void someMethod() {
// 同步代码块
// 在同一时刻最多只有一个线程可以执行到这里
}
- 在方法声明中使用
synchronized
关键字,称为同步方法。 - 当一个线程进入同步方法时,会尝试获取对象的锁,如果获取成功,则执行方法体;如果获取失败(锁被其他线程持有),则线程被阻塞,直到获取到锁为止。
b. 同步代码块
java复制代码
public void someMethod() {
synchronized (this) {
// 同步代码块
// 在同一时刻最多只有一个线程可以执行到这里
}
}
- 使用
synchronized
关键字加上一个对象(通常是当前对象this
或者某个共享的对象)作为锁,称为同步代码块。 - 只有获取了锁的线程可以执行同步代码块中的代码,其他线程必须等待直到锁被释放。
2. 对象监视器
- 在使用
synchronized
同步方法或代码块时,需要指定一个对象作为锁,这个对象称为对象监视器(Monitor)。 - 对象监视器是在 Java 对象头中的一部分数据,每个 Java 对象都可以用作一个锁。
3. 锁的粒度
- 同步方法的锁粒度通常是整个方法体,而同步代码块可以灵活地指定锁的范围,可以减小锁的粒度,提高并发性能。
- 需要根据具体的业务需求和性能考量选择合适的锁粒度。
4. 内置锁(Intrinsic Lock)
- 每个 Java 对象都可以作为一个内置锁(Intrinsic Lock),用于同步方法或同步代码块。
- 内置锁是一种互斥锁,同一时刻只能有一个线程获取到对象的内置锁,其他线程必须等待释放锁后才能继续执行。
5. 锁的释放
- 当线程执行
synchronized
方法或代码块结束时,会释放持有的锁。 - 异常情况下,线程也会释放持有的锁,确保锁的释放不会导致死锁。
6. 对比与优劣
- 优点:
- 简单易用,可以有效避免多线程并发访问共享资源时的竞态条件问题。
- 内置的锁提供了互斥性和可见性,能够确保数据的一致性。
- 缺点:
- 锁的粒度大时可能导致性能问题,因为同一时刻只能有一个线程执行同步代码,其他线程可能会被阻塞。
- 使用不当容易引发死锁等并发问题,需要注意编程时的锁顺序和持有时间。
7. 适用场景
synchronized
关键字适用于简单的并发控制需求,例如对共享资源的读写操作进行同步。- 当需要更细粒度的并发控制或更复杂的并发模型时,可以考虑使用
java.util.concurrent
包中提供的并发工具类,如Lock
接口的实现类、Atomic
类等。 synchronized
关键字:synchronized
关键字用于确保同一时刻只有一个线程可以执行被修饰的方法或代码块,从而避免多个线程同时访问共享资源导致的数据不一致问题。
- 同步方法:
increment
方法被synchronized
关键字修饰,确保对count
的操作是原子性的,即同一时刻只有一个线程可以执行此方法。
将 increment
方法声明为同步方法,确保同一时间只有一个线程可以执行该方法:
public class SafeCounter {
private int count = 0;
// 增加计数的方法,使用synchronized关键字确保线程安全
public synchronized void increment() {
count++;
}
// 获取当前计数值的方法
public int getCount() {
return count;
}
public static void main(String[] args) {
SafeCounter counter = new SafeCounter();
// 创建一个任务,每个任务将计数器增加1000次
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
// 创建两个线程来执行任务
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
// 启动线程
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数值
System.out.println("Final count: " + counter.getCount());
}
}
在上述代码中,通过将 increment
方法声明为 synchronized
,确保了在执行 increment
方法时,只有一个线程可以访问 count
,从而避免了数据不一致的问题。
其他同步机制
除了 synchronized
关键字,还可以使用显式锁(如 ReentrantLock
)、原子变量(如 AtomicInteger
)等其他同步机制来解决线程安全问题。
Lock
接口及其实现类(如 ReentrantLock
)是 Java 提供的另一种用于实现线程同步的机制。相较于 synchronized
关键字,Lock
提供了更加灵活、功能更丰富的锁定机制。下面是关于 Lock
锁的使用细节和关键信息:
1. Lock
接口
Lock
接口是 Java 提供的一种更高级的线程同步机制,允许更灵活的加锁和释放锁的操作。
2. ReentrantLock
类
ReentrantLock
是Lock
接口的一个实现类,提供了与synchronized
关键字类似的功能,但更为灵活和可控。
3. 使用方式
a. 锁定和解锁
- 使用
lock()
方法获取锁,unlock()
方法释放锁,通常结合try-finally
语句确保锁的释放:
java复制代码
Lock lock = new ReentrantLock();
lock.lock();
try {
// 线程安全操作
} finally {
lock.unlock();
}
lock.lock()
方法获取锁时,如果锁已经被其他线程持有,则当前线程会被阻塞,直到获取到锁。lock.unlock()
方法释放锁,必须在try
块的末尾使用,确保锁的释放,避免死锁情况。
b. 尝试获取锁
- 使用
tryLock()
方法尝试获取锁,如果锁可用立即返回true
,否则返回false
,不会阻塞当前线程:
java复制代码
if (lock.tryLock()) {
try {
// 线程安全操作
} finally {
lock.unlock();
}
} else {
// 锁被其他线程持有,处理逻辑
}
c. 可中断的获取锁
lockInterruptibly()
方法可以实现可中断地获取锁,如果当前线程被中断,则抛出InterruptedException
:
try {
lock.lockInterruptibly();
// 线程安全操作
} catch (InterruptedException e) {
// 处理线程被中断的情况
} finally {
lock.unlock();
}
4. 锁的特性
- 可重入性:与
synchronized
关键字一样,ReentrantLock
支持同一个线程多次获取同一把锁而不会死锁。 - 公平性:可以通过构造函数创建公平锁,即按照请求锁的顺序依次获取锁,但会影响性能。
Lock fairLock = new ReentrantLock(true); // 创建一个公平锁
- 条件变量:
ReentrantLock
可以通过Condition
对象实现线程间的等待和通知,比传统的wait()
和notify()
更加灵活。
Condition condition = lock.newCondition();
try {
condition.await(); // 等待条件满足
condition.signal(); // 唤醒一个等待的线程
} finally {
lock.unlock();
}
5. 适用场景
ReentrantLock
适用于需要更灵活控制锁定和解锁过程的情况,如需要尝试获取锁、可中断获取锁、公平性要求、锁定期间条件等待等场景。- 当需要更细粒度的锁定控制、更复杂的线程同步机制时,使用
ReentrantLock
是一个比较好的选择。
6. 性能比较
- 与
synchronized
相比,ReentrantLock
在低竞争情况下性能可能稍差,但在高并发情况下可能更有优势,因为可以避免线程的长时间阻塞等待。
使用 ReentrantLock
解决示例线程安全问题
ReentrantLock
是一个显式锁,实现了Lock
接口,比使用synchronized
关键字更灵活。提供了显式的加锁和解锁操作,可以在复杂的多线程环境中提供更好的控制。- 锁的获取和释放:
- 在
increment
方法中,通过lock.lock()
获取锁,确保只有一个线程可以进入临界区。 - 在
try
块中执行临界区代码,确保对count
的操作是线程安全的。 - 在
finally
块中调用lock.unlock()
释放锁,确保锁一定会被释放,即使在操作中发生异常
- 在
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
// 计数器变量
private int count = 0;
// ReentrantLock 对象,用于确保线程安全
private final ReentrantLock lock = new ReentrantLock();
// 使用 ReentrantLock 确保线程安全的计数器增加方法
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
// 获取当前计数值
public int getCount() {
return count;
}
public static void main(String[] args) {
LockCounter counter = new LockCounter();
// 定义一个任务,每个任务将计数器增加1000次
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
// 创建两个线程来执行任务
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
// 启动线程
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数值
System.out.println("Final count: " + counter.getCount());
}
}
使用 AtomicInteger
解决示例线程安全问题
AtomicInteger
是 Java 中 java.util.concurrent.atomic
包下提供的一个原子类,用于对整数类型的变量进行原子操作。它提供了一种线程安全的方式来进行加减操作,避免了使用 synchronized
或 ReentrantLock
等同步机制的开销和复杂性。以下是关于 AtomicInteger
的使用细节和关键信息:
1. 原子性操作
AtomicInteger
提供了原子性的整数操作,如增加、减少和获取当前值,这些操作在多线程环境下是线程安全的。
2. 常见方法
a. 增加和减少操作
void incrementAndGet()
:原子地将当前值加一,并返回更新后的值。void decrementAndGet()
:原子地将当前值减一,并返回更新后的值。int getAndIncrement()
:原子地将当前值返回后再加一。int getAndDecrement()
:原子地将当前值返回后再减一。
b. 获取当前值
int get()
:获取当前的整数值,这是一个原子操作。
c. CAS 操作
boolean compareAndSet(int expect, int update)
:如果当前值等于expect
,则以原子方式将该值设置为update
。返回true
表示成功设置,返回false
表示设置失败。
3. 线程安全性
AtomicInteger
使用了底层的 CPU 指令(比如 CAS 指令)来保证操作的原子性,因此在多线程环境下是线程安全的。
4. 适用场景
AtomicInteger
适用于需要高效并且线程安全地对整数进行计数或状态标记的情况,例如在并发编程中统计访问次数、计数器等。
5. 性能比较
- 相较于使用
synchronized
或ReentrantLock
的方式实现线程安全,AtomicInteger
的性能通常更高,因为它利用了硬件级别的原子性支持,避免了显式锁带来的性能开销和竞争。 - 线程安全:
AtomicInteger
提供了一组原子操作,确保在多线程环境下对计数器的操作是线程安全的。 - 简洁性:使用
AtomicInteger
可以避免显式的同步代码,使代码更简洁。 - 高效性:
AtomicInteger
使用低级别的原子操作,比传统的同步机制更高效。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.getAndIncrement();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) {
AtomicCounter counter = new AtomicCounter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
AtomicInteger
是处理并发编程中整数计数或状态标记的常用工具类,提供了高效的原子操作方法,避免了使用传统的同步机制时可能存在的性能问题。它适用于需要高性能和线程安全的场景,对于简单的计数和状态更新任务非常方便和有效。
这些同步机制都可以有效地解决线程安全问题,选择哪一种取决于具体的应用场景和性能需求。
线程唤醒机制
线程死锁
定义:
线程死锁是指两个或多个线程在执行过程中,因争夺资源而造成的互相等待的现象,导致所有线程都无法继续执行,称为死锁。通俗的来说就是锁的嵌套。
示例代码:
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 and lock 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 1 and lock 2...");
}
}
});
thread1.start();
thread2.start();
}
}
等待唤醒
在一个小农场里,有一位农夫和几头奶牛。农夫负责每天收集奶牛产的牛奶,然后把它存放在一个桶里。
生产者等待:
- 奶牛生产牛奶:
- 每头奶牛每天都会生产一定量的牛奶。
- 农夫每天都会来到奶牛身边,等待奶牛产奶。
- 奶牛生产牛奶时,会把牛奶放在自己身边的一个小桶里。
- 农夫的任务:
- 农夫负责将奶牛产的牛奶收集到主桶中。
- 如果某一天奶牛产的牛奶超过了主桶的容量,农夫不能立即将多余的牛奶装进去,因为主桶已经满了。
- 在这种情况下,农夫会对奶牛说:“奶牛,主桶已经满了,请等一等,我需要把一些牛奶卖出去,然后再装入主桶。”
- 等待和唤醒:
- 当主桶满了,农夫会通知奶牛:“奶牛们,请等一等,我现在要去卖一些牛奶,然后再回来继续收集。”
- 奶牛听到农夫的通知后,会继续生产牛奶,但将多余的牛奶放在自己的小桶里,直到农夫来把它们收集到主桶中。
消费者等待:
- 卖牛奶:
- 农夫每次都会把主桶里的牛奶运到市场去卖。
- 卖完牛奶后,农夫会返回农场,准备继续收集奶牛产的新牛奶。
- 回归农场:
- 当农夫卖完牛奶回到农场时,他会看到奶牛们都在等待,每头奶牛都希望农夫能收集它们产的牛奶。
- 唤醒奶牛:
- 农夫回到农场后,他会对奶牛们说:“奶牛们,我回来了,现在可以继续把你们产的牛奶收集到主桶中了!”
- 奶牛们听到农夫的声音后,会开始把之前产的牛奶交给农夫放进主桶里。
解释:
- 生产者等待:奶牛是生产者,负责生产牛奶并等待农夫来收集。当主桶满了时,奶牛不能再继续生产,并把多余的牛奶放在自己的小桶里,等待农夫的通知后再继续生产。
- 消费者等待:农夫是消费者,负责收集奶牛产的牛奶并卖到市场。如果主桶的牛奶被卖完了,农夫会回到农场通知奶牛继续生产。
import java.util.LinkedList;
import java.util.Queue;
public class FarmerCowExample {
private static final Queue<Integer> buffer = new LinkedList<>(); // 共享缓冲区
private static final int CAPACITY = 5; // 缓冲区容量
public static void main(String[] args) {
// 生产者线程(农夫)
Thread farmer = new Thread(() -> {
while (true) {
synchronized (buffer) {
while (buffer.size() >= CAPACITY) { // 如果缓冲区满了农夫进入等待状态
try {
System.out.println("Buffer is full. Farmer is waiting...");
buffer.wait(); // 农夫等待消费者消费牛奶
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int milk = (int) (Math.random() * 10) + 1; // 生产随机数量的牛奶
System.out.println("Farmer: Milking " + milk + " liters of milk");
buffer.offer(milk); // 将牛奶放入缓冲区
buffer.notify(); // 唤醒等待中的消费者线程
}
}
});
// 消费者线程(奶牛)
Thread cow = new Thread(() -> {
while (true) {
synchronized (buffer) {
while (buffer.isEmpty()) { // 如果缓冲区为空奶牛进入等待状态
try {
System.out.println("Buffer is empty. Cow is waiting...");
buffer.wait(); // 奶牛等待生产者生产牛奶
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int milk = buffer.poll(); // 从缓冲区取出牛奶
System.out.println("Cow: Produced " + milk + " liters of milk");
buffer.notify(); // 唤醒等待中的生产者线程
}
}
});
farmer.start(); // 启动生产者(农夫)线程
cow.start(); // 启动消费者(奶牛)线程
// 等待一段时间后结束程序(实际应用中需要更合适的停止机制)
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断线程以结束程序(实际应用中需要更合适的停止机制)
farmer.interrupt();
cow.interrupt();
}
}
阻塞对列实现等待唤醒机制
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerExample {
private static final int CAPACITY = 5; // 缓冲区容量
private static final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(CAPACITY); // 阻塞队列
public static void main(String[] args) {
// 生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) { // 生产10个物品
System.out.println("Producer: Producing " + i);
queue.put(i); // 放入队列,如果队列满了,则阻塞
Thread.sleep(500); // 模拟生产过程
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
while (true) {
Integer item = queue.take(); // 取出队列中的物品,如果队列空了,则阻塞
System.out.println("Consumer: Consuming " + item);
Thread.sleep(1000); // 模拟消费过程
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start(); // 启动生产者线程
consumer.start(); // 启动消费者线程
// 主线程等待一段时间后终止生产者和消费者线程
try {
Thread.sleep(10000); // 运行10秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断生产者和消费者线程以结束程序(实际应用中需要更合适的停止机制)
producer.interrupt();
consumer.interrupt();
}
}
线程池和自定义线程池详细介绍
1. 线程池概述
线程池是一种管理和重用线程的机制,它可以有效地管理大量的线程,提高系统的性能和资源利用率。Java提供了 java.util.concurrent
包来支持线程池的实现,包括了多种类型的线程池实现,同时也支持自定义线程池的创建。
主要知识点:
- 线程池的好处:
- 降低线程创建和销毁的开销。
- 控制并发线程数量,避免系统因创建过多线程而导致资源不足或者性能下降。
- 提高响应速度,任务可以立即执行,而不需要等待线程创建。
- 提供线程的管理、监控机制,可以统一管理线程的生命周期、状态和执行统计等信息。
- 线程池的核心组件:
- 任务队列:用于存放提交的任务,可以是无界队列或者有界队列。
- 线程池管理器:用于创建、销毁和管理线程池中的线程。
- 工作线程:线程池中实际执行任务的线程。
- 线程池的类型:
ThreadPoolExecutor
:Java提供的灵活的线程池实现,可以配置核心线程数、最大线程数、工作队列等参数。Executors
工厂类:提供了一些静态方法用于创建不同类型的线程池,如固定大小线程池、单线程池、缓存线程池等。
2. 自定义线程池实现
自定义线程池可以根据具体需求进行定制化配置,例如调整线程池大小、选择合适的任务队列类型、设置拒绝策略等,下面是一个简单的自定义线程池示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 2;
// 最大线程数
int maximumPoolSize = 4;
// 空闲线程存活时间
long keepAliveTime = 5000; // 5秒
// 任务队列
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(10); // 容量为10的有界队列
// 创建自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
workQueue);
// 提交任务给线程池执行
for (int i = 1; i <= 10; i++) {
final int taskId = i;
threadPool.execute(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
threadPool.shutdown();
}
}
线程案例代码示例
案例1:买卖电影票
需求
有1000张电影票,通过两个窗口进行购买,每次领取票据时间为3000毫秒。使用多线程模拟实现。
public class MovieTicketSystem {
public static void main(String[] args) {
// 创建一个票务柜台对象,初始票数为1000
TicketCounter ticketCounter = new TicketCounter(1000);
// 创建两个售票窗口(线程),分别命名为 "窗口1" 和 "窗口2"
Thread window1 = new Thread(new TicketWindow(ticketCounter), "窗口1");
Thread window2 = new Thread(new TicketWindow(ticketCounter), "窗口2");
// 启动售票窗口线程
window1.start();
window2.start();
}
}
// 票务柜台类,负责处理售票逻辑
class TicketCounter {
private int ticketsAvailable; // 可用票数
// 构造方法,初始化可用票数
public TicketCounter(int ticketsAvailable) {
this.ticketsAvailable = ticketsAvailable;
}
// 售票方法,使用synchronized关键字确保线程安全
public synchronized void sellTicket() {
if (ticketsAvailable > 0) {
try {
Thread.sleep(3000); // 模拟领票时间,延迟3000毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前线程售出的票号
System.out.println(Thread.currentThread().getName() + " 售出第 " + ticketsAvailable + " 张票");
ticketsAvailable--; // 票数减少
} else {
// 当票已售罄时,打印提示信息
System.out.println(Thread.currentThread().getName() + " 票已售罄");
}
}
// 检查是否还有剩余票数的方法
public synchronized boolean hasTickets() {
return ticketsAvailable > 0; // 返回是否还有票
}
}
// 售票窗口类,实现Runnable接口
class TicketWindow implements Runnable {
private TicketCounter ticketCounter; // 票务柜台对象
// 构造方法,初始化票务柜台对象
public TicketWindow(TicketCounter ticketCounter) {
this.ticketCounter = ticketCounter;
}
// 线程运行方法
@Override
public void run() {
// 当还有票时,不断进行售票操作
while (ticketCounter.hasTickets()) {
ticketCounter.sellTicket();
}
}
}
案例2:有序输出1-100之间的奇数
需求
同时开启两个线程,共同获取1-100之间的所有数据,要求输出数据有序,且输出的是奇数。
public class OrderedOddNumberPrinter {
// 使用volatile关键字确保currentNumber在多个线程之间的可见性
private static volatile int currentNumber = 1;
public static void main(String[] args) {
// 创建打印任务
Runnable printTask = () -> {
// 循环输出1到100之间的奇数
while (currentNumber <= 100) {
// 使用synchronized同步块确保线程安全
synchronized (OrderedOddNumberPrinter.class) {
// 检查当前数字是否为奇数
if (currentNumber % 2 != 0) {
// 打印当前数字和线程名称
System.out.println(Thread.currentThread().getName() + " 输出: " + currentNumber);
currentNumber++; // 增加当前数字
// 唤醒在等待的其他线程
OrderedOddNumberPrinter.class.notifyAll();
} else {
try {
// 当前数字为偶数,线程等待
OrderedOddNumberPrinter.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
// 创建并启动两个线程
Thread thread1 = new Thread(printTask, "线程1");
Thread thread2 = new Thread(printTask, "线程2");
thread1.start();
thread2.start();
}
}
案例3:抽红包
需求
100元,共有三个红包分配金额,现在有五个人可以抽取,抽取到金额的人打印xxx抽取到xxx元,没抽取到金额则打印xxx没有抽取到金额。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class RedEnvelopeSystem {
public static void main(String[] args) {
// 创建一个红包对象,初始红包数量为3,总金额为100元
RedEnvelope redEnvelope = new RedEnvelope(3, 100);
// 创建五个线程来模拟五个人抽红包
for (int i = 1; i <= 5; i++) {
Thread person = new Thread(new RedEnvelopeTask(redEnvelope, "Person" + i));
person.start();
}
}
}
// 红包类,负责处理红包分配逻辑
class RedEnvelope {
private int remainingEnvelopes; // 剩余红包数量
private double totalAmount; // 总金额
private final Lock lock = new ReentrantLock(); // ReentrantLock对象,用于确保线程安全
// 构造方法,初始化红包数量和总金额
public RedEnvelope(int envelopes, double amount) {
this.remainingEnvelopes = envelopes;
this.totalAmount = amount;
}
// 抽红包的方法
public void grabEnvelope(String name) {
lock.lock(); // 获取锁,确保线程安全
try {
if (remainingEnvelopes > 0) {
// 简单的平均分配红包金额
double amount = totalAmount / remainingEnvelopes;
remainingEnvelopes--; // 剩余红包数量减少
totalAmount -= amount; // 总金额减少
// 打印抽取到的金额
System.out.println(name + " 抽取到 " + amount + " 元");
} else {
// 当没有剩余红包时,打印提示信息
System.out.println(name + " 没有抽取到金额");
}
} finally {
lock.unlock(); // 释放锁
}
}
}
// 红包任务类,实现Runnable接口
class RedEnvelopeTask implements Runnable {
private RedEnvelope redEnvelope; // 红包对象
private String name; // 线程名称(人名)
// 构造方法,初始化红包对象和人名
public RedEnvelopeTask(RedEnvelope redEnvelope, String name) {
this.redEnvelope = redEnvelope;
this.name = name;
}
// 线程运行方法
@Override
public void run() {
redEnvelope.grabEnvelope(name); // 抽取红包
}
}