您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
进程 线程 多线程 同步锁 线程安全解决方案
 
作者:1只少年
   次浏览      
 2023-5-11 
 
编辑推荐:
本文主要介绍了进程 线程 多线程 同步锁 线程安全解决方案等相关内容。希望对你的学习有帮助。
本文来自CSDN,由Linda编辑、推荐。

1 进程

1.1概念:

进程就是正在运行的程序,它代表了程序所占用的内存区域。

1.2 进程的特点:

独立性

进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

动态性

进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的。

并发性

多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响。

1.3 并行和并发:

HA(High Availability)高可用:指在高并发的情景中,尽可能的保证程序的可用性,减少系统不能提供服务的时间

2 线程

2.1 线程的概念

线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.

一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程

我们看到的进程的切换,切换的也是不同进程的主线程

多线程扩展了多进程的概念,使的同一个进程可以同时并发处理多个任务

2.2 进程与线程的关系

一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)

每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间.

所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成

3 多线程的特性

3.1 随机性

线程的随机性指的是同一时刻,只有一个程序在执行

我们宏观上觉得这些程序像是同时运行,但是实际上微观时间是因为CPU在高效的切换着,这使得各个程序从表面上看是同时进行的,也就是说,宏观层面上,所有的进程/线程看似同时运行,但是微观层面上,同一时刻,一个CPU只能处理一件事.切换的速度甚至是纳秒级别的,非常快

3.2 CPU分时调度

时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间,如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。

注意:我们无法控制OS选择执行哪些线程,OS底层有自己规则,如:

1.FCFS(First Come First Service 先来先服务算法)

2.SJS(Short Job Service短服务算法)

3.3 线程的状态

由于线程状态比较复杂,我们由易到难,先学习线程的三种基础状态及其转换,简称”三态模型” :

就绪(可运行)状态:线程已经准备好运行,只要获得CPU,就可立即执行

执行(运行)状态:线程已经获得CPU,其程序正在运行的状态

阻塞状态:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞

就绪 → 执行:为就绪线程分配CPU即可变为执行状态"

执行 → 就绪:正在执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态

执行 → 阻塞:由于发生某事件,使正在执行的线程受阻,无法执行,则由执行变为阻塞

(例如线程正在访问临界资源,而资源正在被其他线程访问)

反之,如果获得了之前需要的资源,则由阻塞变为就绪状态,等待分配CPU再次执行

我们可以再添加两种状态:

创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中

终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统

PCB(Process Control Block):为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程

3.4 线程状态与代码对照

线程生命周期,主要有五种状态:

1.新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();

2.就绪状态(Runnable):当调用线程对象的start()方法,线程即为进入就绪状态.

处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行

3.运行状态(Running):当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态

就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态

4.阻塞状态(Blocked):处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.

根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:

等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态

同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态

其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态

5.死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期

4 多线程代码创建方式1:继承Thread

4.1 概述

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例

启动线程的唯一方法就是通过Thread类的start()实例方法

start()方法是一native方法,它将通知底层操作系统,.最终由操作系统启动一个新线程,操作系统将执行run()

这种方式实现的多线程很简单,通过自己的类直接extends Thread,并重写run()方法,就可以自动启动新线程并执行自己定义的run()方法

模拟开启多个线程,每个线程调用run()方法.

4.2 常用方法

构造方法

  1. Thread() 分配新的Thread对象
  2. Thread(String name) 分
    配新的Thread对象
  3. Thread(Runnable target)
    分配新的Thread对象
  4. Thread(Runnable
    target,String name)
    分配新的Thread对象

 

普通方法

  1. static Thread currentThread( )
  2. 返回对当前正在执行的线程对象的引用
  3. long getId()
  4. 返回该线程的标识
  5. String getName()
  6. 返回该线程的名称
  7. void run()
  8. 如果该线程是使用独立的
    Runnable 运行对象构造的
    ,则调用该 Runnable 对象的 run 方法
  9. static void sleep(long millions)
  10. 在指定的毫秒数内让当
    前正在执行的线程休眠(暂停执行)
  11. void start()
  12. 使该线程开始执行:Java
    虚拟机调用该线程的run()

 

4.3 测试多线程的创建方式1

思路一:继承

1.自定义一个类extends Thread

2.重写run()里面写业务

3.创建线程对象

4.调用start()

注意:可以通过调用父类Thread的含参构造Thread(String name)

