Java 并发编程系列(上篇):多线程深入解析

一、开篇:走进 Java 并发编程世界

        在现代软件开发中,充分利用多核 CPU 的计算能力至关重要,Java 并发编程为我们提供了实现这一目标的工具。从简单的多线程任务并行执行,到复杂的高并发系统设计,掌握并发编程是进阶 Java 工程师的关键一步。本篇作为上篇,聚焦多线程基础、线程状态、线程组与优先级、进程线程区别,以及synchronized锁的基础与状态体系 。

        先叠个甲,由于这一块内容是面试必问的部分,也是经常用的,内容太多,我分三篇逐步更新,从基础线程概念到线程池、锁等复杂场景。

二、Java 多线程入门:创建与核心逻辑

(一)创建线程的三种方式

1. 继承 Thread 类:线程逻辑内聚

        继承Thread,重写run方法定义线程执行体。调用start方法启动新线程(直接调用run是普通方法调用,不会新建线程 )。

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "执行,i=" + i);
        }
    }
}

public class ThreadInheritDemo {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.setName("自定义线程1");
        thread1.start(); 

        MyThread thread2 = new MyThread();
        thread2.setName("自定义线程2");
        thread2.start(); 
    }
}

运行后,两个线程交替输出,体现多线程并发执行特性,适合线程逻辑简单且无需复用的场景。

2. 实现 Runnable 接口:解耦任务与线程

        将线程执行逻辑封装到Runnable实现类,避免单继承限制(Java 类仅能单继承,但可实现多个接口 ),方便任务逻辑复用。        

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "执行,i=" + i);
        }
    }
}

public class RunnableImplDemo {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread threadA = new Thread(runnable, "线程A");
        Thread threadB = new Thread(runnable, "线程B");
        threadA.start();
        threadB.start();
    }
}

 Runnable作为任务载体,被不同线程实例执行,常用于线程池、任务分发等场景。

3. 实现 Callable 接口:支持返回结果

  CallableRunnable类似,但call方法有返回值,需结合FutureFutureTask获取结果,适用于需线程执行产出数据的场景。

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

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 5; i++) {
            sum += i;
        }
        return sum;
    }
}

public class CallableImplDemo {
    public static void main(String[] args) {
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask, "计算线程");
        thread.start();

        try {
            Integer result = futureTask.get(); 
            System.out.println("线程计算结果:" + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

futureTask.get()会阻塞直到线程执行完毕返回结果,也可设置超时时间避免永久阻塞。

二)线程关键疑问:run 与 start、重写 run 的本质

1. 为何重写 run 方法?

  Thread类默认run方法体为空(public void run() {} ),重写run是为了定义线程实际执行的业务逻辑,让线程启动后执行我们期望的代码。

2. run 方法与 start 方法的区别
  • run 方法:普通实例方法,调用时由当前线程(调用run的线程)顺序执行run内代码,不会创建新线程。
  • start 方法Thread类特殊方法,调用后触发 JVM创建新线程,并由新线程执行run逻辑。即start是 “启动新线程 + 执行任务”,run只是 “执行任务(当前线程)” 。
class TestThread extends Thread {
    @Override
    public void run() {
        System.out.println("当前线程名:" + Thread.currentThread().getName());
    }
}

public class StartRunDistinguish {
    public static void main(String[] args) {
        TestThread thread = new TestThread();
        thread.setName("自定义线程");
        
        thread.run(); 
        thread.start(); 
    }
}

输出:
当前线程名:main
当前线程名:自定义线程
清晰展示两者差异,start才是真正启动新线程的方式。

三)控制线程的常用方法

1. sleep ():线程休眠

        使当前线程暂停指定时间(毫秒),让出 CPU 但不释放对象锁(若持有锁)。常用于模拟延迟、协调执行节奏。

public class SleepUsage {
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + "执行,i=" + i);
                try {
                    Thread.sleep(1000); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "睡眠线程").start();
    }
}

线程每隔 1 秒输出,期间 CPU 可被其他线程使用,但锁资源(若涉及同步代码)不会释放。

2. join ():线程等待

让当前线程等待目标线程执行完毕后再继续,用于协调线程执行顺序。

public class JoinUsage {
    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("线程A执行,i=" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadA.start();
        
        threadA.join(); 
        System.out.println("线程A执行完毕,main线程继续");
    }
}

 “main 线程继续” 需在线程 A 执行完 3 次循环后才输出,确保执行顺序。

其实也可以这样理解,让A线程插队,当前线程main在A线程执行完毕后再执行

