Java内存模型与线程

1、Java内存模型

1.1 主内存与工作内存

        Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存进内存和取出的底层细节。这里的变量(Variables)与Java变量有所区别,包括了实例字段、静态字段、构成数组对象的元素,不包括局部变量与方法参数,因为后者是线程私有的,不会共享。

        Java内存模型规定了所有变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存。线程的工作内存中保存了被该线程使用到的变量主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程也无法直接访问其他线程的工作内存中的变量,线程间的值传递需要通过主内存完成,线程、主内存、工作内存三者的交互关系如图所示。

 1.2 内存间交互操作

        关于一个变量在主内存与工作内存之间传输的实现细节,JMM定义了8种操作来完成,虚拟机实现时必须保证每一种操作都是原子的、不可再分的。对于double、long来说,load、store、read、write可能有些例外。

  • lock(锁定):作用于主内存的变量,它把一个变量标识成一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主存中传输到线程的工作内存中,以便随后的load动作。
  • load(载入):作用于工作内存的变量,把read的变量放入内存的变量副本中。
  • use(使用):作用于工作内存,把工作内存的一个变量的值交给执行引擎。
  • assign(赋值):作用于工作内存,它把一个从执行引擎接收到的值赋值给工作内存的变量。
  • store(存储):作用于工作内存,将内存变量传给主内存,随着方便后面wirite一同使用。
  • wirte(写入):把Store的值传递到主内存中,方便以后随时write操作。

        如果要把一个变量从主内存复制到工作内存,就要顺序地执行read、load,如果要把变量从工作内存同步回主内存,就要顺序地执行store、write。这里只说是顺序执行,没有说是连续执行,就是说中间可以插入其他指令。另外执行这8种基本操作需要满足以下规则:

  • 不允许read和load、store和write操作之一单独出现。
  • 不允许一个线程丢弃最近的assign操作,即修改必须同步回主内存
  • 不允许一个线程无原因的把数据从线程工作内存同步回主内存中
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量
  • 一个变量同一个时刻只允许一条线程对其进行lock操作,但可以被一个线程重复lock,lock多少次,需要执行多少次unlock。
  • 对一个变量进行lock,会清除工作内存中此变量的值,在使用的时候,需要重新执行load或者assign操作。
  • 如果一个变量没有被lock,不允许进行unlock,也不允许unlock其他线程lock的变量
  • 对变量进行unlock的时候,必须将数据回写到主内存中。

1.3 volatile的特殊规则

        关键字volatile是JVM提供的最轻量级的同步机制,当一个变量被定义成volatile之后,它将具备两种特性:

  • 第一个是保证此变量对所有线程的可见性,指的是当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。普通变量需要传递到主内存,再到工作内存才会可见。
  • 第二个特性在于禁止指令重排序优化,普通变量只保证执行过程中所有依赖赋值结果的地方能够获得正确的结果,不保证赋值操作的顺序与代码中的执行顺序一致。

1.4 long和double类型的特殊规则

        允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32位操作。所以虚拟机实现可以不保证64位数据的load、store、read和write操作的原子性。这就是long和double的非原子协定。

  如果有多个线程同时对这个进行读取修改,可能会造成其中32位数据出现问题,这种情况非常罕见。虽然允许不把long和double变量的读写实现原子操作,但是强烈建议这么做,所以商用虚拟机基本将64位数据的读写操作作为原子操作对待,在编码的时候不需要对long和double变量专门声明为volatile。

1.5 原子性、可见性与有序性

原子性(Atomicity):JMM直接保证了read、load、assign、use、store、write操作的原子性。大致可以认为基本数据类型的访问读写是具备原子性的。

可见性(Visibility):当一个线程修改了共享变量的值,其他线程能立即得知这个修改。JMM是在变量修改后将新值同步回主存,在变量读取前从主存刷新变量值这种依赖主存作为传递媒介的方式来实现可见性的。普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,因此我们可以说volatile保证了多线程操作时变量的可见性。

有序性(Ordering):如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,则都是无序的。前半句指“线程内表现为串行的语义”,后半句指“指令重排”和“工作内存与主内存同步延迟”现象。

2、Java与线程

2.1 线程的实现

        线程是比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行调度分开。各个线程既可以共享进程资源(内存地址,文件IO等),又可以独立调度(线程是CPU调度的基本单位)。主流的操作系统都提供了线程实现,Java提供了不同操作系统对线程的统一处理,每个执行了start方法的Thread实例,就开启了一个线程。

2.2 Java线程调度

        线程调度是指系统为线程分配处理器使用权的过程,主要有:协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。

        协同式调度的多线程系统,线程的执行时间由线程本身控制,线程把自身的工作执行完后,通知系统切换到另一个进程。其最大的好处是实现简单,而且由于线程把自己的工作执行完后才会进行线程切换,所以不存在线程同步的问题。坏处也很明显,执行时间不可控制,如果一个线程编写有问题,一直不告知系统切换,则程序会一直阻塞。

        抢占式调度的每个线程由系统来分配执行时间,这样一来,线程的执行时间是系统可控的,不会有一个线程导致整个进程阻塞的问题。Java就采用了抢占式调度。

2.3 状态转换

        Java语言定义了5种线程状态,任意时间只能处于一个状态。

  • 新建new:创建后未启动
  • 运行runnable:包括running和ready,可能正在被执行,可能在等待CPU分配时间
  • 无限期等待waiting:这种状态不会被分配执行时间,需要等待其他线程显示唤醒,陷入waiting的方法:Object.wait();Thread.join();LockSupport.park();
  • 限期等待timed waiting:这种状态也不会被分配时间,不过不需要唤醒,到时间就会自动唤醒。Thread.sleep();Object.wait(timeout);Thread.join(timeout);LockSupport.parkNanos();LockSupport.parkUnitl();
  • 阻塞blocked:线程被阻塞了,阻塞状态与等待状态的区别在于,阻塞状态在等待一个排他锁,这个事件将在另一个线程放弃这个锁时发生。等待只是等待一段时间,或者是唤醒动作发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • 结束terminated:已终止线程的线程状态。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值