https://www.runoob.com/java/java-multithreading.html
Java线程
Java 的线程是 Java 并发编程的核心概念之一,用于实现多任务的并发执行。线程是程序执行流的最小单元,多个线程可以同时运行在同一个进程中,共享进程的资源(如内存、文件句柄等),但每个线程有自己的执行路径。
1. 线程的基本概念
进程 vs 线程:
进程:一个独立的程序实例,拥有独立的内存空间。
线程:进程内的一个执行单元,共享进程的资源,但有自己独立的执行栈和寄存器。
多线程的优势:
提高程序的响应速度(例如 GUI 应用中的后台任务)。
提高 CPU 利用率,特别是在多核处理器上。
实现复杂的并发逻辑(如生产者-消费者模型)。
2. 创建线程的方式
在 Java 中,可以通过以下几种方式创建线程:
方法 1:继承 Thread
类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程正在运行:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
方法 2:实现 Runnable
接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程正在运行:" + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
方法 3:使用 Lambda 表达式(简化实现 Runnable
)
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程正在运行:" + Thread.currentThread().getName());
});
thread.start();
}
}
方法 4:实现 Callable
接口(支持返回值和异常处理)
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程完成,结果:" + Thread.currentThread().getName();
}
}
public class Main {
public static void main(String[] args) throws Exception {
FutureTask<String> task = new FutureTask<>(new MyCallable());
Thread thread = new Thread(task);
thread.start();
// 获取线程执行结果
String result = task.get();
System.out.println(result);
}
}
3. 线程的状态
Java 线程在其生命周期中会经历以下几种状态(定义在 Thread.State
枚举中):
1,NEW:线程创建后尚未启动。
2,RUNNABLE:线程正在运行或等待 CPU 时间片。
3,BLOCKED:线程被阻塞,等待获取锁。
4,WAITING:线程无限期等待其他线程的操作(如调用了 wait()
、join()
或 LockSupport.park()
)。
5,TIMED_WAITING:线程在指定时间内等待(如调用了 sleep()
、wait(long)
或 join(long)
)。
6,TERMINATED:线程执行完成。
4. 线程的常用方法
方法名 | 描述 |
---|---|
start() |
启动线程,使其进入就绪状态(RUNNABLE )。 |
run() |
线程的任务代码入口,直接调用不会启动新线程。 |
sleep(long millis) |
让当前线程暂停指定的毫秒数,不释放锁。 |
yield() |
提示调度器当前线程愿意让出 CPU,但不保证立即切换。 |
join() |
等待该线程执行完成后再继续执行调用线程。 |
interrupt() |
中断线程(设置中断标志位)。 |
isInterrupted() |
检查线程是否被中断。 |
setPriority(int newPriority) |
设置线程优先级(范围 1~10,默认为 5)。 |
currentThread() |
返回当前正在执行的线程对象。 |
5. 线程同步与锁
当多个线程访问共享资源时,需要确保线程安全。Java 提供了多种机制来实现线程同步:
1,使用 synchronized
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2,使用 ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
3,使用 volatile
volatile
关键字确保变量的可见性,但不保证原子性。
class SharedResource {
private volatile boolean flag = true;
public void toggleFlag() {
flag = !flag;
}
public boolean isFlag() {
return flag;
}
}
6. 线程池
为了减少频繁创建和销毁线程的开销,Java 提供了线程池机制。常用的线程池类型包括:
1,固定大小线程池:
ExecutorService executor = Executors.newFixedThreadPool(5);
2,缓存线程池:
ExecutorService executor = Executors.newCachedThreadPool();
3,单线程池:
ExecutorService executor = Executors.newSingleThreadExecutor();
4,计划任务线程池:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
7. 线程间通信
Java 提供了多种线程间通信机制:
1. 使用 wait()
和 notify()
class SharedResource {
private boolean flag = false;
public synchronized void produce() throws InterruptedException {
while (flag) {
wait();
}
System.out.println("生产...");
flag = true;
notify();
}
public synchronized void consume() throws InterruptedException {
while (!flag) {
wait();
}
System.out.println("消费...");
flag = false;
notify();
}
}
2. 使用 BlockingQueue
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
queue.put(i);
System.out.println("生产:" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者
Thread consumer = new Thread(() -> {
try {
while (true) {
int value = queue.take();
System.out.println("消费:" + value);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
8. 线程的常见问题
1,死锁:
多个线程互相持有对方需要的资源,导致所有线程都无法继续执行。
解决方法:避免嵌套锁,按固定顺序获取锁。
2,活锁:
线程不断尝试某个操作但始终失败,导致无法取得进展。
3,线程饥饿:
某些线程因优先级低而长时间得不到执行机会。
总结
Java 的线程是实现并发编程的基础工具,提供了丰富的 API 和机制来管理线程的生命周期、同步资源以及线程间通信。合理使用线程和线程池可以显著提升程序的性能和可扩展性,但也需要注意线程安全和潜在的并发问题。
Java线程 和 php队列
Java的线程和PHP的队列并不是同一个概念,它们分别属于两种不同的编程范式和技术实现,用于解决不同类型的问题。
Java的线程
在Java中,线程(Thread)是程序执行流的最小单元。多线程允许一个程序同时执行多个任务,每个线程可以独立地运行代码块。Java提供了强大的并发编程支持,包括但不限于:
- Thread类:用于创建和管理线程。
- Runnable接口:提供一种没有返回值的、能够在线程中运行的方法。
- Executor框架:提供更高级的线程管理和调度功能,例如线程池等。
使用多线程可以提高程序的响应速度和资源利用率,特别是在处理I/O密集型或计算密集型任务时非常有用。
PHP的队列
在PHP环境中,”队列”通常指的是消息队列或者任务队列的概念,它是一种跨进程或跨服务器的任务调度机制。通过队列,可以将耗时任务异步化,避免阻塞HTTP请求。常用的消息队列系统有RabbitMQ、Redis、Beanstalkd等。PHP可以通过这些队列系统来实现以下功能:
- 异步任务处理:比如发送邮件、图片处理等不需要立即完成的任务。
- 解耦系统组件:生产者和消费者模式可以减少系统的耦合度。
- 负载均衡:通过队列可以有效地分配工作负载,提高系统的可扩展性。
总结
虽然两者都涉及到并行或并发处理,但是它们的作用和使用场景不同:
- Java的线程更多是指在同一进程中同时执行多个代码路径的能力,侧重于CPU时间片的分配和共享资源的同步控制。
- PHP的队列则是一种分布式任务调度和处理机制,旨在提高应用的响应速度和处理能力,尤其适用于需要进行异步处理的Web应用。
因此,Java的线程与PHP的队列不是相同的概念,但都是为了提升应用性能而设计的不同解决方案。根据实际需求选择合适的方案是非常重要的。
示例1 按照账号 划分线程
使用线程池,按照账号,划分线程
// 创建一个固定大小为 3 的线程池。
// 就像你开了一个餐厅,请了 3 个服务员 来服务顾客(任务)。
// 这些线程会一直存活,直到你调用 shutdown()。
ExecutorService esMain = Executors.newFixedThreadPool(3);
// 创建一个倒计时门闩,初始值等于 accountList 的大小。
// 就像你手里拿着一个倒计时牌,上面写着“还剩 X 个任务要完成”。
// 每个任务完成后,都会调用 countDown(),表示“我完成了!”
// 当倒计时归零时,主线程才能继续往下走。
CountDownLatch cdlMain = new CountDownLatch(accountList.size());
// 遍历 accountList,为每个账户提交一个任务到线程池中去执行。
// 使用 Lambda 表达式定义任务内容。
// processAccount(account) 是你要对每个账户做的操作,比如登录、查询余额、发送邮件等。
// 不管任务是否出错,最后都会执行 countDown()(因为放在 finally 块里),确保倒计时能正常减少。
for (Account account : accountList) {
esMain.execute(() -> {
try {
// 执行任务
// 执行逻辑的任务
processAccount(account);
} finally {
cdlMain.countDown(); // 表示当前这个任务已完成
}
});
}
// 主线程在这里等待,直到所有子任务都调用了 countDown(),也就是倒计时归零。
// 就像比赛终点裁判说:“我要等所有人都冲过终点线才能宣布结果。”
cdlMain.await(); // 主线程等待所有任务完成
// 所有任务都完成后,这句才会执行。
// 可以在这里做汇总、清理、通知等操作。
System.out.println("所有账户处理完成");
// 告诉线程池不再接收新任务。
// 已经提交的任务还会继续执行。
// 类似于餐厅打烊,服务员可以继续完成手头工作,但不再接新单。
esMain.shutdown(); // 关闭线程池
不使用线程池,按照账号,划分线程
不使用线程池,按照账号,划分线程
// 获取要处理的账户列表。
// 比如可能是从数据库查出来的用户账号信息。
List<Account> accountList = getAccountList(); // 假设这是你的账户列表
// 创建一个倒计时门闩,初始值等于账户数量。
// 就像你手里拿着一个倒计时牌,写着“还剩 X 个任务要完成”。
// 每个任务完成后调用一次 countDown(),表示“我完成了!”
// 当倒计时归零时,主线程才能继续往下走。
CountDownLatch cdlMain = new CountDownLatch(accountList.size());
// 遍历 accountList,为每个账户创建一个新的线程去处理它。
// 使用 Thread 类创建线程,并通过 Lambda 表达式定义任务内容。
// processAccount(account) 是你要对每个账户做的操作(比如登录、查询余额、发送邮件等)。
// 不管任务是否出错,最后都会执行 countDown()(因为放在 finally 块里),确保倒计时能正常减少。
for (final Account account : accountList) {
Thread thread = new Thread(() -> {
try {
processAccount(account);
} finally {
cdlMain.countDown(); // 确保无论是否发生异常都会调用countDown()
}
});
thread.start(); // 启动线程
}
// 主线程在这里等待,直到所有子任务都调用了 countDown(),也就是倒计时归零。
// 就像比赛终点裁判说:“我要等所有人都冲过终点线才能宣布结果。”
cdlMain.await(); // 主线程在这里等待,直到所有子线程完成工作
// 所有任务都完成后,这句才会执行。
// 可以在这里做汇总、清理、通知等操作。
System.out.println("所有账户处理完成");
为什么不使用 i— 代替 CountDownLatch?
为什么不直接用一个 int i
,然后每个线程执行完就 i--
,而要用 CountDownLatch
?
一、先说结论一句话
因为 int i--;
是线程不安全的,并且它没有提供同步机制和阻塞等待的功能。
所以不能用 int i--;
替代 CountDownLatch.countDown();
二、举个生活中的例子帮助理解
想象你在餐厅门口当接待员,要等所有服务员都下班后才能关门。
方式一:自己看表(用 int)
int count = 5;
for (...) {
new Thread(() -> {
doWork();
count--;
}).start();
}
while (count > 0) {
// 等着...
}
这就相当于:
- 你让 5 个服务员去干活。
- 每个人干完活回来打个招呼(
count--
)。 - 你站在门口盯着这个数字,等它变成 0 才关门。
但是问题来了:
- 有可能两个服务员同时改这个数字,导致数据出错。
- 编译器可能对代码进行优化,让你永远看不到
count == 0
。 - 这种方式效率低、不可靠、容易出错。
方式二:使用 CountDownLatch
CountDownLatch cdl = new CountDownLatch(5);
for (...) {
new Thread(() -> {
doWork();
cdl.countDown();
}).start();
}
cdl.await();
System.out.println("所有人都完成了!");
这就相当于:
- 你手里拿着一个倒计时牌,写着“还剩5人”。
- 每个服务员完成任务后按一下按钮(
countDown()
),表示“我完成了!” - 当倒计时归零时,系统自动通知你:“人都到齐了,可以关门了。”
更加安全、可靠、语义清晰。
三、技术上的几个核心区别
特性 | int i--; |
CountDownLatch |
---|---|---|
是否线程安全 | ❌ 不是 | ✅ 是 |
是否能阻塞主线程等待 | ❌ 需要手动写循环 | ✅ 提供 await() 方法 |
是否支持多个线程协作 | ❌ 很难控制 | ✅ 天生为多线程设计 |
是否有原子操作保障 | ❌ 没有 | ✅ 内部使用 CAS 原子操作 |
是否可读性强 | ❌ 含义模糊 | ✅ 表达意图清晰 |
四、更深入一点:线程不安全的问题在哪?
Java 中,i--
看似简单,但其实是一个 复合操作:
1,读取 i
的值
2,把 i
减1
3,把新值写回 i
在多线程环境下,多个线程可能同时执行这三步,导致结果错误。
比如:
- 初始值
i = 5
- 线程A读到了5,准备减1
- 线程B也读到了5,准备减1
- 最终两者都写入4,而不是期望的3
这就是典型的竞态条件(Race Condition)
五、如果真想用变量实现类似功能,应该怎么做?
你可以用 Java 提供的 原子类,比如:
AtomicInteger count = new AtomicInteger(accountList.size());
// 在线程中:
count.decrementAndGet();
但这仍然无法替代 CountDownLatch
的 await()
功能,你还是得自己写循环判断:
while (count.get() > 0) {
Thread.sleep(10);
}
效率低、复杂、不如 CountDownLatch
优雅。
六、总结:
CountDownLatch
是专门为多线程协同工作设计的工具类,它不仅线程安全,还能让你优雅地等待所有任务完成。
而 int i--;
只是一个普通的变量操作,不具备同步和等待功能,在多线程下极易出错。