Java学习-多线程

一、基础知识

        在 Java 中,线程是程序执行的基本单元。多线程编程允许程序同时执行多个任务,提高了程序的效率和性能。在学习多线程编程之前,需要了解以下基本概念:

  • 线程:线程是执行代码的独立路径。一个程序可以同时执行多个线程,每个线程都有自己的执行栈和执行路径。

  • 并发:指的是多个线程在同一时间段内执行,实现任务的并行执行。

  • 同步:指的是协调多个线程的执行顺序,防止出现数据竞争和不一致的情况。

线程生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

进程和线程的区别

常见面试题

        它们分别代表了任务和执行单元。进程是程序的执行实例,拥有独立的地址空间和资源;线程是进程内的执行单元,共享进程的资源,可以并发执行任务。进程切换开销较大,但进程之间隔离性好;线程切换开销较小,但线程之间通信和同步相对复杂。选择进程还是线程取决于具体的应用场景和需求。

1. 定义

  • 进程:进程是程序的执行实例,是系统进行资源分配和调度的基本单位。每个进程都有自己的独立地址空间,包括代码、数据、堆栈等,可以运行在独立的内存空间中。

  • 线程:线程是进程中的一个执行路径,是程序执行的最小单位。一个进程可以包含多个线程,它们共享相同的内存空间和资源,可以并发执行任务。

2. 资源分配

  • 进程:每个进程都拥有独立的地址空间和资源,包括内存、文件句柄、设备等。进程之间的通信需要使用进程间通信(IPC)机制,如管道、信号、共享内存等。

  • 线程:线程是进程内的一个执行单元,共享进程的地址空间和资源。线程之间可以直接访问共享的内存,因此线程间通信更加简单高效,但也更容易出现竞争和同步问题。

3. 调度和切换

  • 进程:进程切换涉及到内存上下文的切换,需要保存和恢复进程的状态信息,因此进程切换开销较大。

  • 线程:线程切换只涉及到线程的上下文切换,通常只需要保存和恢复寄存器的状态信息,因此线程切换开销较小。

4. 创建和销毁

  • 进程:创建和销毁进程的开销较大,需要分配和释放大量的系统资源,包括内存、文件描述符等。

  • 线程:创建和销毁线程的开销较小,因为线程共享进程的资源,只需要分配和释放一些线程私有的资源。

5. 并发性和可扩展性

  • 进程:进程之间的通信和同步相对复杂,进程间切换开销较大,因此进程级别的并发性和可扩展性较差。

  • 线程:线程间通信和同步相对简单高效,线程切换开销较小,因此线程级别的并发性和可扩展性较好。

二、线程创建和管理

在 Java 中,线程的创建和管理有几种常见的方式:

1. 继承 Thread 类

通过继承 Thread 类并重写 run() 方法来创建线程。以下是创建线程的基本步骤:

步骤:
  1. 创建一个类,继承自 Thread 类。
  2. 在该类中重写 run() 方法,定义线程要执行的任务。
  3. 创建该类的实例,并调用 start() 方法启动线程。
示例:
public class MyThread extends Thread {
    public void run() {
        // 定义线程执行的任务
        System.out.println("Thread is running...");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 启动线程
    }
}

2. 实现 Runnable 接口

通过实现 Runnable 接口并将其传递给 Thread 类的构造函数来创建线程。以下是创建线程的基本步骤:

步骤:
  1. 创建一个类,实现 Runnable 接口。
  2. 在该类中实现 run() 方法,定义线程要执行的任务。
  3. 创建 Thread 类的实例,将实现了 Runnable 接口的类作为参数传递给 Thread 类的构造函数。
  4. 调用 start() 方法启动线程。
示例
public class MyRunnable implements Runnable {
    public void run() {
        // 定义线程执行的任务
        System.out.println("Thread is running...");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start(); // 启动线程
    }
}

这两种方法都可以创建线程,并在 Java 中非常常用。通常来说,推荐使用实现 Runnable 接口的方式,因为它避免了单继承的限制,并且使得类的设计更加灵活。

3. 实现 Callable 接口

