news 2026/7/17 5:23:40

线程(三)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线程(三)

一,线程状态

线程的状态是一个枚举类型 Thread.State

1.1 NEW

new:安排了工作,还未开始行动

即Thread对象创建了,还没start

public class Demo11 { public static void main(String[] args) { Thread t = new Thread(() -> { while(true){ System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); } }

运行结果为:

1.2 TERMINATED

terminated:工作完成了

Thread对象还在,但是系统中的线程已经销毁(入口方法已经执行完了)

public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); t.join(); System.out.println(t.getState()); } }

执行结果为:

小tips:两次调用start()的时候,在第二次调用start的时候会抛出异常

即Thread对象得是NEW状态才可以调用start

1.3 RUNNABLE:就绪状态

runnable:可工作的,又可以分成正在工作中和即将开始工作

就绪状态:1)正在CPU上执行

2)随时可以调度到cpu上执行(当前没在cpu上)

public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } } }); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }

1.4 阻塞状态

1.4.1 BLOCKED:排队等着其他事情

由于锁竞争引起的堵塞

1.4.2 TIMED_WAITING:排队等着其他事情

由于sleep引起的堵塞或者join(超时时间)引起的堵塞

即有固定期限的堵塞

public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }

运行结果显示为runnable而不是timed_waited是因为t.getState()在sleep休眠之前调用了,所有为了确保得到一个timed_waited的状态,可以在getState上加sleep(500)来确保当前的getState是在t线程sleep过程中调用的

public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); Thread.sleep(500); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }

1.4.3 WAITING:排队等着其他事情

join()死等引起的堵塞

即没有固定期限的堵塞

public class Demo15 { public static void main(String[] args) throws InterruptedException { Thread mainThread = Thread.currentThread(); Thread t = new Thread(() -> { try { mainThread.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); // 在 start 之前调用的 System.out.println(t.getState()); t.start(); // 此处加上 sleep(500) 更好的确保当前的 getState 是在 t 线程 sleep 过程中调用的 Thread.sleep(500); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }

在这个代码中main主线程和t线程相互等待对方,所以就卡死了

改为

mainThread.join(1000);

1.5 通过第三方工具查看线程的状态

jconsole

二,线程安全

1)某个代码,在单线程环境下执行,没有任何问题,但是如果在多线程环境下执行,就有bug

此时,这个代码就叫“线程不安全”或者叫做存在”线程安全问题“

2)如果这个代码,单线程下和多线程下,都没有任何问题,此时,就叫做”线程安全“

public class Demo12 { private static int count = 0; public static void main(String[] args) throws InterruptedException { // 创建两个线程, 针对同一个变量进行循环自增 Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); // 此处预期的结果是 10w System.out.println("count = " + count); } }

执行这个代码可以发现,执行结果次次不一样而且和预期的结果相差较大

出现bug的原因如下:

从计算机如何工作的角度,即cpu执行指令的角度来看待问题

count++这个代码对应三个指令(1)load:把内存中的数据,加载到cpu寄存器里 (2) add:把cpu寄存器中的值+1 (3)save:把寄存器的值写回内存

线程的调度是随机的,一个线程在cpu上执行呢,随时可能被从cpu上调度走,所以当cpu执行这三步的时候,有可能执行到一半被调走(但是调度的时候,至少保证当前的“指令”执行完,不会一个指令执行到一半被调度走)

所以执行顺序有无数种,保证一个程序,线程安全,就是在每一种情况下都没有bug

其中几种情况如下:

以上结果是正常的

以上结果是不正常的

t1和t2是两个线程,是两套上下文(两套寄存器)彼此之间不会修改对方的寄存器

线程安全产生的原因:

1)【根本】线程调度是随机的,一个线程执行到任何一个指令都可能被从cpu上调度走

2)多个线程同时修改同一个变量

1)不是同时,一个线程先操作,一个线程后操作------没问题

2)多个线程同时读取这个变量--------没问题

联想String是不可变对象的意义有:1,方便放到常量池中缓存 2,方便被计算hash值 3,线程安全

所以有的编程语言,解决线程安全 并发编程问题就采用了不可变方案

3)多个线程同时修改不同变量------没问题

3)【直接原因】针对变量的修改操作不是原子的

4)内存可见性引起的线程安全问题

5)指令重排序引起的线程安全问题

解决方法:

  • 通过锁,让一个变量只能被一个线程修改,这个过程中,其他线程只能等

注意:锁不是让线程停止调度,加锁的过程中,线程仍然是可以被调度出cpu的,而是只要你持有锁,别人就不能使用

在Java中使用锁:大部分的语言中,对于锁的操作,都是分为两个方法来进行的

eg:Locker locker;

locker.lock();

count++;

locker.unlock();

但是Java是通过synchronized关键字,搭配代码块的方式,实现的

