带你理解JavaEE多线程基础(上)

📅 发布时间:2026/8/15 9:42:17
带你理解JavaEE多线程基础(上) 1、了解线程1.1 线程概念1. 进程操作系统资源分配的最小单位。打开微信、浏览器、IDEA、JVM每一个都是独立进程进程拥有独立内存空间、文件句柄、CPU 时间片、堆内存进程之间完全隔离进程切换开销极大操作系统要保存整套上下文2. 线程进程内部独立执行的最小调度单位也叫轻量级进程。一个进程至少包含 1 个主线程同一个进程内所有线程共享进程的堆内存、方法区、文件资源只私有自己的栈内存CPU 只调度线程不直接调度进程线程切换成本远低于进程一句话口诀进程是房子线程是房子里干活的人房子资源所有人共用每个人各自有自己的工作台私有栈。1.2 创建线程方法1MyThread 是Demo1 的非静态内部类继承 Thread 重写 runrun 为线程执行体。非静态内部类依赖外部类实例main静态方法里直接new MyThread()编译报错。启动线程必须调用start()自动执行 run直接调用 run 只是普通方法。class Demo1 { class MyThread extends Thread{ Override public void run(){ System.out.println(hello my thread); } } } public static void main(String[] args) { MyThread myThreadnew MyThread(); myThread.start(); } }方法2采用实现Runnable接口的方式定义线程任务MyThread重写run()放置线程执行代码。MyThread加static成为静态内部类可在静态main方法直接实例化。将Runnable任务对象传入Thread构造器调用start()开启新线程执行run方法。该方式规避单继承限制任务与线程解耦开发更常用。package practice.thread; public class Demo2 { static class MyThread implements Runnable{ Override public void run(){ System.out.println(hello my thread); } } public static void main(String[] args) { Thread threadnew Thread(new MyThread()); thread.start(); } }方法3重要使用Lambda表达式简化Runnable接口省去单独创建类直接填写线程执行任务。把Lambda任务传入Thread构造方法创建线程对象调用start()启动子线程打印一句话后线程结束。属于实现Runnable方式的极简写法代码最简洁日常开发高频使用。public class Demo3 { public static void main(String[] args) { Thread threadnew Thread(()-{ System.out.println(hello my thread); }); thread.start(); } }2、Thread类及其方法2.1 常见构造方法构造方法说明Thread()创建线程对象使用默认名称Thread(Runnable target)传入 Runnable 任务创建线程对象Thread(String name)创建线程对象并自定义线程名称Thread(Runnable target, String name)传入 Runnable 任务同时自定义线程名称2.2 常见属性属性获取方法简要说明IDgetId()获取线程唯一标识 ID不可修改名称getName()获取线程名可通过setName()修改状态getState()返回线程 6 种生命周期状态NEW/RUNNABLE 等优先级getPriority()获取线程优先级1~10影响 OS 调度概率是否后台线程isDaemon()判断是否为守护线程主线程退出守护线程自动终止是否存活isAlive()判断线程是否处于运行 / 未消亡状态是否被中断isInterrupted()检测线程中断标记不清除标记2.3 启动线程执行线程的start方法是调用系统API真正在操作系统内创建一个线程这个新线程会以run作为入口方法执行run里的逻辑→run方法不显式调用创建新线程自动执行的注如果是执行run方法不创建新线程2.4 中断线程方法完整说明public void interrupt()给目标线程打上中断标记若线程处于sleep()/wait()/join()阻塞状态直接抛出InterruptedException并自动清除中断标记public static boolean interrupted()静态方法检测当前执行线程的中断标记检测后立即清空标记位public boolean isInterrupted()实例方法检测调用该方法的线程对象的中断标记只查询、不会清除标记位public class Demo6 { public static void main(String[] args) throws InterruptedException { Thread tnew Thread(()-{ //currentThread()是在start方法之后执行的 //并且在t线程中执行返回t线程对象 while (!Thread.currentThread().isInterrupted()) { System.out.println(hello thread); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } }); t.start(); Thread.sleep(2000); t.interrupt(); System.out.println(线程中断); } }结果注意任务内部无法直接访问外部变量t做通用判断Thread.currentThread()自动获取正在执行这段代码的当前线程通用性强同一个任务可以绑定任意线程运行。降低代码耦合线程执行逻辑不依赖外部线程引用是线程内部检测自身中断标记的标准规范写法。如果直接写t.isInterrupted()语法能跑但任务被写死只能给t线程使用无法复用。2.5 等待线程join() 作用让当前线程阻塞插队等待另一个线程执行完成实现线程执行顺序控制。方法说明public void join()无限阻塞直到调用该方法的线程执行完毕当前线程才继续往下运行public void join(long millis)限时等待最多阻塞millis毫秒超时无论目标线程是否结束当前线程都会恢复执行public void join(long millis, int nanos)高精度限时等待毫秒 纳秒组合设置等待时长极少使用package practice.thread; public class Demo4 { public static void main(String[] args) throws InterruptedException{ Thread threadnew Thread(()-{ for(int i0;i10;i){ System.out.println(hello my threadi); } }); thread.start(); // 等待线程执行3秒 thread.join(3000); for(int i0;i10;i){ System.out.println(hello maini); } } }结果不加Join vs 加Join2.6 获取当前线程引用返回当前线程对象的引用public class Demo5 { public static void main(String[] args) { Thread threadThread.currentThread(); thread.setName(main thread); System.out.println(thread.getName()); } }2.7 休眠线程我们熟悉的sleeppublic class Demo6 { public static void main(String[] args) { Thread tnew Thread(()-{ long startTimeSystem.currentTimeMillis(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } long endTimeSystem.currentTimeMillis(); System.out.println(hello my thread (endTime-startTime)s); }); t.start(); } }结果2.8 前后台线程及区别1. 前台线程默认所有新建的线程都是前台JVM 退出规则只要任意一个前台线程还在运行JVM 进程就不会终止。必须所有前台线程全部执行完毕JVM 才会正常关闭。2. 守护线程后台线程setDaemon(true) 设置当所有前台线程全部执行完毕无论守护线程有没有跑完JVM 直接强制退出守护线程被立刻粗暴终止。public class Demo4 { public static void main(String[] args) { Thread t new Thread(()-{ while(true){ System.out.println(hello thread); try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } } },我的线程); System.out.println(是否是后台线程 t.isDaemon()); t.setDaemon(true);// 设置为后台线程 System.out.println(是否设置为后台线程 t.isDaemon()); System.out.println(已设置为后台线程); t.start(); for (int i 0; i 10; i) { System.out.println(hello main); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }结果原本线程t是无限循环将其设置为后台线程后线程t随着主线程结束而结束3、线程的状态3.1 线程状态情况状态简写核心说明NEW新建线程对象已创建未调用start()未启动RUNNABLE就绪 / 运行正在 CPU 执行或就绪排队争抢 CPU 时间片BLOCKED阻塞竞争synchronized同步锁失败阻塞排队等锁WAITING无限等待无期限挂起需其他线程主动唤醒才能恢复TIMED_WAITING限时等待设置等待时长超时自动苏醒也可提前唤醒TERMINATED终止run()执行完毕或异常退出线程生命周期结束3.2 状态的转移简单示例public class Demo7 { public static void main(String[] args) { Thread tnew Thread(()-{ System.out.println(hello my thread); }); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); } }结果4、多线程的安全问题4.1 不安全的情况public class Demo9 { public static int count 0; public static void main(String[] args) throws InterruptedException { Thread t1new Thread(()-{ for(int i0;i50000;i){ count; } }); Thread t2new Thread(()-{ for(int i0;i50000;i){ count; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }结果按照预期来讲结果应该是100000但是为什么是小于100000呢因为count不是原子操作其指令可拆解成1. 读取 count 当前值load 2. 执行数值 1 运算add 3. 把新值写回 count 变量store这三步中间线程可以被 CPU 调度打断就会发生覆盖丢失。假设 count 10 t1 读到 count10正要 1 CPU 切换到 t2t2 也读到 count10 t1 计算得到 11写回 count11 t2 计算得到 11写回 count11循环 5 万次大量这种冲突叠加最终结果必然 100000。解决方法count外加锁4.2 不安全的原因1. 线程调度是随机的操作系统采用抢占式调度线程获取 CPU 执行权、被挂起的时机完全随机不可预测。原本单线程串行执行的代码在多线程下会被任意切割、穿插执行所有并发安全问题的底层根源都来源于此。编写并发代码时必须兼容所有调度时序才能保证最终结果准确。2. 多个线程同时修改一个变量多个线程同时读写堆中静态变量、成员变量等公共资源共享变量仅有一份存储在主内存中直接产生数据竞争。若线程仅操作各自的局部私有变量不存在资源争抢天然不会发生线程安全问题。3. 修改操作不是原子的原子操作指不可被 CPU 中断、一次性执行完成的指令。count 10 单纯赋值属于原子操作count 自增是复合操作底层拆分为三步读取变量值 → 数值自增运算 → 结果写回变量。在线程切换时极易被打断出现两次自增只生效一次的丢失更新问题最终统计值小于预期。4. 内存可见性1JMM 内存模型基础Java 内存模型JMM为屏蔽不同 CPU、操作系统缓存架构差异做了两层抽象主内存对应 JVM 堆存放所有线程共享变量访问速度慢工作内存线程私有抽象空间底层为 CPU 寄存器 L1/L2/L3 高速缓存读写速度极快。线程不会直接操作主内存会先将变量拷贝到私有缓存运算再同步回主内存。2可见性异常产生过程JIT 即时编译器遵循as-if-serial规则仅保证单线程运行结果正确会对高频只读代码做性能优化。以空自旋while (flag 0)为例JIT 判定该循环只读取不修改flag仅第一次从主内存加载值存入 CPU 缓存后续循环直接复用缓存旧数据不再访问主内存。当t2修改flag并更新至主内存后t1依旧读取过期缓存值导致无限死循环无法退出。package Thread; import java.util.Scanner; public class Demo15 { // 未加volatile触发可见性问题 public static int flag 0; public static void main(String[] args) throws InterruptedException { Thread t1 new Thread(() - { while (flag 0) { // 空循环极易被JIT优化 } System.out.println(t1 结束); }); Thread t2 new Thread(() - { Scanner sc new Scanner(System.in); System.out.println(请输入flag的值); flag sc.nextInt(); System.out.println(t2 结束); }); t1.start(); t2.start(); t1.join(); t2.join(); } }3volatile 解决可见性及局限性1核心作用禁止 JIT 对变量做本地缓存优化强制每次读写都直接操作主内存通过内存屏障禁止指令重排序保障执行有序性。2重要局限volatile只保证可见性、有序性不具备原子性无法解决count这类复合运算并发问题仅适用于单线程写入、多线程读取的场景多线程同时修改共享变量必须使用synchronized或Lock锁。4volatile 与 synchronized 简要对比volatile轻量级无锁解决可见性与有序性不保证原子性synchronized重量级互斥锁同时保证原子性、可见性、有序性会阻塞竞争线程。5. 指令重排序1重排序定义编译器、CPU 为提升执行效率在单线程运行结果不变的前提下打乱代码原有书写顺序重新编排指令属于正常性能优化。单线程环境无影响多线程并发时会破坏业务逻辑。2经典风险案例对象初始化重排序对象创建逻辑理想有序三步分配堆内存空间执行构造方法完成对象初始化将内存地址赋值给引用变量指令重排序可能打乱为分配内存 → 赋值引用地址 → 执行构造方法初始化。其他线程若拿到未初始化完毕的对象引用会触发空指针、成员变量默认值异常等严重 bug。3约束方案volatile添加内存屏障禁止变量读写前后指令重排synchronized同步代码块锁保证临界区指令串行执行规避重排序干扰。4.3 解决这个问题5、synchronized关键字5.1 该关键字的特性1. 互斥synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏ 到同⼀个对象synchronized就会阻塞等待进⼊synchronized修饰的代码块,相当于加锁退出synchronized修饰的代码块,相当于解锁2. 可重入同一个线程已经拿到了某个对象的锁之后可以再次重复获取这把锁不会自己阻塞自己这就是锁的可重入机制。5.2 简要案例比如前面内容涉及的count数小于100000的情况可以通过加锁解决public class Demo12 { static class Counter{ private static int count 0; Object lockernew Object(); public void add(){ synchronized(locker){ count; } } } public static void main(String[] args) throws InterruptedException { Counter counternew Counter(); Thread t1new Thread(()-{ for(int i0;i50000;i){ counter.add(); } }); Thread t2new Thread(()-{ for(int i0;i50000;i){ counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }结果