实现 Callable 接口创建线程与实现 Runnable 接口类似,但是 Callable 接口的 call() 方法可以返回一个结果或抛出一个异常。需要通过 ExecutorServicesubmit() 方法提交 Callable 对象来执行,并可以获得 Future 对象来获取线程执行的结果。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class MyCallable implements Callable<String> {
    public String call() throws Exception {
        return "Callable thread is running...";
    }

    public static void main(String[] args) {
        // 创建一个 ExecutorService 实例
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // 提交 Callable 对象给 ExecutorService 执行
        Future<String> future = executor.submit(new MyCallable());

        try {
            // 获取线程执行的结果
            String result = future.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 关闭 ExecutorService
        executor.shutdown();
    }
}

日常工作的实际使用,会在项目里创建线程池,统一管理。可以提高系统稳定性。

三、线程同步和通信

在多线程编程中,需要特别注意线程之间的同步和通信问题,以避免数据竞争和死锁等并发问题。

  • 同步块和锁:使用 synchronized 关键字或 ReentrantLock 类来创建同步块,确保多个线程对共享资源的访问是安全的。

  • 线程通信:使用 wait()notify()notifyAll() 方法来实现线程之间的通信,以便在多个线程之间进行协调和同步。

线程同步(Thread Synchronization)

线程同步是指多个线程之间对共享资源的访问进行协调和控制,以避免数据竞争和并发访问导致的不确定性结果。常见的线程同步方式包括:

  1. Synchronized 关键字:使用 synchronized 关键字对关键代码块或方法进行同步,确保同一时刻只有一个线程可以访问共享资源。
public synchronized void synchronizedMethod() {
    // 同步的代码块
}
  1. Lock 接口:使用 Lock 接口及其实现类如 ReentrantLock 进行显式锁定和解锁,提供了更灵活和精细的线程同步控制。
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 同步的代码块
} finally {
    lock.unlock();
}

线程通信(Thread Communication)

线程通信是指多个线程之间通过共享对象来进行信息交换和协作,实现线程之间的互斥执行和协同工作。常见的线程通信方式包括:

  1. wait()、notify() 和 notifyAll() 方法:通过 wait() 方法使当前线程进入等待状态,通过 notify()notifyAll() 方法唤醒等待的线程。
synchronized (sharedObject) {
    while (condition) {
        sharedObject.wait();
    }
    // 执行任务
    sharedObject.notify();
}
  1. Condition 接口:使用 Condition 接口及其实现类如 ReentrantLocknewCondition() 方法来实现更灵活的线程通信。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
    while (condition) {
        condition.await();
    }
    // 执行任务
    condition.signal();
} finally {
    lock.unlock();
}

注意事项:

  1. 避免死锁:合理设计同步和通信机制,避免出现死锁现象。

  2. 避免饥饿和活锁:合理设计线程调度策略,避免线程饥饿和活锁问题。

  3. 精细控制同步范围:尽可能减小同步代码块的范围,以减少同步的开销和提高程序的并发性能。

  4. 合理使用线程间通信:根据实际需求选择合适的线程通信方式,避免不必要的线程唤醒和等待。

四. 线程池(Thread Pools)

        使用线程池可以有效地管理和重用线程,提高系统的稳定性和性能。线程池可以减少线程的创建和销毁开销,避免频繁地创建和销毁线程所带来的性能损耗,并且可以限制系统中并发线程的数量,防止因过多线程而导致系统资源耗尽或系统负载过高的问题。

线程池实现方式

1. ThreadPoolExecutor

ExecutorService executor = new ThreadPoolExecutor(
    5, // 核心线程池大小
    10, // 最大线程池大小
    60, // 空闲线程存活时间
    TimeUnit.SECONDS, // 时间单位
    new LinkedBlockingQueue<>() // 任务队列
);

executor.submit(() -> {
    // 执行任务的操作
});

2. Executors 工具类

ExecutorService executor = Executors.newFixedThreadPool(10);

executor.submit(() -> {
    // 执行任务的操作
});

3. ScheduledExecutorService

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

executor.scheduleAtFixedRate(() -> {
    // 定时执行任务的操作
}, 0, 1, TimeUnit.MINUTES);

4.自定义线程池

ExceptionCatchThreadPoolExecutor

//定义一个名为 "asyncExecutor" 的线程池 bean
    @Bean(name = "asyncExecutor")
    public Executor asyncExecutor() {
        //15:线程池的核心线程数,表示线程池中同时能够运行的线程数量。
        //30:线程池的最大线程数,表示线程池中最多能够拥有的线程数量。
        //200:空闲线程存活时间,表示当线程池中的线程数量超过核心线程数时,空闲线程的存活时间
        //TimeUnit.SECONDS:空闲线程存活时间的时间单位。
        //new LinkedBlockingQueue<>(200):任务队列,用于存放等待执行的任务。这里使用了一个容量为 200 的链表阻塞队列作为任务队列。
        //(runnable) -> new Thread(runnable, "activity_async_task_executor"):线程工厂,用于创建新的线程。这里使用一个 lambda 表达式来创建线程,线程的名称为 "activity_async_task_executor"。
        return new ExceptionCatchThreadPoolExecutor(15, 30, 200, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200),
                (runnable) -> new Thread(runnable, "activity_async_task_executor"),
                new ThreadPoolExecutor.DiscardOldestPolicy());
    }

注意事项:

  1. 资源管理:使用完线程池后及时关闭,释放资源,避免资源泄漏和浪费。

  2. 性能调优:合理配置线程池的大小、任务队列等参数,以达到最佳的性能和资源利用率。

  3. 异常处理:在任务执行过程中可能会抛出异常,需要及时捕获并处理。

  4. 线程安全性:线程池通常会涉及到共享的资源和状态,需要保证线程安全。

  • 12
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值