给自定义线程对象起名字,调用方式:super(name);

5 多线程代码创建方式2:实现Runnable接口

5.1 概述

如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口

5.2 常用方法

void run()使用实现接口Runnable的对象创建线程时,启动该线程将导致在独立执行的线程中调用对象的run()方法

5.3 练习2:测试多线程的创建方式2

思路二:实现

1.自定义一个类implements Runnable

2.实现接口中未实现的run()

3.打印线程名称:Thread.currentThread().getName()

4.创建目标业务对象–接口实现类的对象–包含的是我们的业务

5.创建线程对象–Thread t1 = new Thread(target);

目的:为了把实现类与Thread建立关系,原因是想用Thread的start()

6.通过线程对象调用start(),把线程对象加入就绪队列

5.4 两种实现方式的比较

继承Thread类

优点: 编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程

缺点: 自定义的线程类已继承了Thread类,所以后续无法再继承其他的类

实现Runnable接口

优点: 自定义的线程类只是实现了Runnable接口或Callable接口,后续还可以继承其他类,在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想

缺点: 编程稍微复杂,如想访问当前线程,则需使用Thread.currentThread()方法

6 售票案例

需求:设计4个售票窗口,总计售票100张。用多线程的程序设计并写出代码

6.1 方案1:继承Thread

6.2 方案2:实现Runnable

6.3 问题

1.每次创建线程对象,都会生成一个tickets变量值是100,创建4次对象就生成了400张票了。不符合需求,怎么解决呢?能不能把tickets变量在每个对象间共享,就保证多少个对象都是卖这100张票。

解决方案: 用静态修饰

2.产生超卖,0 张 、-1张、-2张。

3.产生重卖,同一张票卖给多人。

4.多线程安全问题是如何出现的?常见情况是由于线程的随机性+访问延迟。

5.以后如何判断程序有没有线程安全问题?

在多线程程序中 + 有共享数据 + 多条语句操作共享数据

同步锁-线程安全问题解决方案

7 同步锁

7.1 前言

经过前面多线程编程的学习,我们遇到了线程安全的相关问题,比如多线程售票情景下的超卖/重卖现象.

我们如何判断程序有没有可能出现线程安全问题,主要有以下三个条件:

在多线程程序中 + 有共享数据 + 多条语句操作共享数据

多线程的场景和共享数据的条件是改变不了的(就像4个窗口一起卖100张票,这个是业务)

所以思路可以从第3点"多条语句操作共享数据"入手,既然是在这多条语句操作数据过程中出现了问题

那我们可以把有可能出现问题的代码都包裹起来,一次只让一个线程来执行

7.2 同步与异步

那怎么"把有可能出现问题的代码都包裹起来"呢?我们可以使用synchronized关键字来实现同步效果

也就是说,当多个对象操作共享数据时,可以使用同步锁解决线程安全问题,被锁住的代码就是同步的

接下来介绍下同步与异步的概念:

同步:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权利的线程排队。

坏处就是效率会降低,不过保证了安全。

异步:体现了多线程抢占资源的效果,线程间互相不等待,互相抢占资源。

坏处就是有安全隐患,效率要高一些。

7.3 synchronized同步关键字

7.3.1 写法

  1.  synchronized (锁对象){
  2. 需要同步的代码(也就是可能出
    现问题的操作共享数据的多条语句);
  3. }

 

7.3.2 前提

同步效果的使用有两个前提:

前提1:同步需要两个或者两个以上的线程(单线程无需考虑多线程安全问题)

前提2:多个线程间必须使用同一个锁(我上锁后其他人也能看到这个锁,不然我的锁锁不住其他人,就没有了上锁的效果)

7.3.3 特点

1.synchronized同步关键字可以用来修饰方法,称为同步方法,使用的锁对象是this

2.synchronized同步关键字可以用来修饰代码块,称为同步代码块,使用的锁对象可以任意

3.同步的缺点是会降低程序的执行效率,但我们为了保证线程的安全,有些性能是必须要牺牲的

4.但是为了性能,加锁的范围需要控制好,比如我们不需要给整个商场加锁,试衣间加锁就可以了

为什么同步代码块的锁对象可以是任意的同一个对象,但是同步方法使用的是this呢?

因为同步代码块可以保证同一个时刻只有一个线程进入

但同步方法不可以保证同一时刻只能有一个线程调用,所以使用本类代指对象this来确保同步