3. setDaemon ():守护线程

        守护线程为用户线程服务,所有用户线程结束后,守护线程自动终止(如 JVM 的 GC 线程 )。需在start前设置。

public class DaemonUsage {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("守护线程运行中...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        daemonThread.setDaemon(true); 
        daemonThread.start();
        
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main线程结束");
    }
}

main 线程(用户线程)结束后,守护线程随之停止,不会无限循环。

4. yield ():线程让步

        当前线程主动让出 CPU 使用权,回到就绪状态重新参与调度,仅为 “建议”,不保证生效,用于给同优先级线程更多执行机会。
 

public class YieldUsage {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("线程1执行,i=" + i);
                Thread.yield(); 
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("线程2执行,i=" + i);
            }
        });
        thread1.start();
        thread2.start();
    }
}

线程 1 每次循环尝试让步,线程 2 可能更频繁执行,但因调度不确定性,结果不绝对一致。

三、Java 线程的 6 种状态:生命周期全解析

Java 线程状态定义在Thread.State枚举中,共 6 种,理解状态转换对排查线程问题、优化并发逻辑至关重要。

(一)状态枚举与含义

  1. NEW(新建):线程对象已创建(如new Thread() ),但未调用start,未启动。
  2. RUNNABLE(可运行):线程已启动(start调用后),可能正在 CPU 执行,或在就绪队列等待调度,也就是就绪状态。
  3. BLOCKED(阻塞):线程竞争synchronized锁失败,进入阻塞态,等待锁释放。
  4. WAITING(等待):线程调用Object.wait()(无超时)、Thread.join()(无超时)、LockSupport.park()等,无时限等待唤醒。
  5. TIMED_WAITING(计时等待):调用Thread.sleep(long)Object.wait(long)Thread.join(long) 、LockSupport.parkNanos/parkUntil等,限时等待,超时自动唤醒。
  6. TERMINATED(终止):线程执行完毕(run正常结束或抛未捕获异常),生命周期结束。

(二)状态转换示例

通过代码观察线程从新建到终止的状态变化:

public class ThreadStateAnalysis {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1500); 
                synchronized (ThreadStateAnalysis.class) {
                    System.out.println("线程执行中,获取锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println("线程状态(NEW):" + thread.getState()); 
        
        thread.start();
        Thread.sleep(500); 
        System.out.println("线程状态(TIMED_WAITING):" + thread.getState()); 
        
        Thread.sleep(1500); 
        System.out.println("线程状态(RUNNABLE/执行中):" + thread.getState()); 
        
        thread.join(); 
        System.out.println("线程状态(TERMINATED):" + thread.getState()); 
    }
}

         结合getState()与线程执行逻辑,可清晰看到状态NEWTIMED_WAITINGRUNNABLETERMINATED的流转。实际调试中,可借助 JConsole、VisualVM 等工具直观分析复杂状态切换。

四、线程组与线程优先级:管理与调度辅助

(一)线程组(ThreadGroup)

        线程组用于批量管理线程,可统一设置优先级、捕获未处理异常等。默认情况下,新建线程加入创建它的线程所在组(通常是main线程组 )。

public class ThreadGroupManagement {
    public static void main(String[] args) {
        ThreadGroup customGroup = new ThreadGroup("自定义线程组");
        Thread thread1 = new Thread(customGroup, () -> {
            System.out.println("线程1所属组:" + Thread.currentThread().getThreadGroup().getName());
        }, "线程1");
        Thread thread2 = new Thread(customGroup, () -> {
            System.out.println("线程2所属组:" + Thread.currentThread().getThreadGroup().getName());
        }, "线程2");
        
        thread1.start();
        thread2.start();
        
        Thread[] threads = new Thread[customGroup.activeCount()];
        customGroup.enumerate(threads);
        for (Thread t : threads) {
            System.out.println("线程组线程:" + t.getName());
        }
    }
}

线程组辅助批量操作,但现代并发更依赖线程池,线程组使用场景逐渐减少,了解即可。

(二)线程优先级:调度 “建议”

        线程优先级是调度器优先调度的 “建议”,范围 1(最低)~10(最高),默认 5。优先级高的线程理论上获取 CPU 时间片机会更多,但不保证执行顺序(受操作系统调度策略影响 )。