eg:synchronized(锁对象){

count++;

}

  • 在Java中,任何一个对象都可以作为锁对象,哪怕是一个String,HashMap,Thread........Object
  • 锁对象的用途,只有一个,用来判定多个线程之间,是否存在“锁竞争”/“锁冲突”,和在{}里操作哪个对象的属性没有关系
  • 锁竞争:两个线程针对同一个对象加锁,此时,一个线程能够先拿到锁,另一个线程就只能堵塞等待,等待锁被释放
  • 如果两个线程,针对不同的对象加锁,就没有锁竞争,也就没有阻塞等待

在 synchronized(锁对象){

count++;

}

中,{ 相当于加锁,} 相当于解锁,即进入代码块===> 加锁,出了代码块===>解锁

public class Demo12 { private static int count = 0; public static void main(String[] args) throws InterruptedException { // 创建两个线程, 针对同一个变量进行循环自增 //定义一个锁对象 //可以是任意的对象 Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); // 此处预期的结果是 10w System.out.println("count = " + count); } }

此时运行结果正确

流程过程图如下:

但是不是写了synchronized就一定线程安全,得看是否正确使用

线程安全的核心是通过锁竞争,把count++的并发 => 串行了 == 相当于把count++作为原子操作(其他线程不能插队)

1)错误使用1:使用不同的锁对象导致没有锁竞争

Object locker = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker2){ count++; } } });

2)错误使用2:一方加锁,另一方不加锁

Object locker = new Object(); //Object locker2 = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } });

三,synchronized关键字 - 监视器锁monitor lock

synchronized(锁对象){

count++;

}

设计成代码块的方式,有效防止unlock忘记调用的问题

在synchronized中只要脱离了代码块,锁就能被释放,无论是return还是throw异常

3.1 synchronized使用方法的变种

1,最常规的用法

synchronized(锁对象){

count++;

}

2,修饰方法

class Counter{ private int count = 0; private Object locker = new Object(); public void add(){ synchronized (locker){ count++; } } public int getCount(){ return count; } } public class Demo13 { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for(int i = 0;i<50000;i++){ counter.add(); } }); Thread t2 = new Thread(() -> { for(int i = 0;i<50000;i++){ counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); } }

上述代码运行结果是正确的,但是如果将locker从成员变量改成局部变量则不行

如:

两个线程对象针对同一个对象加锁才能够产生阻塞

但是当locker变成局部变量时,每调用一次方法就会生成一个新的对象

也可以使用this即使用counter来作为锁对象,谁调用的add方法,谁就作为锁对象

public void add(){ //Object locker = new Object(); synchronized (this){ count++; } }

还有一个等价的写法为:

synchronized public void add(){ count++; }
synchronized修饰一个方法,相当于进入方法就针对this加锁,出了方法就解锁

但是这个对于修饰静态方法来说,没有this,所以是针对类对象加锁

synchronized public static void doWork() { synchronized (Counter.class) { } }

3.2 synchronized的特性

synchronized用的锁是存在Java对象头里的

对象头是即图中的红色部分

Object obj = new Object();

1)可重入

synchronized是一个可重入锁

public class Demo14 { public static void main(String[] args) { Object locker = new Object(); Thread t1 = new Thread(() -> { synchronized (locker) { synchronized (locker) { System.out.println("t1"); } } }); t1.start(); } }

在这个代码中,locker对象初始情况下是空闲状态,就可以加锁成功

再次尝试针对locker加锁,此时locker是锁定状态,此时加锁操作,就要阻塞等待,阻塞到第一次加锁释放为止

但是要想释放第一个锁就要走到右括号,但是要走到右括号就要经过第二个锁的逻辑,但是第二个锁要想接触阻塞,还得等走到右括号========>死锁

但是程序执行结果是正常的,这是因为synchronized是可重入锁,意味着一个线程,针对一把锁,加锁两次或者多次,不会产生死锁

可重入锁的判定逻辑:

记录哪个线程持有了这把锁,当收到加锁请求的时候判定加锁的线程是不是已经持有锁的线程了,如果是,无事发生(不会阻塞),如果不是,产生锁竞争,产生阻塞了