7.4.1练习-改造售票案例

  1. package cn.tedu.tickets;
  2. /*本类用于改造多线程售
    票案例,解决数据安全问题*/
  3. public class TestRunnableV2 {
  4. public static void
    main(String[] args) {
  5. //5.创建目标业务类对象
  6. TicketR2 target
    = new TicketR2();
  7. //6.创建线程对象
  8. Thread t1 = new
    Thread(target);
  9. Thread t2 =
    new
    Thread(target);
  10. Thread t3 =
    new Thread(target);
  11. Thread t4 =
    new Thread(target);
  12. //7.以多线程的方式运行
  13. t1.start();
  14. t2.start();
  15. t3.start();
  16. t4.start();
  17. }
  18. }
  19. /*1.多线程中出现数据安全问题的
    原因:多线程程序+共享数据+
    多条语句操作共享数据*/
  20. /*2.同步锁:相当于给容易出现问
    题的代码加了一把锁,包裹了所有
    可能会出现数据安全问题的代码
  21. * 加锁之后,就有了同步(排队)的效
    果,但是加锁的话,需要考虑:
  22. * 锁的范围:不能太大,太大,
    干啥都得排队,也不能太小,太
    小,锁不住,还是会有安全隐患*/
  23. //1.创建自定义多线程类
  24. class TicketR2 implements Runnable {
  25. //3.定义成员变量,保存票数
  26. int tickets = 100;
  27. //创建锁对象
  28. Object o = new Object();
  29. //2.实现接口中未实现的
    方法,run()中放着的是我们的业务
  30. @Override
  31. public void run() {
  32. //4.通过循环结构完成业务
  33. while (true) {
  34. /*3.同步代码
    块:synchronized
    (锁对象){会出现安全隐患的所有代码}
  35. * 同步代码块在同
    一时刻,同一资源只会被一个线程独享*/
  36. /*这种写法不对,相当于
    每个线程进来的时候都会new一个锁
    对象,线程间使用的并不是同一把锁*/
  37. //synchronized (new Object()){
  38. //修改同步代码块的锁
    对象为成员变量o,因为锁对象必须唯一
  39. synchronized (o) {
    //同步代码块解决的是重卖的问题
  40. //如果票数>0就卖票
  41. if (tickets > 0) {
  42. try {
  43. Thread.sleep(10);
  44. } catch (InterruptedException e) {
  45. e.printStackTrace();
  46. }
  47. //4.1打印当前正在售票的线程名以及票数-1
  48. System.out.println
    (Thread.currentThread
    ().getName() + "=" + tickets--);
  49. }
  50. //4.2退出死循环--没票的时候就结束
  51. if (tickets <= 0) break;
  52. }
  53. }
  54. }
  55. }

 

