news 2026/1/13 18:25:45

【JavaSE】十四、线程创建多种方式 Thread类 线程状态与生命周期 线程安全synchronized 编译器优化问题 volatile JMM

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【JavaSE】十四、线程创建多种方式 Thread类 线程状态与生命周期 线程安全synchronized 编译器优化问题 volatile JMM

文章目录

  • Ⅰ. 创建线程的方式
      • 方式一:继承 `Thread` 类,重写 `run()`
      • 方式二:实现 `Runnable` 接口,重写 `run()`,然后构造 `Thread` 进行传参
      • 方式三:匿名内部类继承 `Thread`,重写 `run()`
      • 方式四:匿名内部类实现 `Runnable`,重写 `run()`
      • 方式五:`lambda` 表达式创建 `Thread` 对象(推荐⭐⭐⭐)
    • 注意事项💥💥💥
    • 🛠️使用 `jconsole` 程序观察线程
  • Ⅱ. `Thread` 类及常见方法
  • Ⅲ. 线程的状态与生命周期
  • Ⅳ. 线程安全
    • 一、线程不安全的原因
    • 二、`synchronized` 关键字
      • **① 修饰实例方法**
      • ② **修饰静态方法**
      • ③ **修饰代码块**(推荐⭐⭐⭐)
      • 💥`synchronized` 是可重入锁
    • 三、编译器优化问题
      • ① 内存可见性问题 -- `volatile`
      • ② Java内存模型 -- `JMM`
      • ③ 指令重排序

Ⅰ. 创建线程的方式

方式一:继承Thread类,重写run()

