当前位置: 首页 > news >正文

多线程.下

目录

1.线程等待

2.join()介绍

3.获取当前对象引用

4.线程的状态

5.线程安全

6.synchronized()关键字

7.synchronized关键字底层介绍


1.线程等待

对于操作系统而言,内部多个线程的执行是“随机调度,抢占式执行”的。简而言之线程的等待是在确定线程的“结束顺序”。在操作系统中,虽然无法确定哪个线程执行的顺序以及执行的频率,但是可以控制哪个线程先结束。

例如现有两线程A,B;若果在A线程中调用B线程,B.join();

意思就是就是控制A线程等待B线程执行结束后再执行。哪个线程线程调用join();哪个线程就先执行。

如举例所示:在 t.start(); 之后本来应该两个线程一起执行的,但是经过 t.join();main 线程发生堵塞,只有 t 线程执行,等到 t 结束后,join才会返回,main线程接着执行。

调整一下,如果先让 t 结束,然后main才开始执行,这个时候才开始join() 是否main会堵塞呢

可以发现,如果 t 线程已经执行结束,主线程再次调用 t.join(); main线程也不会发生阻塞,因为join();就是为了确保能够先结束,如果已经在join();之前结束,join()就不必再等待了。

注意:此处仅仅是main线程等待t1,t2,但是t1,t2之间没有等待关系

总结:任何线程之间都是可以相互等待的,并不是只能主线程等待别人,线程等待并不是两个线程之间,一个线程可以等待多个线程。


2.join()介绍
方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束最多等待millis毫秒
public void join(long millis,int nanos)同理,但是精度更高

public void join(long millis) :millis十毫秒级的时间,设置等待时间一旦超过时间就会自己结束

public void join(long millis,int nanos):一个时间的范围,nanos是更高精度的纳秒。一般精度只到ms级别,再往上计算机就很难做到了。精度越高,开销就越大。

join();方法有多种重载的方法,上一标题介绍的是无参数版本无参数就意味着没有等待时间限制,如果被等待的线程发生阻塞结束不了,那么join();就会一直等待。这是一个十分危险的操作。

用于军工领域的计算机系统“实时操作系统”,可以把调度的开销降到很低但是符合一定误差要求,从而获取更高精度。舍弃很多功能换来了高精度实时性


3.获取当前对象引用

在某个线程中,想要获取自身的Thread对象的引用,就可以使用currentThead()方法来获取。

在其他线程中想要调用main线程,不采用currentThread();好像很难获取main线程的引用,想要获取当前线程的引用只需要在当前线程调用currentThread即可。

任何线程中都可以通过这样的操作,拿到线程的引用,线程的终止也是通过调用当前线程的引用里的方法: Thread.currentThread.isInterruptted()  


调用Thread.sleep(); 方法让线程阻塞等待,是具有一定时间的。

线程执行sleep,就会使线程不参与cpu调度,从而把cpu的资源让出来给别的操作使用。

这样的sleep操作称为“放权” 操作。在有的场景中,发现某个线程占用的cpu资源很高但是又使用的很少就可以通过sleep休息短暂的时间来改善

线程的优先级也可以产生此影响,但是影响是有限的。通过sleep更加明显的影响到cpu占用。


4.线程的状态

java中对于线程的状态做出了更明晰的划分,不只有阻塞和就绪两种状态。

1.NEW:当你使用 new Thread() 创建了一个线程对象,但还没有调用 start() 方法时,线程处于 NEW 状态。

2.TERMINATED:当线程执行完 run() 方法,或者在 run() 方法中抛出未捕获的异常,它就会进入 TERMINATED 状态。

3.RUNNABLE:当你调用了 start() 方法后,线程就会进入 RUNNABLE 状态,它表示线程已准备就绪,等待被操作系统调度。

4.BLOCKED: 一个进程试图获取其他进程所持有的锁时,他就会处于这个状态。直到这个锁释放

5.TIMED_WAITING:有具体的时间等待