public class ThreadPriorityControl {
    public static void main(String[] args) {
        Thread highPriority = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("高优先级线程执行,i=" + i);
            }
        });
        highPriority.setPriority(Thread.MAX_PRIORITY); 

        Thread lowPriority = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("低优先级线程执行,i=" + i);
            }
        });
        lowPriority.setPriority(Thread.MIN_PRIORITY); 

        highPriority.start();
        lowPriority.start();
    }
}

        多次运行可能观察到高优先级线程更 “活跃”,但因系统调度不确定性,不能完全依赖优先级控制执行顺序,实际开发需谨慎使用。

五、进程与线程的区别:资源与执行单元

(一)核心差异对比

对比维度
进程
线程
资源分配单位操作系统分配资源(内存、文件句柄等)的基本单位进程内的执行单元,CPU 调度的基本单位
资源占用独立地址空间,资源消耗大共享进程资源(内存、文件描述符等),消耗小
上下文切换成本高(需切换地址空间、寄存器等)
低(主要切换寄存器、程序计数器)
通信复杂度进程间通信复杂(IPC:管道、socket 等)线程间通信简单(共享内存)
独立性进程间相互独立,一个崩溃不影响其他进程线程同属进程,一个崩溃可能致进程崩溃

(二)通俗类比

以 “在线文档编辑应用” 为例:

  • 进程:整个应用是进程,操作系统为其分配独立内存,存储代码、用户数据等,是资源隔离的单位。
  • 线程:拼写检查、自动保存、实时协作同步等功能,作为线程共享进程内存,协作完成任务。若拼写检查线程崩溃,可能导致整个应用(进程)异常,体现线程对进程的依赖。

 

六、synchronized 关键字:锁的基础与状态体系

(一)锁的基本认知:基于对象的锁

        Java 中每个对象均可作为锁,常说的 “类锁” 本质是Class对象的锁(Class对象在 JVM 加载类时创建,唯一对应类元数据 )。通过synchronized实现同步,保障多线程下共享资源的原子性、可见性。

(二)synchronized 的三种使用形式

1. 同步实例方法(锁当前对象)
        银行账户(Account)有一个withdraw方法,一个人在不同设备上同时取钱,多线程可能同时取款,需保证余额正确。

代码示例

public class Account {
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    // 同步实例方法:锁当前对象(this)
    public synchronized void withdraw(double amount) {
        if (balance >= amount) {
            try {
                Thread.sleep(100); // 模拟业务耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + "取款失败,余额不足");
        }
    }
}
  • 锁的是当前对象(this),若多个线程操作同一对象,会互斥。
  • 若多个线程操作不同对象,则互不影响(每个对象有独立的锁)。
2. 同步静态方法(锁类对象)
统计网站访问量(静态变量visitCount),多线程并发访问需保证计数正确。

代码示例

public class WebSite {
    private static int visitCount = 0;

    // 同步静态方法:锁类对象(WebSite.class)
    public static synchronized void incrementVisit() {
        visitCount++;
        System.out.println(Thread.currentThread().getName() + "访问,总访问量:" + visitCount);
    }
}
  • 锁的是类的Class对象(全局唯一),无论创建多少实例,所有线程都会互斥。
  • 适合保护静态共享资源(如全局计数器、配置信息)。
3. 同步代码块

电商系统中,商品库存(stock)和订单号生成器(orderIdGenerator)需分别加锁。

代码示例:

public class ShoppingSystem {
    private int stock = 10;
    private static final Object STOCK_LOCK = new Object(); // 库存锁
    private static final Object ORDER_LOCK = new Object(); // 订单号锁
    private static int orderId = 0;

    // 扣减库存
    public void reduceStock() {
        synchronized (STOCK_LOCK) { // 锁库存专用对象
            if (stock > 0) {
                stock--;
                System.out.println(Thread.currentThread().getName() + "扣减库存成功,剩余:" + stock);
            } else {
                System.out.println(Thread.currentThread().getName() + "库存不足");
            }
        }
    }

    // 生成订单号
    public static void generateOrderId() {
        synchronized (ORDER_LOCK) { // 锁订单号专用对象
            orderId++;
            System.out.println(Thread.currentThread().getName() + "生成订单号:" + orderId);
        }
    }
}
  • 锁对象可以是任意Object,推荐使用private static final修饰,避免外部访问。
  • 缩小锁的范围,提高并发性能(如只锁需要保护的代码,而非整个方法)。

结合场景更容易理解,注意理解,而非死记硬背 

(三)synchronized 的四种锁状态

        JVM 对synchronized锁进行优化,存在四种状态:偏向锁、轻量级锁、重量级锁、无锁,状态随竞争情况升级(不能降级,单向升级 )。

后面的内容我们在下一篇中讲....

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值