java内存模型与线程

这一篇和接下来我将要写的一篇文章都是java并发相关,但并不完全是JUC包中的内容了。实际上,它们都可算作是《深入理解jvm虚拟机》这本书中“高效并发”这一部分的笔记。它们从一个更加深入的角度理解线程和进程,原书非常非常地经典,值得仔仔细细反复研读。

硬件层面的一致性与java内存模型

回想一下我们学过的操作系统,和java内存模型,就会发现二者其实几乎是相同的。在硬件层面,多路处理器系统中,每个处理器都有着自己的高速缓存(cache或是多级cache),而它们又共享同一块主内存。这理所当然地引发了诸如 “如果我修改了我的cache里的数据,他修改了他的cache里的数据,而我们都想写回主内存,那么到底该写谁的cache数据呢?” 这样的问题。由此就引出了缓存一致性协议,就是让各个处理器读写时根据一定的规定进行操作,以此来保证不会乱套。这类协议有MSI、MESI(Illinois Protocol)、MOSI、 Synapse、Firefly及Dragon Protocol等。
而在java内存模型中,也是一样的,每个线程拥有自己的工作内存(类比于上面的处理器自己的高速缓存),而它们共享一块虚拟机的主内存(类比于上面的主内存),它们之间通过save和load操作进行交互。
在这里插入图片描述
在这里插入图片描述
是不是非常相像?那么上文提到的缓存一致性协议,在java内存模型中也应有着与之相对应的规则。

JVM中的一致性规则

java虚拟机定义了一大串的规则来确保一致性。首先是8种操作:
·lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
·unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
·read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
·load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
·use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
·assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
·store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
·write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
这其中,最重要的当然是read、load、store和write,它们之间也是有顺序的。比如必须read->load,store->write。除此之外,还有种种规则确保。
再然后,还有一些特殊规则:
volatile关键字——之前也介绍过了,有两种语义:一是保证内存可见性,二是阻止指令重排序(也就是阻止字节码优化,使得写在volatile前的指令,绝对不会跑到volatile后面去执行,类似内存屏障的实现)。
long和double型长变量(64位数据类型)——:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行, 即允许虚拟机实现自行选择是否 要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非原子性协定”。
这么一大堆的规则,其实都是围绕着三个特性展开的——原子性、可见性和有序性。

Java内存模型的三个特性和一个原则

原子性: 就是要么发生要么不发生,不会存在另一个线程读取到“发生到一半”的值的情况。基础类型访问一般都能保证原子性,另外如果需要更大范围的原子性,还提供了lock机制和synchronized关键字。
可见性: 就是一个值被改变后立即刷回主内存,让其他线程可见。典型的如上面提到的volatile,synchronized和final(final根本就不能改变当然可见了……啥同步都不需要)
有序性: 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的。前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。典型的就是volatile和synchronized分别保证的操作的有序性。
先行发生原则: 是Java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
有如下几个规则判断先行发生:
·程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行 发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循 环等结构。
·管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这 里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
·volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量 的读操作,这里的“后面”同样是指时间上的先后。
·线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
·线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检 测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止 执行。
·线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程 的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
·对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。
·传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出 操作A先行发生于操作C的结论。
这里注意时间上操作A是否比操作B先发生,跟操作A是否先行发生于操作B,没有任何的关系。 典型的例子有:1、A改变了一个值但没写回主内存,B读不到A改变的值,因此A没有先行发生于B;2、同一个线程中指令重排序,把B排到了A之前,因此程序次序上A先行发生于B,但时间上A晚于B。

Java中的线程

线程有三种实现:内核线程(1:1),用户线程(1:N),混合实现(N:M)。
内核线程(Kernel-Level Thread,KLT): 直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP), 轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1 的关系就称为一对一的线程模型。
在这里插入图片描述
用户线程(User Thread,UT):用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。 如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的, 也能够支持 规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
在这里插入图片描述
用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都 需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处 理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的。因为使用用户线程实现的程序通 常都比较复杂[1],除了有明确的需求外(譬如以前在不支持多线程的操作系统中的多线程程序、需要支持大规模线程数量的应用),一般的应用程序都不倾向使用用户线程
混合实现: 就是把上述两种方案混合在一起。如图:
在这里插入图片描述
java中的线程采用了第一种模式,即完全一比一映射到内核线程上,线程怎么调度,怎么分配时间片占用CPU资源,统统都是操作系统的事儿,和我java没有关系。但这只是主流的解决方式,还有一些特别的平台依然可以采用N:M的第三种方式进行执行。
虽然说Java线程调度是系统自动完成的,但是我们仍然可以“建议”操作系统给某些线程多分配一点执行时间,另外的一些线程则可以少分配一点——这项操作是通过设置线程优先级来完成的。 Java 语言一共设置了10个级别的线程优先级。但这样做也会带来一点小问题,如windows操作系统一共就只有七个优先级,这就导致必然有几个优先级映射到操作系统的同一个优先级上,导致优先级虽然不同,但事实上却还是“平等地”执行的。

Java中的线程有六种状态:New、Runnable、Waiting、Timed Waiting、Blocked、Terminated。其中Waiting和Timed Waiting可以对应操作系统五种线程状态中的“就绪态”,其余均相同。
在这里插入图片描述

Java中的协程

永远用一个虚拟机线程对应一个内核线程的模式,在很大的线程量之下很容易产生问题。如果只有几十个请求倒还好说,几百个呢?几万个呢?再多呢?线程之间的轮换开销不要面子的吗?这就产生了矛盾。
操作系统线程之间的轮换开销,主要来源就是保护和恢复现场的成本。把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时候的状态,这样的操作相当耗时。而协程就可以用来解决这个问题。
协程:在一个线程中模拟执行多个线程,由程序的开发者自己维护一系列调用栈。 它最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
关于协程更加详细的解释,可以参考廖雪峰老师的协程介绍文章。这里不再展开。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值