6.WAITING:没有具体的时间等待


5.线程安全

举例:多个线程同时执行一个代码的时候可能会引起一些bug,理解线程安全是解决或者避免bug的关键。

上述代码并没有正确相加得出100000,下面分析原因。

此处的count++,在cpu看来是3个指令(下面的指令在不同编译器中写法不同)

1.把内存的数据读取到cpu寄存器里:load

2.把cpu寄存器的数据+1                  :add

3.把寄存器里的值写回内存             :save

但是因为是三个指令,cpu会出现只执行了其中一个或者两个,剩下的指令就会被调度走。这样就会容易出现bug

根本原因是因为:

1.线程在操作系统中随机调度,抢占式执行,

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

3.修改的操作不是“原子”的(原子的:不可分割的最小单位),在cpu的视角,一条指令就是不可分割的最小单位,cpu在切换线程的时候只能确保执行完一条指令。

4.内存可见性,指令重排(下节介绍)

只有第一张图中的两种方式是正确执行的,第二张图就是错误的。指令的调度有很多种顺序,除了第一张图中的两种顺序是正确的,其他任何执行顺序得到的结果都是不正确的。

如何解决上述问题?

那就要从原因下手了,线程在操作系统的随机调度抢占式执行是很难干涉的,其次如果多个线程能同时修改同一变量也是不可控制的,因为这也是操作系统多线程的特性。但是如果修改的操作不是原子的就可以。使分开执行的指令一次性执行完就好了。

比如count++中,让数据的读取,修改,写回内存都是原子性的,其他线程的指令插入不进来就可以了。

6.synchronized()关键字

这个关键字后面的()并非填的是”参数“,而是填入的是一个指定的锁对象,通过锁对象来进行判定,锁对象可以是任何对象。

{} 内部就是要一同执行的整体,在执行的时候,其他线程的代码的逻辑插入不进来。

值得注意的是,想要针对修改同一变量的线程加锁,这些线程所持有的锁对象必须一致是同一对象。不然加锁无效!!

由于t1和t2 都是针对locker对象加锁的,t1先加锁成功,t1就直接执行 {} 里的代码

t2也加锁了,但是比t1慢上一步,当t2发现对象已经被别人先锁起来了,那么t2只能等到t1 的{}执行结束释放锁后,t2再加锁。

又因释放锁unlock一定是在save之后,确保了t1的count++的结果可以正确写入内存,两者的count++不会穿插执行,也就不会覆盖掉对方的结果了。

加锁本质上是把 局部随机并发执行的代码 强行变成了串行,从而解决线程安全问题。

注意:

1.锁对象的作用是区分两个线程或者多个线程是否针对同一个对象加锁。都是同一对象的锁就会出现“阻塞”(锁竞争)。所加的锁不是同一对象那么多个线程还是并发执行。

2.锁对象必须是对象,是引用类型Object类或者其子类,不能使int,double这种内置类型。

3.加锁后代码只是局部代码穿行,但效率依然比join要快。只有锁里面的是串行,其他部分代码不影响并发执行。


7.synchronized关键字底层介绍

synchronized()关键字是jvm提供的功能,底层实现就是通过C++代码编写的,也是依靠操作系统提供的API实现的加锁,操作系统的API是来自由于cpu上支持特殊的指令实现的

操作系统原生的API就是两个函数lock()/unlock(),大多数编程语言是类似于封装的方式来使用这两个函数,但是java中直接通过一个关键字来同时完成加锁解锁,这样的好处是在编写代码的时候最后很有可能会忘记unlock解锁,这个关键字会自动替你解锁。就算直接trturn也会帮你释放锁再return。

synchronized()里面可以是任何对象,最偷懒的写法就是直接某个类.class(类对象),但是偷懒需要付出的代价就是代码效率会降低。

一个类对象可以获取到这个类里面的详细情况,包括但不限于类有哪些属性,方法,属性是什么类型,什么名字,方法是什么类型,返回类型,这个类实现了哪些接口等等。这就是反射,反射是一组API可以对上述信息获取或者修改

