博客
Java线程
计算机
Java

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();

但这仍然无法替代 CountDownLatchawait() 功能,你还是得自己写循环判断:

while (count.get() > 0) {
    Thread.sleep(10);
}

效率低、复杂、不如 CountDownLatch 优雅。

六、总结:

CountDownLatch 是专门为多线程协同工作设计的工具类,它不仅线程安全,还能让你优雅地等待所有任务完成。
int i--; 只是一个普通的变量操作,不具备同步和等待功能,在多线程下极易出错。

© 2025 LH1010 - 豫ICP备2021036601号-1