/** * 创建线程方式一:继承Thread类,重写run() */classMyThreadextendsThread{@Overridepublicvoidrun(){// ...}}publicclassdemo1{publicstaticvoidmain(String[]args){MyThreadt=newMyThread();t.start();}}

方式二:实现Runnable接口,重写run(),然后构造Thread进行传参

/** * 创建线程方式二:实现Runnable接口,重写run(),然后传给Thread */classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){// ...}}publicclassdemo2{publicstaticvoidmain(String[]args){// 传给Thread,同时也可以给线程命名Threadt=newThread(newMyRunnable(),"MyThread");t.start();}}

方式三:匿名内部类继承Thread,重写run()

/** * 创建线程方式三:匿名内部类继承Thread,重写run() */publicclassdemo3{publicstaticvoidmain(String[]args){Threadt=newThread(){publicvoidrun(){// ...}};t.start();}}

方式四:匿名内部类实现Runnable,重写run()

/** * 创建线程方式四:匿名内部类实现Runnable,重写run() */publicclassdemo4{publicstaticvoidmain(String[]args){Runnablemyrunnable=newRunnable(){@Overridepublicvoidrun(){// ...}};Threadt=newThread(myrunnable);t.start();}}

方式五:lambda表达式创建Thread对象(推荐⭐⭐⭐)

/** * 创建线程方式五:lambda表达式创建Thread对象,顺便命名 */publicclassdemo5{publicstaticvoidmain(String[]args){Threadt=newThread(()->{// ...},"MyThread");t.start();}}

注意事项💥💥💥

  • 继承Thread类,直接使用this就表示当前线程对象的引用。
  • 实现Runnable接口,this表示的是MyRunnable的引用,需要使用Thread.currentThread()来获取当前线程对象。

🛠️使用jconsole程序观察线程

jdkbin目录中有一个jconsole程序,打开然后连接到对应的java程序,就能看到对应的程序信息,比如内存、线程等情况,如下图所示:

Ⅱ.Thread类及常见方法

方法名返回类型功能描述
Thread``()void构造一个新线程,但不与任何Runnable对象关联。(线程不会自动启动,需要调用start()方法来启动线程
Thread(Runnable target)void构造一个新线程,并与指定的Runnable对象关联。
Thread(Runnable target, String name)void构造一个新线程,并与指定的Runnable对象关联,同时设置线程的名称。
Thread(String name)void构造一个新线程,不与任何Runnable对象关联,但设置线程的名称。
static Thread currentThread()Thread返回当前正在执行的线程对象
void start()void启动线程,使线程进入就绪状态,等待调度器调度后执行run()方法。 如果线程已经启动,再次调用start()会抛出IllegalThreadStateException
void run()void线程的入口点,通常需要重写以实现线程的具体逻辑。如果线程是通过Thread(Runnable)构造的,则会调用Runnablerun()方法。如果不重写,该方法默认为空实现。
void join()void等待当前线程终止。调用该方法的线程会阻塞,直到被调用join()的线程执行完毕。如果线程已经终止,则立即返回。
void join(long millis)void等待当前线程终止,最多等待指定的毫秒数。如果线程在指定时间内结束,则调用该方法的线程会继续执行。如果线程在指定时间内未结束,则抛出InterruptedException
void join(long millis, int nanos)void等待当前线程终止,最多等待指定的毫秒数和纳秒数。精确控制等待时间,如果线程在指定时间内结束,则调用该方法的线程会继续执行。
(下面三个中断相关的方法,可以看看表格后面的注意事项!)
void interrupt()void中断线程。如果线程正在休眠或阻塞,不仅会设置中断标志为true还会抛出InterruptedException;如果线程正在运行,则只会设置中断标志为true
boolean isInterrupted()boolean检查线程是否被中断,如果线程被中断了,则返回true,否则返回false。不设置中断标志。
static boolean interrupted()boolean检查当前线程是否被中断,如果当前线程被中断了,则返回true并清除中断状态,即设置中断标志为false
void sleep(long millis)void使当前线程暂停执行指定的毫秒数。调用该方法的线程会进入休眠状态,不会占用 CPU 资源。如果线程在休眠期间被中断,会抛出InterruptedException
void sleep(long millis, int nanos)void使当前线程暂停执行指定的毫秒数和纳秒数。精确控制线程的休眠时间。如果线程在休眠期间被中断,会抛出InterruptedException
void yield()void暂停当前线程,让其他具有相同优先级的线程运行。线程调度器会重新调度线程,但不保证其他线程一定会运行。
void setPriority(int priority)void设置线程的优先级,优先级范围为 1(最低)到 10(最高),默认为 5。优先级高的线程可能会获得更多 CPU 时间。
int getPriority()int获取线程的优先级,返回线程的优先级值。
void setName(String name)void设置线程的名称,便于调试和监控。
String getName()String获取线程的名称,返回线程的名称字符串。
void setDaemon(boolean on)void设置线程为守护线程或用户线程。守护线程在所有用户线程结束后自动退出。
boolean isDaemon()boolean检查线程是否为守护线程,如果是守护线程,返回true
State getState()State获取线程的状态,返回线程的当前状态(如新建、运行、阻塞等)。
boolean isAlive()boolean检查线程是否处于活动状态,如果线程已经启动且尚未终止,返回true
void setUncaughtExceptionHandler(UncaughtExceptionHandler eh)void设置线程的未捕获异常处理器,用于处理线程中未捕获的异常。
UncaughtExceptionHandler getUncaughtExceptionHandler()UncaughtExceptionHandler获取线程的未捕获异常处理器,返回设置的异常处理器对象。
static void dumpStack()void打印当前线程的堆栈跟踪,通常用于调试。

💥注意事项:

  1. run()只是注册了线程要做的动作,start()才是真的启动线程。

  2. 关于中断线程:

    1. interrupt()方法将中断标志设为true,表示 “我希望你停下来了”,而是否停下来取决于程序员的代码逻辑,不一定会真的中断,只是希望中断!

    2. 在匿名内部类或者lambda表达式中要判断是否被中断,有两种方式:

      • Thread.interrupted()
      • Thread.currentThread().isInterrupted(),更推荐这种方式,因为保持中断标志不变有助于上层逻辑知道这个线程是否被中断过
    3. 通常interrupt()的是休眠或者阻塞的线程,则会抛出InterruptedException异常,此时捕获异常后,可以在异常中处理一些需要收尾的业务,而不要直接抛出RuntimeException等异常来终止进程,这样子是不合理的!如下面代码所示:

    4. publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt=newThread(()->{// 还没被打断则进行循环while(Thread.currentThread().isInterrupted()==false){System.out.println(Thread.currentThread().getName());try{Thread.sleep(1000);}catch(InterruptedExceptione){// 此时最好恢复中断标志,因为 sleep 抛出异常时会清除中断状态💥💥💥Thread.currentThread().interrupt();// 处理可能剩余的业务,而不至于直接退出导致业务完成一半// ...break;}}});t.start();System.out.println("main thread, 3秒后中断子线程");Thread.sleep(3000);t.interrupt();}
  3. 有时我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才能决定是否存钱,这时我们需要join()来明确等待线程的结束,在张三的线程中调用李四.join()即可保证在李四之后进行存钱操作!

  4. JVM只有在一个进程的所有非后台线程结束后,才会结束运行。

Ⅲ. 线程的状态与生命周期

线程的状态是枚举类型Thread.State,如下所示:

状态名英文名描述
NEW新建线程对象已创建,但还没调用start()
RUNNABLE可运行线程已启动,等待CPU调度执行run()方法
BLOCKED阻塞线程因等待锁资源,而暂停执行
WAITING等待线程无限期地等待另一个线程的唤醒(例如调用了wait()join()
TIMED_WAITING计时等待线程在等待一个特定的时间后被唤醒(如sleep(long)join(long)wait(long)
TERMINATED终止线程已完成执行或异常退出

我们可以通过Thread.getState()来获取当前进程在某个时刻的状态,如下所示:

publicclassdemo{publicstaticvoidmain(String[]args)throwsInterruptedException{// 打印所有状态for(Thread.Statestate:Thread.State.values()){System.out.println(state);}System.out.println("-----------");// 打印 t 线程某个时刻的状态Threadt=newThread(()->{for(inti=0;i<5;i++){try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}});System.out.println(t.getState()+" "+t.isAlive());t.start();System.out.println(t.getState()+" "+t.isAlive());t.join();System.out.println(t.getState()+" "+t.isAlive());}}// 运行结果:NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED-----------NEWfalseRUNNABLEtrueTERMINATEDfalse

Ⅳ. 线程安全

一、线程不安全的原因

  1. 线程的调度执行是随机的(抢占式调度),这是线程不安全的根本原因
  2. 多个线程同时修改同一个变量
  3. 修改变量的操作,不是原子性的,比如count++并不是原子操作。
    1. 注意:java中对于内置类型读取赋值,是原子性的
  4. 内存可见性、指令重排序问题
    1. 这两个问题通常用volatile来解决,但是volatile并不保证原子性

二、synchronized关键字

进入synchronized修饰的代码块,相当于加锁

退出synchronized修饰的代码块,相当于解锁

实际上synchronized底层不只是加锁,还有锁升级等机制,具体可以看多线程进阶的笔记!

① 修饰实例方法

publicsynchronizedvoiddoSomething(){// 临界区代码}
  • 锁对象是this(当前实例)。
  • 多个线程调用同一个对象的该方法时,会串行执行。

修饰静态方法

publicstaticsynchronizedvoiddoSomethingStatic(){// 临界区代码}
  • 锁对象是类名.class
  • 多个线程访问同一个类的静态同步方法,也会被串行化。

修饰代码块(推荐⭐⭐⭐)

privatefinalObjectlock=newObject();// 自定义锁对象publicvoiddoSomething(){synchronized(lockObject){// 临界区代码}}
  • 锁对象除了内置类型以外,可以是任意类型的对象,但通常不能是StringIntegerBoolean等常量对象,因为多个类、多个线程可能锁的是同一个对象!
  • 锁对象最好声明为private。因为如果锁对象是public的,别的类也能访问和同步这个对象,可能破坏原有的同步逻辑。
  • 此外,为了保证锁对象在多线程可见性和稳定性上的安全性,通过要给锁对象加上final关键字修饰
    • 举个例子,给你一把锁,如果别人偷偷换了一把新的,你以为门锁住了,其实别人可以从另一把锁进来,这就是 “非 final locker” 导致的线程安全崩溃。

💥synchronized是可重入锁

java中,synchronized同步块对同一条线程来说是可重入的,即不会出现自己把自己锁死的问题

可重入锁指的是同一个线程 可以多次获得同一个锁,不会发生死锁。每进入一层synchronized或调用一层锁方法,锁的持有计数lock count + 1,退出对应的一层,lock count - 1。只有当计数变为0,锁才真正释放。

如下面代码所示:

for(inti=0;i<50000;i++){// 连续两次锁同一个对象,在java中并不会出现死锁,只会被看做是一层加锁synchronized(locker){// lock count+1synchronized(locker){// lock count+1count++;}// lock count-1}// lock count-1}

注意如果是一个线程进行多层获取同一个锁,只有锁计数count0,才会释放该锁对象,如下所示:

Objectlocker=newObject();synchronized(locker){// +1synchronized(locker){// +1synchronized(locker){// +1synchronized(locker){// +1// 此时锁计数 = 4,线程持有 locker 的锁 4 次}// -1(计数 3)}// -1(计数 2)}// -1(计数 1)}// -1(计数 0,真正释放锁)

三、编译器优化问题

java程序编译期间,用javac.java文件编译成.class文件,这并不涉及到优化问题,但后续JIT编译器(Just-In-Time Compiler)会在JVM运行期间将热点代码(即被JVM判定为值得优化的代码)编译为本地机器码,提升性能,这就形成了一些优化比如内联函数、指令重排序、内存不可见、锁优化

这些优化实际上是很有必要的,因为很多编程新手对于程序的性能把握并不好,所以编译器这一层就多干了很多活来提高程序的效率,相当于提高了程序的效率下限。

但这些优化又无形带来了一些编程时候的bug,为了防止这些bug,我们可以使用内存屏障语义的关键字来避免,如下所示:

  • volatile:阻止指令重排序 + 保证变量对其他线程的可见性
  • synchronized:包含获取/释放锁的内存屏障
  • final:构造后不可变,构造安全(不能被重排)

① 内存可见性问题 –volatile

以下面代码为例,在t2输入非零之后,正常情况应该是t1就结束循环了,但程序却停不下来,t1一直在循环之中,这是为什么❓❓❓

publicclassdemo1{privatestaticintcount=0;publicstaticvoidmain(String[]args){Threadt1=newThread(()->{while(count==0){// do something}System.out.println("t1循环结束!!!");});Threadt2=newThread(()->{Scannersc=newScanner(System.in);count=sc.nextInt();System.out.println("t2结束!!!");});t1.start();t2.start();}}

这就是之前提到的编译器优化带来的问题,编译器会把while(count == 0)认为是热点代码,因为这是一个循环,会大量重复地访问count,所以编译器会将原本存放在内存中的count,复制到CPU的寄存器或者缓存中,然后在CPU中访问,这可以大大提高访问速度,因为CPU速度可是要比内存访问速度快上几千倍不止的!

但正是因为这个问题,t2修改的count还是那个存放在内存中的count,而此时t1那边跑的是CPU中的count,看不到内存那边已经修改了,所以就会一直判断while(count == 0)是成功的,导致了程序错误!

此时只需要在count前面加上volatile进行修饰,强制让编译器访问内存的那份count,即可解决这个优化带来的问题,但同时速度就会降低!

② Java内存模型 –JMM

JVM定义了一种Java内存模型(Java Memory ModelJMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此会由于不同平台下的内存模型的差异,导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。

在多线程中,CPU和编译器会出于性能考虑进行:

  • 缓存优化(每个线程有自己的工作内存)
  • 指令重排序(编译器或CPU会乱序执行某些语句)

这些优化可能导致一个线程修改的变量,另一个线程看不到,从而引发 “明明已经赋值却读取到旧值” 等并发bug,而JMM就是为了解决这些问题而生。

JMM的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段静态字段构成数组对象的元素,但不包括局部变量方法参数,因为后两者是线程私有的,不会被线程共享。

JMM规定了所有的变量都存储在 “主内存” 中。每个线程还有自己的 “工作内存”,线程的 “工作内存” 中保存了被该线程使用到的变量的 “主内存” 副本拷贝,线程对变量的所有操作(如读取、赋值等)都必须在 “工作内存” 进行,而不能直接读写 “主内存” 中的变量。所以不同的线程之间也无法直接访问对方 “工作内存” 中的变量,线程间变量值的传递均需要通过 “主内存” 来完成。

“主内存” 和 “工作内存” 的描述如下表所示:

位置描述
主内存(Main Memory)所有共享变量都在这里
工作内存(Working Memory)每个线程自己的变量副本(缓存)

看起来很复杂,其实很简单!"主内存" 就是我们平常所说的主存,而 "工作内存" 实际上是CPU中的缓存和寄存器,而JMM这一套规则,实际上和我们上面讲解volatile时候解释的内存可见性是一个道理的,只不过用 “工作内存” 这个术语来涵盖了我们提到的CPU中的缓存或者寄存器!

即原始数据都是存放在 “主内存”,然后根据JMM优化会放到 “工作内存” 中执行,速度会大大提高,但就会存在 “主内存” 和 “工作内存” 数据不一致的情况,此时就要通过volatile来解决!

如果只关注操作系统或者硬件来说,根本就没有 “主内存”、“工作内存” 的说法!

虽然java官方给的 “工作内存” 这个概念让人很晕,实际上就是指CPU中的缓存和寄存器,甚至指以后可能更新的缓存技术,但它的目的实际上是要让java程序员不用去关心底层是什么结构,让 “工作内存” 来直接代指这些缓存或者寄存器,甚至以后可能更新出来的技术,只需要让程序员知道这是 “工作内存” 的概念即可!

③ 指令重排序

简单的说,指令重排序就是编译器或CPU为了优化性能,改变了语句执行顺序,导致和原先的程序逻辑不一致的情况!

为了避免这种情况,同样是要使用volatilesynchronizedfinal等手段建立有序性屏障!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/15 11:01:36

25、SSH应用案例解析:安全访问与数据传输方案

SSH应用案例解析:安全访问与数据传输方案 在网络安全领域,SSH(Secure Shell)协议凭借强大的加密和身份验证功能,为远程访问和数据传输提供了安全保障。本文将通过三个具体案例,深入探讨SSH在不同场景下的配置与应用。 案例一:多用途SSH服务器配置 本案例涉及两个VShe…

作者头像 李华
网站建设 2025/12/18 10:59:34

1、深入了解 SSH:功能、优势与实施指南

深入了解 SSH:功能、优势与实施指南 1. SSH 概述 Secure Shell(SSH)是一种用途广泛的实用工具,它可以被描述为协议、加密工具、客户端/服务器应用程序或命令接口。SSH 凭借其多样的服务以及安全提供这些服务的能力,成为许多企业网络中的重要组成部分。 1.1 SSH1 与 SSH…

作者头像 李华
网站建设 2025/12/15 11:01:24

17、文本处理与输入输出操作指南

文本处理与输入输出操作指南 1. 常用文本处理命令 在文本处理中,有几个常用的命令可以帮助我们完成各种任务。 1.1 tr 命令 tr 命令可以用于字符替换和文件加密。例如,在显示 MyFile 文件时,tr 可以将单词 “Halloween” 改为 “Haloween”。同时,它还能使用 ROT13 方法…

作者头像 李华
网站建设 2026/1/13 16:14:50

科研写作新范式:解锁书匠策AI期刊论文功能的隐藏生产力

在科研写作的赛道上&#xff0c;研究者往往需要与文献海洋博弈、与逻辑漏洞周旋、与格式规范较劲。而当人工智能技术深度渗透学术场景&#xff0c;一款名为书匠策AI的科研工具正以“问题导向的智能辅助”重构期刊论文创作生态。本文将以创新视角拆解其核心功能&#xff0c;揭示…

作者头像 李华
网站建设 2025/12/15 11:01:04

13、SSH在网络设备中的应用与安全管理

SSH在网络设备中的应用与安全管理 1. Cisco设备的SSH配置 1.1 Cisco交换机 Cisco Catalyst操作系统(CatOS)从6.1版本开始支持SSH,不过仅支持SSH版本1。尽管SSH 1存在安全隐患,但它替代Telnet后,极大增强了网络设备的安全性。支持SSH的Cisco交换机有Catalyst 3550、4000…

作者头像 李华