安派尔

Java并发多线程学习指南

2021/06/16
1
0

Java并发多线程学习指南

在Java开发中,并发多线程是提升程序性能、优化资源利用率的核心技能。本文将从核心概念入手,逐步拆解线程创建、同步控制、线程池等关键技术,搭配可直接运行的代码示例来学习掌握Java多线程编程。

一、并发与多线程核心概念

1.1 并发与并行的区别

并发(Concurrency)与并行(Parallelism)容易混淆,二者本质差异在于是否“真正同时执行”:

  • 并发:多个任务交替执行,适用于单核CPU环境,通过CPU时间片轮转实现“逻辑上的同时”。例如单核手机同时运行音乐播放和消息推送,CPU在两个任务间快速切换。
  • 并行:多个任务真正同时执行,依赖多核CPU,每个任务分配独立核心。例如多核服务器同时处理多个HTTP请求,实现“物理上的同时”。

1.2 线程与进程的关系

进程是资源分配的基本单位,拥有独立的内存空间;线程是CPU调度的基本单位,共享所属进程的内存资源,仅拥有程序计数器、栈等独立上下文。一个进程可以包含多个线程,线程间切换的开销远小于进程切换,这也是多线程提升效率的核心原因。

1.3 多线程的核心价值

在实际开发中,多线程的应用场景无处不在,核心价值体现在三点:

  1. 资源利用率最大化:通过线程复用充分利用CPU、I/O设备资源,例如数据库连接池通过并发管理提升查询效率。
  2. 响应速度优化:异步处理耗时任务,避免主线程阻塞。例如电商平台订单提交后,用独立线程处理库存更新,主线程快速返回结果。
  3. 吞吐量提升:高并发场景下,通过线程池和任务队列提升单位时间处理能力,例如秒杀系统实现每秒万级请求处理。

二、Java线程创建的3种核心方式

Java提供了3种线程创建方式,各有适配场景,需根据业务需求选择,避免盲目使用。

方式1:继承Thread类

最基础的实现方式,核心是继承Thread类并重写run()方法,通过start()启动线程(注意:不可直接调用run()方法,否则会作为普通方法执行)。

代码示例:卖车票

package com.apierr.springtest;


// 1. 继承Thread类,实现车票售卖线程
class TicketThread extends Thread {
    // 静态变量存储车票总数,保证所有线程共享同一资源
    private static int ticketCount = 10;
    private static int totalTicketsSold;
    // 窗口名称,区分不同售卖窗口
    private String windowName;

    // 构造方法传入窗口名称
    public TicketThread(String windowName) {
        this.windowName = windowName;
    }