7.4.2 练习-改造售票案例

  1. package cn.tedu.tickets;
  2. /*本类用于改造多线程售票
    案例,解决数据安全问题*/
  3. public class TestThreadV2 {
  4. public static void
    main(String[] args) {
  5. //5.创建多个线程对
    象并以多线程的方式运行
  6. TickectT2 t1 =
    ew
    TickectT2();
  7. TickectT2 t2 =
    new TickectT2();
  8. TickectT2 t3 =
    new TickectT2();
  9. TickectT2 t4
    = new TickectT2();
  10. t1.start();
  11. t2.start();
  12. t3.start();
  13. t4.start();
  14. }
  15. }
  16. //1.自定义多线程类
  17. class TickectT2 extends Thread {
  18. //3.新增成员变量用来保存票数
  19. static int tickets = 100;
  20. //static Object o = new Object();
  21. //2.添加重写的run()来完成业务
  22. @Override
  23. public void run() {
  24. //3.创建循环结构用来卖票
  25. while (true) {
  26. //Ctrl+Alt+L调整代码缩进
  27. //7.添加同步代码块,解决数据安全问题
  28. //synchronized (new Object()) {
  29. /*static的Object的对象o这种写法也可以*/
  30. //synchronized (o) {
  31. /*我们每通过class关键字创建一个
    类,就会在工作空间中生成一
    个唯一对应的类名.class字节码文件
  32. * 这个类名.class对应的对象我
    们称之为这个类的字节码对象
  33. * 字节码对象极其重要,是反射技
    术的基石,字节码对象中包含了
    当前类所有的关键信息
  34. * 所以,用这样一个唯一且明
    确的对象作为同步代码块
    的锁对象,再合适不过了*/
  35. synchronized
    (TickectT2.class) {/*比较标准的写法*/
  36. if(tickets > 0){
  37. //6.添加线程休眠,暴露问题
  38. try {
  39. Thread.sleep(10);//让线
    程休眠,增加线程状态切换的频率
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. //4.1打印当前正在售票的线程名与票数-1
  44. System.out.println(getName()
    + "=" + tickets--);
  45. }
  46. //4.2给程序设置一个出
    口,没有票的时候就停止卖票
  47. if (tickets <= 0) break;
  48. }
  49. }
  50. }
  51. }

 

注意:如果是继承的方式的话,锁对象最好用"类名.class",否则创建自定义线程类多个对象时,无法保证锁的唯一

7.5 之前遇到过的同步例子

StringBuffer JDK1.0

加了synchronized ,性能相对较低(要排队,同步),安全性高

StringBuilder JDK1.5

去掉了synchronized,性能更高(不排队,异步),存在安全隐患

8 线程创建的其他方式

8.1 ExecutorService/Executors

ExecutorService:用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理

execute(Runnable任务对象) 把任务丢到线程池

Executors 辅助创建线程池的工具类

newFixedThreadPool(int nThreads) 最多n个线程的线程池

newCachedThreadPool() 足够多的线程,使任务不必等待

newSingleThreadExecutor() 只有一个线程的线程池

8.2 练习:线程的其他创建方式

  1. public class TestRunnableV2 {
  2. public static void main(String[] args)
  3. //5.创建接口实现类对象作为
    目标对象(目标对象就是要做的业务)
  4. SaleTicketsV2 target
    = new SaleTicketsV2();
  5. //6.将目标对象与Thread线程对象绑定
  6. // Thread t1 = new Thread(target);
  7. // hread t2 = new Thread(target);
  8. // Thread t3 = new Thread(target);
  9. // Thread t4 = new Thread(target);
  10. //7.以多线程的方式启动线程--会将
    线程由新建状态变为就绪状态
  11. **1.如果只创建了一个线程对象,是单
    线程场景,不会出现数据问题*/
  12. // t1.start();
  13. // t2.start();
  14. // t3.start();
  15. // t4.start();
  16. /**7.线程池ExecutorService:用来存
    储线程的池子,把新建线程/启动线程/关
    闭线程的任务都交给池来管理*/
  17. /**8.Executors用来辅助创建
    线程池对象,newFixedThreadPool
    ()创建具有参数个数的线程数的线程池*/
  18. ExecutorService pool =
    Executors.newFixedThreadPool(5);
  19. for(int i = 0;i<5;i++) {
  20. /**9.excute()让线程池
    中的线程来执行任务,每
    次调用都会启动一个线程*/
  21. pool.execute(target);
    本方法的参数就是执行
    的业务,也就是实现类的目标对象
  22. }
  23. }

 

   
次浏览       
相关文章

一文了解汽车嵌入式AUTOSAR架构
嵌入式Linux系统移植的四大步骤
嵌入式中设计模式的艺术
嵌入式软件架构设计 模块化 & 分层设计
相关文档

企点嵌入式PHP的探索实践
ARM与STM简介
ARM架构详解
华为鸿蒙深度研究
相关课程

嵌入式C高质量编程
嵌入式操作系统组件及BSP裁剪与测试
基于VxWorks的嵌入式开发、调试与测试
嵌入式单元测试最佳实践

最新活动计划
C++高级编程 12-25 [线上]
白盒测试技术与工具实践 12-24[线上]
LLM大模型应用与项目构建 12-26[特惠]
需求分析最佳实践与沙盘演练 1-6[线上]
SysML建模专家 1-16[北京]
UAF架构体系与实践 1-22[北京]
 
 
最新文章
基于FPGA的异构计算在多媒体中的应用
深入Linux内核架构——简介与概述
Linux内核系统架构介绍
浅析嵌入式C优化技巧
进程间通信(IPC)介绍
最新课程
嵌入式Linux驱动开发
代码整洁之道-态度、技艺与习惯
嵌入式软件测试
嵌入式C高质量编程
嵌入式软件可靠性设计
成功案例
某军工所 嵌入式软件架构
中航工业某研究所 嵌入式软件开发指南
某轨道交通 嵌入式软件高级设计实践
深圳 嵌入式软件架构设计—高级实践
某企业 基于IPD的嵌入式软件开发
更多...