在前面已经看了java的内存模型,主要介绍的还是java运行时的内存结构,对于内存模型没怎么涉猎,正好在这里正好详细介绍java的内存模型,并且介绍一下线程,
1、主内存和工作内存
(1)模型
上面这个更详细一些,说明一下,这里的主内存你可以当成进程占领的内存单元,而Save和Load操作只是操作,也相当于主内存和工作内存之间的流程
java内存模型规定所有的变量都要存储在主内存中,每条线程都有一个自己的工作内存(保存了被该线程使用到的变量的主内存副本拷贝),线程对变量的操作都必须在工作内存中进行,而不能直接读写在工作内存,但是可以靠Save和Load等操作来更新工作内存和主内存中变化的变量,
(2)内存间交互操作
这里其实就是Save和Load的用途
- 锁定(lock):作用于主内存的变量,标识为被一条线程占用
- 解锁(unlock):解锁上面的锁定状态,其线程就可以占用了
- 读取(read):作用于主内存的变量,把一个变量读取出来,为后面载入做数据的准备
- 载入(load):把读取的数据载入到工作内存中
- 使用(use):作用于工作内存的变量,它把工作内存的一个变量传递给执行引擎,每当 遇到需要使用此变量的值的字节码指令时就会执行此操作
- 赋值(assign):作用于工作内存变量,把上面使用(use)的变量的值需要赋值时就会执行这个操作
- 存储(store):把工作内存中一个变量的值传输到主内存
- 写入(write):把存储过来的值放入主内存中(如果是改变值就改变值)
对于上面的8中操作需要满足下面的规则
- 读取和载入必须一起,存储和写入必须一起,锁定和解锁必须一起,看上面的你应该也能理解为什么
- 不允许丢弃赋值操作,这里丢弃指的是不往主存中同步这个变量的新值
- 不允许一个线程无原因的进行工作内存和主存中的变量进行同步,减少无意义的频繁更新
- 一个新变量只能在主存中"诞生",不允许在工作内存中直接使用一个未被初始化的变量,这个地方我是这么理解的,新变量必须从主内存中读取,载入过来,如果创建一个新变量就必须先往主内存中放,再从主内存读取到工作内存
- 一个变量只能被一个线程锁定,但是可以多次锁,但是必须解锁相同的次数才能真正的解锁这个变量
- 在对一个变量解锁前,必须把此变量同步回主内存,这里也明白了,其他的操作大部分都是在锁定和解锁期间完成的
(3)volatile关键字
为什么在这里说这个呢?
因为这个关键字为什么这么强大可以从这里分析出来
两个特点:
- 可见性:当一个线程修改了被此关键字修饰的变量,那其他的线程是可以立即知道的(即使可以存在工作线程不一致的情况,但是执行引擎每次使用都刷新,看不到不一致的情况,所以就可以认为是一致的)而普通变量就不行,因为需要主内存同步才行,还是有延迟
- 禁止指令重排序:对于程序而言只要最后的结果是正确的,对于里面的执行顺序可以不按逻辑来,比如
int i=5
在int j=6
的前面,但是在运行的时候有可能j先执行,i后执行,但是加上volatile 就没问题了,在此关键字前面的必须全部执行完才能执行此关键字的程序,后面的也不允许提前于此程序,原因就是在汇编语言里,加了lock 相当于加锁,当一个内存屏障
对于可见性你肯定还是有迷惑的,立即知道是怎么立即知道的呢?
查询IA32手册,对于lock的前缀 它的作用使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache
通俗点讲就是你这个被volatile修饰的变量当被修改后肯定要存储,写入内存的,但是在你修改时别的线程如果有此变量,那别的线程的此变量就会无效化,需要执行引擎重新去主存中读取,载入新的此变量。
有一个误区:volatile能保证原子性?其实是不能的,因为如果要让它保证原子性,那就需要它修饰的变量进行的操作是原子性的,区别就是i++
和i=1
的区别,一个不是原子性操作,一个是原子性操作。
volatitle同步机制的性能是优于锁(synchronized和java.util.concurent包下的锁),CAS和AQS也是运用了volatitle 的性能,
如果volatitle能满足应用场景的需求就可以用它,不行的话再考虑锁。
(4)先行发生原则
这个规则是非常重要的,因为前面我们说了指令重排序
如果我的程序中没有volatitle的变量,那我的程序会不会乱逃呢?比如赋值比定义快?这个是不可能的,要不我们的程序还怎么运行,它都找不到定义的此变量肯定就不能赋值了,
下面有一些规则,我就不细写了,看看就懂
- 程序次序规则:一个线程内,按照控制流顺序执行
- 管程锁定规则:解锁先发生于后面同一个锁的加锁
- volatitle变量规则:文章上面
- 线程启动规则:线程的start()最先发生
- 线程终止规则:线程终止在最后发生
- 线程中断规则:线程interrupt()方法的调用先行发生于被中断线程的代码的检测和中断事件的发生
- 对象终结规则:一个对象的发生初始化在此对象执行finalize()之前
- 传递性:A比B先发生,B比C先发生,那么A肯定比C先发生
2、java与线程
(1)线程的实现
实现线程有三种方式
- 内核线程实现
- 用户线程实现
- 用户线程加轻量级线程实现
内核线程实现:直接由操作系统内核支持的线程,这种线程由内核来完成调度,但程序一般不会直接去使用内核,而是使用内核线程的一种高级接口-轻量级进程(这做一下区分,在用户态中,如果直接和内核线程对应的线程叫轻量级进程,不对应就叫用户线程),也就是我们通常所说的线程,为了区分,内核里的线程叫内核线程,轻量级线程与内核线程的比是1:1,因为有内核线程的支持,即使轻量级进程阻塞了也不会影响整合进程的工作。局限行:系统调用的代价较高,而且需要在内核态和用户态之间来回切换,耗资源,而且内核线程是有限的。
用户线程实现: 只在用户态执行线程,对于线程的分类,只要不是内核线程,其他的一律为用户线程,因为不能用内核线程,效率会受到限制,而且还很麻烦,线程模型是进程与线程的比例:1:N,
和上面内核线层的区别是这里一个进程可以有很多线程,而上面的线程数量会受内核线程的限制,所以为什么上面的是和内核线程是比例,而这个是和进程的比例
用户线程加轻量级进程之间的混合实现
这里的比例是N:M,就是多对多,因为混杂着轻量级进程和用户线程这两种,这样既可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用需要用到轻量级进程来完成,打打降低了完全阻塞的风险,还不受内核线程数量的限制。
对于Sun JDK而言,java在Windows和Linux都是使用1:1的线程模型来实现的,一条java线程映射一条轻量级进程
(2)java线程的调度和状态
线程的调度是指系统为线程分配处理器使用权的过程
分为两种:
- 协同式线程调度:执行时间由线程决定,主动通知系统更换,实现简单,但是执行时间不可控,阻塞就完了
- 抢占式线程调度:由系统来分配执行时间,时间可控,不会导致进程阻塞,java就是用这种方式
线程的状态有6种(但是在java语言中定义了5种)
- 新建:线程建立
- 运行:线程运行
- 无限期等待:等待被其他线程唤醒wait
- 限期等待:无须被唤醒,到时间会醒的,sleep
- 阻塞:和等待的区别是在等待获取一个排它锁
- 结束:终止线程状态