synchronized 还可以修饰一个方法:

如果修饰类方法就没有this,就是直接给类对象加锁。

注意:在多线程中并非就是写了synchronized就是安全的,还要看具体代码怎么写,是否要加synchronized是要看具体场景。

比如StringBuffer,Vector,Hashtable都不推荐,因为加了太多的锁,会导致代码效率降低。


总结:synchronized的几种使用方式:

1.synchronized(){ };圆括号指定锁对象

2.synchronized 修饰一个普通方法相当于针对this加锁。

3.synchronized 修饰一个静态方法,相当于对类对象加锁。

可以把任意Object子类或者Object类的对象作为锁对象,锁对象是什么不重要,重要的是多个线程的对象是否同一个,是同一个才会出现锁竞争。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 昇思25天学习打卡营第29天 | 基于MindSpore通过GPT实现情感分类
  • Stable Diffusion 使用详解(1)---- 提示词及相关参数
  • 提交(git-add git-commit git-push)
  • 第十课:telnet(远程登入)
  • Mysql-索引结构
  • 数据结构——线性表(循环链表)
  • 【JVM实战篇】内存调优:内存泄露危害+内存监控工具介绍+内存泄露原因介绍
  • Spring Boot 中,监听应用程序启动的生命周期事件的4种方法
  • ubuntu2204配置anacondacuda4090nvidia驱动
  • 【C#】| 与 及其相关例子
  • [Doris]阿里云搭建Doris,测试环境1FE 1BE
  • k8s学习笔记——dashboard安装
  • KAFKA搭建教程
  • 国产麒麟、UOS在线打开pdf加盖印章
  • C语言:键盘录入案例
  • 【跃迁之路】【699天】程序员高效学习方法论探索系列(实验阶段456-2019.1.19)...
  • chrome扩展demo1-小时钟
  • java第三方包学习之lombok
  • Kibana配置logstash,报表一体化
  • mysql常用命令汇总
  • overflow: hidden IE7无效
  • Python 反序列化安全问题(二)
  • Rancher如何对接Ceph-RBD块存储
  • ReactNative开发常用的三方模块
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • Zsh 开发指南(第十四篇 文件读写)
  • 创建一种深思熟虑的文化
  • 从PHP迁移至Golang - 基础篇
  • 分布式任务队列Celery
  • 关于springcloud Gateway中的限流
  • 开源地图数据可视化库——mapnik
  • 前端之Sass/Scss实战笔记
  • 如何设计一个比特币钱包服务
  • 微服务入门【系列视频课程】
  • 用简单代码看卷积组块发展
  • AI算硅基生命吗,为什么?
  • ​520就是要宠粉,你的心头书我买单
  • ​LeetCode解法汇总2583. 二叉树中的第 K 大层和
  • ​一、什么是射频识别?二、射频识别系统组成及工作原理三、射频识别系统分类四、RFID与物联网​
  • # 达梦数据库知识点
  • #Datawhale X 李宏毅苹果书 AI夏令营#3.13.2局部极小值与鞍点批量和动量
  • #pragma 指令
  • #我与Java虚拟机的故事#连载05:Java虚拟机的修炼之道
  • $(selector).each()和$.each()的区别
  • (1)虚拟机的安装与使用,linux系统安装
  • (145)光线追踪距离场柔和阴影
  • (33)STM32——485实验笔记
  • (9)STL算法之逆转旋转
  • (delphi11最新学习资料) Object Pascal 学习笔记---第14章泛型第2节(泛型类的类构造函数)
  • (Python) SOAP Web Service (HTTP POST)
  • (zt)基于Facebook和Flash平台的应用架构解析
  • (二)c52学习之旅-简单了解单片机
  • (二)延时任务篇——通过redis的key监听,实现延迟任务实战
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (附源码)ssm考生评分系统 毕业设计 071114