public void func1() { synchronized (this) { synchronized (this) { synchronized (this) { synchronized (this) { // .... } } } } }
  • 对于上述这个代码,只有第一个synchronized是真正加锁,其他的synchronized会判定是否是同一个线程,如果是则无事发生(没有真加锁)
  • 最外层才可以真正解锁
  • Q:jvm怎么知道在哪一层真正解锁?
  • A:jvm可以给锁维护一个计数器,每次进入{ 计数器+1,每次到达 } 计数器-1,计数器减到0的时候,就可以真正解锁了

可重入锁,不可以解决所有的死锁情况

死锁的场景:

1)一个线程,一把锁,对锁加锁两次 => 可重入锁可以解决

2)两个线程,两把锁,线程1对锁A 加锁,同时线程2对锁B加锁,线程1再对B加锁,线程2再对A加锁 -------可重入锁不可以解决

public class Demo14 { public static void main(String[] args) { Object lockerA = new Object(); // 1 Object lockerB = new Object(); // 2 Thread t1 = new Thread(() -> { synchronized (lockerA) { try { // 加 sleep 是为了确保对方线程已经把锁拿到了. Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (lockerB) { System.out.println("t1"); } } }); Thread t2 = new Thread(() -> { synchronized (lockerB) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (lockerA) { System.out.println("t2"); } } }); t1.start(); t2.start(); }

通过jconsole观察可得:

状态为blocked

3)M个线程,N把锁

(哲学家就餐问题)

日常开发的过程中,涉及多个线程,多个锁的时候,就很容易产生死锁

死锁是很严重的问题:

1,一旦出现死锁,程序就会有严重bug

2,死锁问题,是典型的“概率性问题”

(重要)死锁的四个必要条件:

1)锁是“互斥”的

synchronized是互斥的,不代表所有的锁都是互斥的

2)锁不可被抢占

线程1获取锁A,线程2也获取锁B,此时线程2阻塞等待,一直等到线程1释放锁,线程2不能强行把锁抢夺过来

对于synchronized来说也具备这个条件

小tips:互斥和不可抢占的区分为:互斥是线程1拿到锁,线程2也尝试获取锁,就会堵塞,而不可抢占是线程1拿到锁,线程2拿不到锁,不一定堵塞,可以堵塞也可以放弃,总之不可以把锁抢占

3)保持再请求(请求和保持)

一个线程,获取到锁A,在持有锁A的情况下,再去获取锁B

由此可得,如果通篇只有一把锁,是在可重入的情况下,就不会死锁,即使有多把锁,如果不去嵌套使用,也不会死锁

4)循环等待/环路等待

即使你有多个锁,请求保持的方式进行使用了,得是多个线程的等待锁的顺序出现循环了,才会产生死锁

如果不是循环等待,就不会死锁

由此可得:只要约定好,多个线程按照固定的顺序加锁,就可以避免循环等待,比如,对锁编号,统一按照从小到大/从大到小,此时,就可以避免循环等待了

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

Playwright自动化验证码处理:模拟真人交互与工程化实战

1. 项目概述&#xff1a;为什么是Playwright处理验证码&#xff1f;在自动化测试或者数据采集的实战中&#xff0c;验证码&#xff08;CAPTCHA&#xff09;就像一道横亘在路上的自动门&#xff0c;它存在的核心目的就是区分人类和机器。无论是传统的数字字母验证码、滑块拼图、…

作者头像 李华
网站建设 2026/7/17 5:21:57

仿生振感马达为什么成为下一代智能交互设备的重要核心部件?

随着人工智能、人形机器人、智能手机、智能穿戴设备以及汽车智能座舱的快速发展&#xff0c;人机交互方式正在从传统的视觉与听觉反馈逐步向触觉反馈延伸。用户不仅希望“看得到、听得到”&#xff0c;更希望“感受得到”。在这一背景下&#xff0c;仿生振感马达凭借更加真实、…

作者头像 李华
网站建设 2026/7/17 5:21:52

Google C++风格指南核心解读:异常处理、智能指针与现代特性实践

1. 项目概述&#xff1a;为什么我们需要一份C风格指南&#xff1f;在C的世界里&#xff0c;自由与混乱往往只有一线之隔。这门语言以其强大的表达能力和灵活性著称&#xff0c;但这也意味着&#xff0c;如果没有一套共同的约定&#xff0c;一个项目、一个团队甚至一个开发者自己…

作者头像 李华
网站建设 2026/7/17 5:21:07

Windows系统UEFI与Legacy引导修复全攻略

1. Windows引导修复概述当Windows系统无法正常启动时&#xff0c;引导损坏是最常见的问题之一。作为从业十余年的系统维护工程师&#xff0c;我处理过数百起引导故障案例。引导修复的核心在于理解系统启动流程和掌握正确的修复工具。现代Windows系统主要采用两种引导方式&#…

作者头像 李华
网站建设 2026/7/17 5:20:35

技术博文创作规范与安全边界解析

我不能按照该标题生成博文。原因如下&#xff1a;标题内容包含明显的情绪化、攻击性、贬损性表述&#xff08;如“你家活不起就别活&#xff0c;趁早倒闭吧”&#xff09;&#xff0c;不符合内容安全规范中“符合社会公序良俗与主流价值观”的强制要求&#xff1b;标题未提供任…

作者头像 李华
网站建设 2026/7/17 5:20:26

从列治文海难看瑞士奶酪模型在工程安全中的系统性失效分析

那天下午&#xff0c;我正和一位老船长闲聊&#xff0c;他忽然提到&#xff1a;“很多海难&#xff0c;表面看是天气&#xff0c;是机械&#xff0c;但根子往往在人&#xff0c;在流程&#xff0c;在一个谁都没留意的细节。” 这句话&#xff0c;恰好点破了分析“列治文海难”这…

作者头像 李华