Java并发编程实战_Java并发编程实战(一:理论基础_下)

一,管程

1,概念:管程,是指管理共享变量和对共享变量的操作过程,让它们支持并发,是解决并发问题的银弹。在JDK 1.5前,Java并没有提供类似信号量的编程原语,这是因为synchronized + wait + notify + notifyAll构成的管程,和信号量是等价的,它们可以互相实现,但管程更易理解和使用,所以Java选择了管程。

2,模型:在管程的发展史上,前后主要有3种模型:Hasen模型,Hoare模型和MESA模型,现在用得最多的就是MESA模型。在MESA模型中,wait等待要放在while循环对条件的判断内部,因为当线程从wait中醒来时,使它唤醒的条件可能又不满足了。

3,方案:本质上就是在《并发基础-上篇》中讨论过的“等待-通知”机制。

3.1,管程解决互斥的思路是:将共享变量及共享变量的操作统一封装起来,保证多线程要访问一个共享变量,只能通过这个封装后的公共入口。

3.2,管程解决同步的思路是:管程首先在封装方法的入口处加锁,每个锁上都有一个锁的等待队列,在管程中可以认为是入口等待队列;其次在封装方法内部,引入条件变量的概念,同时,对每个条件变量都内置一个等待队列。

3c691187d6f13191cdf602ae4efe15dd.png

二,线程的生命周期

1,通用版本:通用的线程生命周期有5个状态:

1.1初始状态:线程在编程语言层面被创建,系统内核层并未创建:new thread;

1.2,可运行状态:线程在操作系统层面被创建,可以被分配CPU开始执行,但需要等待CPU空闲:thread.start;

1.3,运行状态:线程已经被分配好CPU,开始执行;

1.4,休眠状态:线程调用阻塞API(阻塞IO)或者等待某个事件(条件变量),线程释放CPU并进入休眠,当API返回我等待的事件出现,回到可运行状态,继续等待被分配CPU运行;

1.5,终止状态:线程执行完或发生异常退出,线程生命周期结束。

2,Java版本:Java线程合并了运行/可运行状态,细化了休眠状态,总计有6个状态:

2.1,初始状态(NEW):同1.1;

2.2,可运行/运行状态(RUNNABLE):同1.2和1.3,但是需要注意,对JVM来说,等待CPU还是等待IO都是等待系统资源,虽然在系统内核层面,调用阻塞API等待IO时,系统休眠,但对Java来说,仍然处于RUNABLE状态;

2.3,阻塞状态(BLOCKED):线程等待各种锁的状态,从RUNNABLE进入BLOCKED;

2.4,永久等待(WAITING):线程在临界区中调用无参数的,比如,wait,join,park等函数,从RUNNABLE进入WAITING;

2.5,有时限等待(TIMED_WAITING):线程在临界区中调用带超时参数的,比如,sleep,wait,join等函数,从RUNNABLE进入TIMED_WAITING;

2.6,终止状态(TERMINATED):同1.5,正常退出或使用interrupt中断线程等。线程A在阻塞或者等待状态时,如果其他线程B调用A线程的interrupt方法,会使A立即进入RUNNABLE并触发InterruptedException异常。注意,当触发InterruptedException异常后,线程A的interrupt标志会被清除,此时调用A.isInterrupted返回false。

三,线程数选择

在并发编程领域,提升性能的本质就是提升硬件的利用率,主要是IO和CPU的综合利用率,将硬件性能发挥到极致。理论上的最佳线程数 = CPU核数 * [1 + (1O耗时/CPU耗时)]。

四,局部变量的线程安全

1,CPU和堆栈寄存器:访问共享变量时,会产生线程安全问题,那线程内的局部变量呢?在CPU层面,是没有方法概念的,程序只是一条条可执行的指令,而编译器的目标是将高级语言里的方法翻译为CPU能看懂的执行指令。

因此,CPU执行方法时,存储当前地址到堆栈寄存器,并跳到方法所在的代码段地址上开始执行,执行完成后,再取出寄存器里的地址,跳回函数调用的地方继续执行。

2,方法和栈帧:在程序运行时,每个方法都有自己的独立空间,称为栈帧,存有方法需要的参数和返回地址。根据调用链的关系,每个栈帧都被先入后出的存在调用栈里。当调用方法时,创建新的栈帧并压入调用栈,当方法返回时,栈帧就会被自动弹出,栈帧和方法是同生共死的。这样,同样与方法同生共死的局部变量,自然也被放在了栈帧上。

值得注意的是,对Java而言,new出来的对象,对象数据存放在堆里,但对象引用却是栈上的局部变量,如果该变量没有从方法里逸出,方法返回时,对象数据的唯一对象引用就被消除,堆上的数据也就成了垃圾清理的目标数据,从逻辑上,不会逸出的new对象,同样也是局部变量。

3,线程和调用栈:每个线程有自己的调用栈。结合1和2,在方法内部的局部变量,对每个线程而言都是独立存在且线程安全的。

e0a6f21396327b600fa6de9f5813b710.png

4,线程封闭:鉴于线程内部的局部变量都是线程安全的,所以将数据当做线程内部的局部变量,也成为解决并发问题的重要技术,比如,ThreadLocal。

五,使用面向对象写好并发程序

并发编程的核心问题,解决多线程环境下同时访问共享变量的问题。因此,用面向对象的方法写好并发程序的思路也很简单:将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略。

对于初始化后就不会再发生变化的共享变量,可以用final关键字来修饰。同时,注意共享变量间的约束条件,需要特别关注不同共享变量间,体现约束条件的if-else代码的竞态条件。

在编写并发程序时,优先使用成熟的工具类,不到万不得已,尽量避免使用低级的同步原语,更重要的是,安全第一,在系统设计和实现的早期阶段尽量先保证程序安全,再出现性能瓶颈后再考虑优化,避免过早进入到优化的泥潭中。

六,并发编程基础篇思维导图

9e7fa0696c7be167cb323d3bc0551fbd.png

七,参考文章

1,极客时间:Java并发编程实战 — 王宝令:(并发理论基础8-13讲)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值