文章目录
- Ⅰ. 创建线程的方式
- 方式一:继承 `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程序观察线程
在jdk的bin目录中有一个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)构造的,则会调用Runnable的run()方法。如果不重写,该方法默认为空实现。 |
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 | 打印当前线程的堆栈跟踪,通常用于调试。 |
💥注意事项:
run()只是注册了线程要做的动作,start()才是真的启动线程。关于中断线程:
interrupt()方法将中断标志设为true,表示 “我希望你停下来了”,而是否停下来取决于程序员的代码逻辑,不一定会真的中断,只是希望中断!在匿名内部类或者
lambda表达式中要判断是否被中断,有两种方式:Thread.interrupted()Thread.currentThread().isInterrupted(),更推荐这种方式,因为保持中断标志不变有助于上层逻辑知道这个线程是否被中断过。
通常
interrupt()的是休眠或者阻塞的线程,则会抛出InterruptedException异常,此时捕获异常后,可以在异常中处理一些需要收尾的业务,而不要直接抛出RuntimeException等异常来终止进程,这样子是不合理的!如下面代码所示: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();}
有时我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才能决定是否存钱,这时我们需要
join()来明确等待线程的结束,在张三的线程中调用李四.join()即可保证在李四之后进行存钱操作!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Ⅳ. 线程安全
一、线程不安全的原因
- 线程的调度执行是随机的(抢占式调度),这是线程不安全的根本原因!
- 多个线程同时修改同一个变量
- 修改变量的操作,不是原子性的,比如
count++并不是原子操作。- 注意:
java中对于内置类型的读取和赋值,是原子性的!
- 注意:
- 内存可见性、指令重排序问题
- 这两个问题通常用
volatile来解决,但是volatile并不保证原子性!
- 这两个问题通常用
二、synchronized关键字
进入synchronized修饰的代码块,相当于加锁
退出synchronized修饰的代码块,相当于解锁
实际上
synchronized底层不只是加锁,还有锁升级等机制,具体可以看多线程进阶的笔记!
① 修饰实例方法
publicsynchronizedvoiddoSomething(){// 临界区代码}- 锁对象是
this(当前实例)。 - 多个线程调用同一个对象的该方法时,会串行执行。
②修饰静态方法
publicstaticsynchronizedvoiddoSomethingStatic(){// 临界区代码}- 锁对象是
类名.class。 - 多个线程访问同一个类的静态同步方法,也会被串行化。
③修饰代码块(推荐⭐⭐⭐)
privatefinalObjectlock=newObject();// 自定义锁对象publicvoiddoSomething(){synchronized(lockObject){// 临界区代码}}- 锁对象除了内置类型以外,可以是任意类型的对象,但通常不能是
String、Integer、Boolean等常量对象,因为多个类、多个线程可能锁的是同一个对象! - 锁对象最好声明为
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}注意如果是一个线程进行多层获取同一个锁,只有锁计数count为0,才会释放该锁对象,如下所示:
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 Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此会由于不同平台下的内存模型的差异,导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。
在多线程中,CPU和编译器会出于性能考虑进行:
- 缓存优化(每个线程有自己的工作内存)
- 指令重排序(编译器或
CPU会乱序执行某些语句)
这些优化可能导致一个线程修改的变量,另一个线程看不到,从而引发 “明明已经赋值却读取到旧值” 等并发bug,而JMM就是为了解决这些问题而生。
JMM的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后两者是线程私有的,不会被线程共享。
JMM规定了所有的变量都存储在 “主内存” 中。每个线程还有自己的 “工作内存”,线程的 “工作内存” 中保存了被该线程使用到的变量的 “主内存” 副本拷贝,线程对变量的所有操作(如读取、赋值等)都必须在 “工作内存” 进行,而不能直接读写 “主内存” 中的变量。所以不同的线程之间也无法直接访问对方 “工作内存” 中的变量,线程间变量值的传递均需要通过 “主内存” 来完成。
“主内存” 和 “工作内存” 的描述如下表所示:
| 位置 | 描述 |
|---|---|
| 主内存(Main Memory) | 所有共享变量都在这里 |
| 工作内存(Working Memory) | 每个线程自己的变量副本(缓存) |
看起来很复杂,其实很简单!"主内存" 就是我们平常所说的主存,而 "工作内存" 实际上是CPU中的缓存和寄存器,而JMM这一套规则,实际上和我们上面讲解volatile时候解释的内存可见性是一个道理的,只不过用 “工作内存” 这个术语来涵盖了我们提到的CPU中的缓存或者寄存器!
即原始数据都是存放在 “主内存”,然后根据JMM优化会放到 “工作内存” 中执行,速度会大大提高,但就会存在 “主内存” 和 “工作内存” 数据不一致的情况,此时就要通过volatile来解决!
如果只关注操作系统或者硬件来说,根本就没有 “主内存”、“工作内存” 的说法!
虽然
java官方给的 “工作内存” 这个概念让人很晕,实际上就是指CPU中的缓存和寄存器,甚至指以后可能更新的缓存技术,但它的目的实际上是要让java程序员不用去关心底层是什么结构,让 “工作内存” 来直接代指这些缓存或者寄存器,甚至以后可能更新出来的技术,只需要让程序员知道这是 “工作内存” 的概念即可!
③ 指令重排序
简单的说,指令重排序就是编译器或CPU为了优化性能,改变了语句执行顺序,导致和原先的程序逻辑不一致的情况!
为了避免这种情况,同样是要使用volatile、synchronized、final等手段建立有序性屏障!