    // 2. 重写run()方法,定义车票售卖逻辑
    @Override
    public void run() {
        // 循环售卖,直到车票售罄
        while (true) {
            if (ticketCount <= 0) {
                System.out.println(windowName + ":车票已售罄,停止售卖!");
                break;
            }
            // 模拟售票延迟(如用户付款、打印车票)
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 售卖车票,车票数量减1
            System.out.println(windowName + " 售出1张车票,剩余车票:" + --ticketCount);
            System.out.println("一共售出" + ++totalTicketsSold + "张车票");

            // 模拟窗口间切换间隔,让并发效果更明显
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class TicketThreadDemo {
    public static void main(String[] args) {
        // 3. 创建3个窗口线程,启动多线程售卖
        TicketThread window1 = new TicketThread("窗口1");
        TicketThread window2 = new TicketThread("窗口2");
        TicketThread window3 = new TicketThread("窗口3");

        window1.start();
        window2.start();
        window3.start();
    }
}

优缺点

优点:实现简单,适合快速验证逻辑;缺点:受Java单继承机制限制,继承Thread后无法再继承其他类,且线程与任务耦合。

注:以上会出现超卖的情况

方式2:实现Runnable接口

为解决单继承痛点而生,将任务与线程分离,实现Runnable接口定义任务逻辑。

代码示例

package com.apierr.springtest;

import java.util.concurrent.FutureTask;

class TickeRunnable implements Runnable{
    // 静态变量存储车票总数,保证所有线程共享同一资源
    private static int ticketCount = 10;
    private static int totalTicketsSold;
    // 窗口名称,区分不同售卖窗口
    private String windowName;

    // 构造方法传入窗口名称
    public TickeRunnable(String windowName) {
        this.windowName = windowName;
    }
    @Override
    public void run() {
        // 循环售卖,直到车票售罄
        while (true) {
            if (ticketCount <= 0) {
                System.out.println(windowName + ":车票已售罄,停止售卖!");
                break;
            }
            // 模拟售票延迟(如用户付款、打印车票)
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 售卖车票,车票数量减1
            System.out.println(windowName + " 售出1张车票,剩余车票:" + --ticketCount);
            System.out.println("一共售出" + ++totalTicketsSold + "张车票");

            // 模拟窗口间切换间隔,让并发效果更明显
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TicketThreadDemo2 {
    public static void main(String[] args) {
        TickeRunnable tickeRunnable1 = new TickeRunnable("窗口1");
        TickeRunnable tickeRunnable2 = new TickeRunnable("窗口2");
        TickeRunnable tickeRunnable3 = new TickeRunnable("窗口3");
        Thread window1 = new Thread(tickeRunnable1);
        Thread window2 = new Thread(tickeRunnable2);
        Thread window3 = new Thread(tickeRunnable3);
        window1.start();
        window2.start();
        window3.start();
    }
}

优缺点与适用场景

优点:解除线程与任务的耦合,支持多线程共享任务逻辑,不影响类的继承关系,扩展性强。适用场景:大多数业务开发,尤其是多线程共享任务的场景(如多线程处理同一份数据)。

方式3:实现Callable接口

前两种方式的run()方法无返回值、无法抛出受检异常,若需任务执行后返回结果(如复杂计算、远程接口调用),可使用Callable接口,配合FutureTask获取结果。

代码示例

package com.apierr.springtest;

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

class TicketCallable implements Callable{
    // 静态变量存储车票总数,保证所有线程共享同一资源
    private static int ticketCount = 10;
    private static int totalTicketsSold;
    // 窗口名称,区分不同售卖窗口
    private String windowName;

    // 构造方法传入窗口名称
    public TicketCallable(String windowName) {
        this.windowName = windowName;
    }
    @Override
    public Integer call() throws Exception {
        int i  = 0;
        // 循环售卖,直到车票售罄
        while (true) {
            if (ticketCount <= 0) {
                System.out.println(windowName + ":车票已售罄,停止售卖!");
                break;
            }
            // 模拟售票延迟(如用户付款、打印车票)
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 售卖车票,车票数量减1
            System.out.println(windowName + " 售出1张车票,剩余车票:" + --ticketCount);
            System.out.println("一共售出" + ++totalTicketsSold + "张车票");
            ++i;

            // 模拟窗口间切换间隔,让并发效果更明显
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return i;
    }
}
public class TicketThreadDemo3 {
    public static void main(String[] args) throws Exception{
        FutureTask<Integer> futureTask1 = new FutureTask<>(new TicketCallable("窗口1"));
        FutureTask<Integer> futureTask2 = new FutureTask<>(new TicketCallable("窗口2"));
        FutureTask<Integer> futureTask3 = new FutureTask<>(new TicketCallable("窗口3"));

        new Thread(futureTask1).start();
        new Thread(futureTask2).start();
        new Thread(futureTask3).start();
        Integer i1 = futureTask1.get();
        Integer i2 = futureTask2.get();
        Integer i3 = futureTask3.get();
        System.out.println(i1);
        System.out.println(i2);
        System.out.println(i3);
    }

}

核心特点

支持返回值和受检异常,FutureTask可控制任务状态(取消任务、判断是否完成),适用于需要获取任务执行结果的场景(如异步计算、数据统计)。

三、线程生命周期与状态转换

Java线程有5种核心状态,理解状态转换是掌握线程调度的关键,避免出现线程阻塞、死锁等问题:

  1. 新建(New):线程对象创建但未调用start()方法,此时未与操作系统线程关联。
  2. 就绪(Runnable):调用start()方法后,线程等待CPU调度,具备执行条件但未获得时间片。
  3. 运行(Running):获得CPU时间片,执行run()/call()方法中的任务逻辑。
  4. 阻塞(Blocked):因等待锁、I/O资源、sleep()等操作暂停执行,释放CPU时间片,等待条件满足后重回就绪状态。
  5. 死亡(Terminated):任务执行完毕或异常终止,线程生命周期结束,不可再次启动。

核心转换触发条件:sleep()、wait()会使线程进入阻塞态;notify()/notifyAll()唤醒线程重回就绪态;线程执行完毕自动进入死亡态。

四、线程同步与锁机制(解决线程安全问题)

多线程共享资源时,易出现数据错乱(如上面的几个示例都会出现车票超卖的情况),需通过同步机制保证原子性、可见性和有序性,核心方案是锁机制。

4.1 synchronized关键字(基础锁)

synchronized是Java内置的互斥锁,可修饰方法或代码块,保证同一时刻只有一个线程进入临界区,无需手动释放锁(异常或方法结束时自动释放)。

代码示例:

package com.apierr.springtest;


// 1. 继承Thread类,实现车票售卖线程
class TicketThread extends Thread {
    // 静态变量存储车票总数,保证所有线程共享同一资源
    private static int ticketCount = 10;
    private static int totalTicketsSold;
    // 窗口名称,区分不同售卖窗口
    private String windowName;

    // 构造方法传入窗口名称
    public TicketThread(String windowName) {
        this.windowName = windowName;
    }


    // 2. 重写run()方法,定义车票售卖逻辑
    @Override
    public void run() {
        // 循环售卖,直到车票售罄
        while (true) {
            //添加类级别锁
            synchronized (TicketThread.class){
                if (ticketCount <= 0) {
                    System.out.println(windowName + ":车票已售罄,停止售卖!");
                    break;
                }
                // 模拟售票延迟(如用户付款、打印车票)
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 售卖车票,车票数量减1

                ticketCount--;
                totalTicketsSold++;

                System.out.println(windowName + " 售出1张车票,剩余车票:" + ticketCount);
                System.out.println("一共售出" + totalTicketsSold + "张车票");

                // 模拟窗口间切换间隔,让并发效果更明显
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

public class TicketThreadDemo {
    public static void main(String[] args) {
        // 3. 创建3个窗口线程,启动多线程售卖
        TicketThread window1 = new TicketThread("窗口1");
        TicketThread window2 = new TicketThread("窗口2");
        TicketThread window3 = new TicketThread("窗口3");

        window1.start();
        window2.start();
        window3.start();
    }
}

4.2 Lock接口(进阶锁)

JDK 5引入的Lock接口(常用实现类ReentrantLock),提供比synchronized更灵活的锁控制,支持可中断锁、超时获取锁、公平锁等特性,需手动释放锁(建议在finally中释放)。

代码示例:ReentrantLock使用

package com.apierr.springtest;

import java.util.concurrent.FutureTask;
import java.util.concurrent.locks.ReentrantLock;

class TickeRunnable implements Runnable{
    // 静态变量存储车票总数,保证所有线程共享同一资源
    private static int ticketCount = 10;
    private static int totalTicketsSold;
    // 窗口名称,区分不同售卖窗口
    private String windowName;
    // 静态重入锁:所有线程共享同一把锁,保证线程安全
    private static final ReentrantLock lock = new ReentrantLock();

    // 构造方法传入窗口名称
    public TickeRunnable(String windowName) {
        this.windowName = windowName;
    }
    @Override
    public void run() {
        // 循环售卖,直到车票售罄
        while (true) {
            lock.lock();
            try {
                if (ticketCount <= 0) {
                    System.out.println(windowName + ":车票已售罄,停止售卖!");
                    break;
                }
                // 模拟售票延迟(如用户付款、打印车票)
                Thread.sleep(300);

                // 售卖车票,车票数量减1
                System.out.println(windowName + " 售出1张车票,剩余车票:" + --ticketCount);
                System.out.println("一共售出" + ++totalTicketsSold + "张车票");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                // 解锁:必须在finally中,避免死锁
                lock.unlock();
            }


            // 模拟窗口间切换间隔,让并发效果更明显
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TicketThreadDemo2 {
    public static void main(String[] args) {
        TickeRunnable tickeRunnable1 = new TickeRunnable("窗口1");
        TickeRunnable tickeRunnable2 = new TickeRunnable("窗口2");
        TickeRunnable tickeRunnable3 = new TickeRunnable("窗口3");
        Thread window1 = new Thread(tickeRunnable1);
        Thread window2 = new Thread(tickeRunnable2);
        Thread window3 = new Thread(tickeRunnable3);
        window1.start();
        window2.start();
        window3.start();
    }
}

4.3 并发安全集合

普通集合(如ArrayList、HashMap)线程不安全,高并发场景下需使用并发集合:

  • ConcurrentHashMap:JDK 8后采用CAS+Synchronized+红黑树结构,替代分段锁,并发性能优异,适用于高频读写场景。
  • CopyOnWriteArrayList:写时复制机制,读操作无锁,适用于读多写少场景(如配置缓存)。
  • BlockingQueue:支持阻塞的队列(如ArrayBlockingQueue),常用于生产者-消费者模型,简化线程通信。

五、线程池(实战核心:线程复用)

频繁创建/销毁线程会产生大量开销,线程池通过复用线程、管理任务队列,实现线程生命周期的统一管控,是实际开发的首选方案。

5.1 线程池核心参数(ThreadPoolExecutor)

手动创建线程池需配置核心参数,避免使用Executors工具类(易导致内存溢出),核心参数如下:

  1. corePoolSize:核心线程数,线程池长期保留的线程数(建议设为CPU核心数:Runtime.getRuntime().availableProcessors())。
  2. maximumPoolSize:最大线程数,线程池可创建的最多线程数(建议为核心线程数的2倍)。
  3. keepAliveTime:非核心线程空闲存活时间,超时后回收(建议设为60秒)。
  4. workQueue:任务队列,存储等待执行的任务(推荐有界队列,如ArrayBlockingQueue,防止内存溢出)。
  5. RejectedExecutionHandler:拒绝策略,任务超出线程池承载时的处理方式(推荐CallerRunsPolicy,由调用方线程执行)。

5.2 常用线程池类型与实战

package com.apierr.springtest;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

// 售票任务类:封装售票逻辑
class TicketSaleTask implements Runnable {
    // 静态变量:共享车票总数(暂不处理超卖)
    private static int ticketCount = 10;
    private static int totalTicketsSold;
    // 核心:静态重入锁,所有任务共享同一把锁,保证线程安全
    private static final ReentrantLock lock = new ReentrantLock();
    // 窗口名称
    private String windowName;

    public TicketSaleTask(String windowName) {
        this.windowName = windowName;
    }

    @Override
    public void run() {
        // 循环售票,直到车票售罄
        while (true) {
            lock.lock();
            try {
                // 暂不处理超卖,仅实现多线程并发逻辑
                if (ticketCount <= 0) {
                    System.out.println( windowName + ":车票已售罄,停止售卖!");

                    break;
                }

                // 模拟售票延迟(用户付款/打印车票)
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    // 响应线程中断,优雅退出
                    Thread.currentThread().interrupt();
                    System.out.println(windowName + " 售票被中断,停止售卖!");
                    break;
                }

                // 售卖车票(暂不处理超卖)
                System.out.println( windowName + " 售出1张车票,剩余车票:" + --ticketCount);
                System.out.println("一共售出" + ++totalTicketsSold + "张车票");
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }



            // 模拟窗口切换间隔
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}

public class TicketThreadPoolDemo {
    public static void main(String[] args) {
        // 1. 配置ThreadPoolExecutor线程池参数(适配3个窗口场景)
        int corePoolSize = 3; // 核心线程数:固定3个(对应3个窗口)
        int maximumPoolSize = 3; // 最大线程数:和核心线程数一致,避免创建临时线程
        long keepAliveTime = 0; // 空闲线程存活时间:核心线程不超时
        TimeUnit unit = TimeUnit.MILLISECONDS;
        // 任务队列:无界队列,存放待执行的任务(本场景仅3个任务,队列不会满)
        LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();

        // 2. 创建线程池(使用默认的线程工厂和拒绝策略)
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue
        );

        // 3. 提交3个窗口的售票任务到线程池
        threadPool.submit(new TicketSaleTask("窗口1"));
        threadPool.submit(new TicketSaleTask("窗口2"));
        threadPool.submit(new TicketSaleTask("窗口3"));

        // 4. 优雅关闭线程池:先停止接收新任务,再等待所有任务执行完成
        threadPool.shutdown();
        try {
            // 等待60秒,确保所有售票任务执行完成(超时则强制关闭)
            if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
                threadPool.shutdownNow(); // 超时强制关闭
            }
        } catch (InterruptedException e) {
            threadPool.shutdownNow(); // 响应中断,强制关闭
        }

        System.out.println("所有窗口售票结束,线程池已关闭!");
    }
}

常见线程池类型:FixedThreadPool(固定大小线程池)、CachedThreadPool(可缓存线程池)、ScheduledThreadPool(定时任务线程池),需根据业务场景选择。