云计算百科
云计算领域专业知识百科平台

Java进阶技术3-多线程

线程

一. 概念

线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享该进程的资源(如内存空间、文件描述符等),但同时拥有各自独立的执行栈、程序计数器和寄存器集合。

线程的核心特征
  • 轻量级:相较于进程,线程的创建、销毁和切换成本更低(无需重新分配独立内存空间)。
  • 资源共享:同一进程内的线程共享进程的堆内存、全局变量、文件句柄等资源,便于线程间通信。
  • 独立调度:线程是 CPU 调度的基本单位,每个线程都有自己的执行路径(即函数调用栈)。
  • 并发执行:多个线程可在同一进程内并发运行,提高程序效率(如多任务处理时减少等待时间)。
  • 与进程的关系
    • 进程是资源分配的基本单位,线程是调度执行的基本单位。
    • 一个进程至少包含一个线程(称为主线程),也可创建多个线程(称为子线程)。
    • 进程间资源相互独立,线程间资源共享(因此需注意线程安全问题,如竞态条件)。
    举例说明
    • 打开一个浏览器(进程),其中 “网页渲染”“下载文件”“播放音频” 等功能可由不同线程同时执行,它们共享浏览器的内存和网络连接。

    二. 创建线程的四种方式

    1. 继承Thread类并重写run()方法

    步骤:

    • 定义类继承java.lang.Thread类
    • 重写run()方法(线程执行的核心逻辑)
    • 创建线程对象并调用start()方法启动线程(而非直接调用run())

    示例代码:

    package com.itheima.ReStudy2;

    public class Demo1 {
    public static void main(String[] args) {
    Thread t = new MyThread();
    t.start();
    }
    }
    class MyThread extends Thread{
    @Override
    public void run() {
    System.out.println("恭喜你,创建线程成功");
    }
    }

    特点:

    • 实现简单,但受限于 Java 单继承特性,灵活性较低。

    2. 实现Runnable接口

    步骤:

    • 定义类实现java.lang.Runnable接口
    • 实现run()方法(线程执行逻辑)
    • 创建Thread对象,将Runnable实例作为参数传入
    • 调用Thread对象的start()方法启动线程

    示例代码:

    package com.itheima.ReStudy2;

    public class Demo2 {
    public static void main(String[] args) {

    Thread t = new Thread(new MyThread2());
    t.start();
    }
    }
    class MyThread2 implements Runnable{
    @Override
    public void run() {
    System.out.println("恭喜你,通过Runnable接口成功实现了一个线程");
    }
    }

    特点:

    • 避免单继承限制,可同时实现其他接口,推荐优先使用。

    3. 实现Callable接口(带返回值的线程)

    步骤:

    • 定义类实现java.util.concurrent.Callable<V>接口(V为返回值类型)
    • 实现call()方法(线程逻辑,可返回结果并抛出异常)
    • 创建FutureTask<V>对象包装Callable实例(用于获取返回值)
    • 创建Thread对象传入FutureTask,调用start()启动
    • 通过FutureTask的get()方法获取线程执行结果

    示例代码:

    package com.itheima.ReStudy2;

    import java.util.Objects;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;

    public class DEmo3Callable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    FutureTask f = new FutureTask<Object>(new MyThread3());
    Thread t = new Thread(f);
    t.start();
    String a = (String) f.get();
    // 需要通过FutureTask获取Callable的返回值
    System.out.println(a);
    }
    }
    class MyThread3 implements Callable<Object>{
    @Override
    public Object call() throws Exception {
    String a = "执行Callable成功 返回了一个String字符串!";
    return a;
    }
    }

    特点:

    • 支持返回执行结果,可抛出异常,适用于需要线程执行结果的场景(如并行计算)。

    核心区别总结

    方式实现复杂度是否有返回值灵活性适用场景
    继承Thread 简单 低(单继承) 简单线程任务
    实现Runnable 中等 多线程共享资源或需多实现
    实现Callable 较高 需要线程执行结果或异常处理

    实际开发中,Runnable和Callable因灵活性更常用,尤其是配合线程池使用时。

    4.线程池

    在 Java 中,线程池是管理线程生命周期的重要机制,能够有效避免频繁创建 / 销毁线程的性能开销。你提到的应该是ThreadPoolExecutor(核心实现类)和Executors(工具类),下面详细介绍:

    4.1 核心实现类:ThreadPoolExecutor

    ThreadPoolExecutor是线程池的核心实现类,位于java.util.concurrent包下,通过灵活配置参数可满足不同场景需求。

    核心构造参数

    public ThreadPoolExecutor(
    int corePoolSize, // 核心线程数(始终存活的线程数)
    int maximumPoolSize, // 最大线程数(线程池允许的最大线程数)
    long keepAliveTime, // 非核心线程空闲超时时间
    TimeUnit unit, // 超时时间单位(如TimeUnit.SECONDS)
    BlockingQueue<Runnable> workQueue, // 任务等待队列(存放未执行的任务)
    ThreadFactory threadFactory, // 线程工厂(用于创建线程)
    RejectedExecutionHandler handler // 拒绝策略(任务满时的处理方式)
    )

    常用拒绝策略
  • AbortPolicy(默认):直接抛出RejectedExecutionException异常
  • CallerRunsPolicy:让提交任务的线程自己执行任务
  • DiscardPolicy:直接丢弃任务(不抛异常)
  • DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交新任务
  • 示例代码:

    package com.itheima.ReStudy2;

    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;

    public class Demo4ThreadPoolExecutor {
    public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
    3,//核心线程数
    5, //最大线程数
    10,//线程空闲时间
    TimeUnit.SECONDS,//销毁时间单位
    new ArrayBlockingQueue<>(4),//任务队列
    Executors.defaultThreadFactory(),//线程工厂
    new ThreadPoolExecutor.AbortPolicy()//拒绝策略
    );
    for (int i = 0; i < 15; i++) {
    executor.execute(new MyThread4());
    System.out.println("当前线程数:"+executor.getPoolSize());
    System.out.println("当前排队线程数:"+executor.getQueue().size());
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    executor.shutdown();
    }
    }
    class MyThread4 implements Runnable{
    public void run()
    {
    System.out.println(Thread.currentThread().getName()+"执行了");
    }
    }

    4.2 工具类:Executors

    Executors提供了便捷的静态方法,可快速创建预配置的线程池(底层仍基于ThreadPoolExecutor),适合简单场景。但阿里巴巴开发手册不推荐使用,因默认参数可能导致资源耗尽(如无界队列)。

    常用方法
  • Executors.newFixedThreadPool(int nThreads)

    • 固定大小的线程池(核心线程数 = 最大线程数)
    • 队列使用LinkedBlockingQueue(无界,可能堆积大量任务导致 OOM
  • ExecutorService fixedPool = Executors.newFixedThreadPool(5); // 固定5个线程

    Executors.newCachedThreadPool()

    • 可缓存的线程池(核心线程数 = 0,最大线程数 = Integer.MAX_VALUE)
    • 空闲线程 60 秒后销毁,适合短期任务
    • 风险:任务过多时可能创建大量线程导致 OOM

    ExecutorService cachedPool = Executors.newCachedThreadPool();

    Executors.newSingleThreadExecutor()

    • 单线程池(核心线程数 = 最大线程数 = 1)
    • 所有任务按顺序执行,适合需要串行执行的场景

    ExecutorService singlePool = Executors.newSingleThreadExecutor();

    Executors.newScheduledThreadPool(int corePoolSize)

    • 定时任务线程池,支持延迟执行或周期性执行

    ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
    // 延迟1秒后执行
    scheduledPool.schedule(() -> System.out.println("延迟执行"), 1, TimeUnit.SECONDS);
    // 延迟2秒后,每3秒执行一次
    scheduledPool.scheduleAtFixedRate(() -> System.out.println("周期执行"), 2, 3, TimeUnit.SECONDS);

    4.3 线程池使用流程
  • 创建线程池(推荐ThreadPoolExecutor手动配置参数)
  • 通过submit()或execute()提交任务(submit()可返回Future获取结果)
  • 任务执行完毕后,调用shutdown()(平缓关闭)或shutdownNow()(强制关闭)
  • 4.4 核心区别与建议
    类型优势劣势建议场景
    ThreadPoolExecutor 参数可控,灵活适配场景 配置复杂 生产环境,需精细控制资源
    Executors工具类 快速创建,代码简洁 默认参数有潜在风险(如 OOM) 简单场景或测试环境

    最佳实践:生产环境中推荐直接使用ThreadPoolExecutor,根据业务需求合理设置核心线程数、队列大小和拒绝策略,避免资源耗尽风险。

    三. 多线程并发安全解决

    在多线程环境中,当多个线程同时操作共享资源时,可能会导致数据不一致、逻辑错误等问题,这就是并发安全问题。

    1. 并发安全问题的产生条件
  • 多线程环境:存在多个线程同时执行
  • 共享资源:线程操作同一份可修改的资源(如全局变量、静态变量、对象属性等)
  • 非原子操作:对共享资源的操作不是一步完成的(如i++实际包含读取、修改、写入三步)
  • 示例:不安全的多线程操作

    public class UnsafeThreadDemo {
    private static int count = 0; // 共享资源

    public static void main(String[] args) throws InterruptedException {
    // 创建1000个线程,每个线程对count加100次
    Runnable task = () -> {
    for (int i = 0; i < 100; i++) {
    count++; // 非原子操作,存在安全问题
    }
    };

    Thread[] threads = new Thread[1000];
    for (int i = 0; i < 1000; i++) {
    threads[i] = new Thread(task);
    threads[i].start();
    }

    // 等待所有线程执行完毕
    for (int i = 0; i < 1000; i++) {
    threads[i].join();
    }

    System.out.println("最终结果:" + count); // 预期100000,实际往往小于该值
    }
    }

    问题原因:多个线程同时读取count的值,修改后再写回(由于线程执行并发机制),导致部分增量操作被覆盖。

    二、并发安全问题的解决方案
    1. 同步锁(synchronized代码块)

    通过 synchronized关键字锁定共享资源,确保同一时间只有一个线程能执行临界区代码。

    语法:

    synchronized (锁对象) { // 操作共享资源的代码(临界区) }

    示例:

    public class SynchronizedBlockDemo {
    private static int count = 0;
    private static final Object lock = new Object(); // 锁对象(推荐使用专用对象)

    public static void main(String[] args) throws InterruptedException {
    Runnable task = () -> {
    for (int i = 0; i < 100; i++) {
    synchronized (lock) { // 锁定临界区
    count++;
    }
    }
    };

    // 启动线程逻辑同上…
    Thread[] threads = new Thread[1000];
    for (int i = 0; i < 1000; i++) {
    threads[i] = new Thread(task);
    threads[i].start();
    }

    for (int i = 0; i < 1000; i++) {
    threads[i].join();
    }

    System.out.println("最终结果:" + count); // 正确输出100000
    }
    }

    特点:

    • 锁对象可以是任意对象,但多个线程必须使用同一个锁对象才能保证同步
    • 推荐使用private static final Object作为锁对象,避免锁对象被修改
    2. 同步方法(synchronized方法)

    将 synchronized关键字修饰在方法上,此时锁对象为:

    • 非静态方法:当前对象(this)
    • 静态方法:当前类的Class对象

    示例:

    public class SynchronizedMethodDemo {
    private static int count = 0;

    // 静态同步方法(锁对象为SynchronizedMethodDemo.class)
    private static synchronized void increment() {
    count++;
    }

    public static void main(String[] args) throws InterruptedException {
    Runnable task = () -> {
    for (int i = 0; i < 100; i++) {
    increment(); // 调用同步方法
    }
    };

    // 启动线程逻辑同上…
    Thread[] threads = new Thread[1000];
    for (int i = 0; i < 1000; i++) {
    threads[i] = new Thread(task);
    threads[i].start();
    }

    for (int i = 0; i < 1000; i++) {
    threads[i].join();
    }

    System.out.println("最终结果:" + count); // 正确输出100000
    }
    }

    Lock相比synchronized的优势:

    • 可中断锁:支持tryLock(long time, TimeUnit unit)超时获取锁,避免死锁
    • 可尝试获取锁:tryLock()方法尝试获取锁,失败则立即返回
    • 公平锁:ReentrantLock可通过构造参数new ReentrantLock(true)实现公平锁(按请求顺序获取锁)
    • 可分离锁:配合Condition实现更灵活的线程通信
    3.三种方案的对比
    方案实现方式灵活性性能(一般场景)适用场景
    同步代码块 synchronized(锁) 中等 较好 需锁定代码片段时
    同步方法 synchronized修饰方法 低(粒度粗) 一般 简单场景,整个方法需同步时
    Lock锁 显式lock()/unlock() 较好(竞争激烈时更优) 复杂场景(超时、公平锁等)

    最佳实践:

    • 简单场景优先使用synchronized(代码简洁,JVM 优化更好)
    • 复杂场景(如需要超时控制、公平锁)使用Lock
    • 尽量缩小同步范围(只锁必要的代码),减少线程阻塞时间
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java进阶技术3-多线程
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!