JVM之Java内存模型与线程(十二)

硬件的效率与一致性
在执行并发多任务的时候,由于处理器至少要与内存交互,而他们间速度相差几个数量级,因此计算机增加了一层高速缓存作为缓冲,解决了速度矛盾。但是却对多处理器系统带来一个新的问题:由于每一个处理器都有自己的高速缓存,并且他们共享一块主存。当多个处理器的运算任务都涉及到同一块主内存时,可能导致各自的缓存数据不一致。如果发生这种情况,同步回到主存时,到底以谁的数据为准那?为了解决一致性问题,需要各个处理器在访问缓存时都遵循一些协议,读写时根据协议来操作,比如:MSI、MESI、MOSI等。后面所说的“内存模型”可以理解为在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
除了增加高速缓存外,为了使处理器的内部运算单元尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与书序执行的结果是一致的,但并不能保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。JIT编译器的指令重排优化与此相似。
Java内存模型
      主存与工作内存
Java内存的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量是指实例字段、静态字段和构成数组对象的元素(线程共享),但不包括局部变量和方法参数(线程私有)。Java内存模型规定了所有的变量都存储在主存中,每条线程有自己的工作内存,工作内存中保存了被线程使用的主存中变量的副本拷贝,并且线程对变量的所有操作(读取、赋值等)都必须在工作内存中执行,不能直接主存中的变量。不同线程间的工作内存不能被别的线程访问,线程间变量值的传递均需要通过主内存来完成。
      内存间的交互操作
Java内存模型定义了一下8种操作来完成主内存与工作内存之间具体交互协议,即一个变量如何从主存拷贝到工作内存、如何从工作内存同步回主内存之类的细节。(站在线程或者工作内存一方角度)
lock(锁定):把主存中的变量标识为一条线程独占的状态
unlock(解锁):把主存中处于锁定状态的变量释放出来,才可以被其他变量锁定
read(读取):把一个变量从主存中传输(并非赋值到)到线程的工作内存中,后+load
load(载入):把read操作从主存中得到的变量值放入工作内存的变量副本中。
use(使用):虚拟机遇到需要使用变量值的字节码指令时,把工作内存中的值传递给执行引擎
assign(赋值):把一个执行引擎接收到的值赋给工作内存的变量。
store(存储):把工作内存中一个变量的值传递到主存中。
write(写入):把store操作从工作内存中得到的变量值放入到主内存的变量中。
      Volatile型变量的特殊规则
与synchronize相比作为轻量级的同步机制,当一个变量定义为volatile后。具备两个特性:
保证此变量对所有的线程可见性(一条线程修改了这个变量的值,新值对于其他线程来说是立刻得知的。volatile不存在-各个线程工作内存变量不一致问题:因为存在各个线程工作内存中变量不一致的情况,但是由于每次使用前都会先刷新,所以执行引擎看不到不一致情况。
由于java运算并非原子操作,因此volatile变量的运算在并发情况下并不安全。比如多个线程对于一个静态变量a进行++操作,由于当某个线程把变量值放入栈顶时,其他线程可能已经修改变量值,并同步到主存中,而栈顶值已经过期。这样得到的结果都小于预期值。
因此适用于一下情况:
》运算结果并不依赖变量的当前值,或保证只有单一的线程修改变量的值(每个线程计算并不与当前值变化)
》变量不需要与其他这状态变量共同参与不变约束。
比如:变量flag控制并发等 比较适合
volatile boolean flag=true;
while(flag){
….
}
禁止指令重排优化通过操作相当于一个内存屏障,把本cpu的cache写入主存,而该操作会把别的cpu或别的内核无效化其cache,这样就可以对其他线程可见性。
————————————————————————————————————————————
原子性、可见性与有序性
Java内存模型势围绕如何处理并发过程中的原子性、可见性与有序性这三个特征建立的。
原子性;Java内存模型直接保证变量原子性操作:read、load、assign、use、store、write,基本上数据访问读写具备原子性。还可以(系统lock、unlock)用户使用synchronized关键字来同步代码块。
可见性:volatile可以使变量修改后立即同步到主存中,其他cache失效从而重新获取刷新。synchronized(同步块unlock前先把变量同步回主存)和final(一旦初始化完成就不会修改)也可以实现。
有序性:java程序的天然有序性:在本线程中观察,所有的操作都是有序的(线程内串行),如果从一个线程观察另一个线程,所有的操作都是无序的(指令重排和主存和工作内存同步延迟)。java提供valotitle和synchrized关键字保证线程间操作有序性。valotitle禁止指令重排,synchrized则是一个变量在同一个时刻只允许一条线程对其lock,这就决定了,持有同一个锁的两个同步块只能串行进入,别的线程要进入必须获取该变量的锁(但已被当前线程持有)
先行发生原则
仅靠上边两个关键字莱安城所有有序性,那么这些操作将会变得很繁琐,因此java语言中存在一个先行发生原则,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子解决并发环境下是否可能存在冲突问题。
先行发生原则:内存模型定义的两项操作偏序关系–如果操作A先行发生于操作B,就是说发生操作B之前,操作A产生的影响(修改变量、调用方法等)能被B观察到。
Java与线程
线程可以把进程资源分配与执行调度分开,各个线程可以共享进程资源,但可以独立调度。
Thread类与大部分API显著差别是,所有方法都是native。
实现线程的方式主要有三种:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现
Sun jdk 在windows版和linux版都是(一对一)一个java线程映射到一个轻量级的进程中。在solaris平台中可以一对一和多对多
Java线程调度
指为线程分配处理器使用权的过程,主要方式:协同式线程调度和抢占式线程调度
协同式线程调度:执行时间线程自己来控制,但执行时间不可控制,如果线程编写有问题,没有告诉系统切换,,那么会一直阻塞在那里。
抢占式调度(java使用的方式):系统分配线程执行时间和切换线程,如果想给某个线程多点时间可以通过设置优先级来执行。但是并不是十分靠谱,java采用混合模式实现线程,把线程映射到系统内核线程,但具体执行还是取决于操作系统。当然windows体统中还有一个优先级推进器,即如果发现某一个线程执行特别勤奋努力,可能会越过优先级分配时间。
状态转换
java定义了5种线程状态,一个线程任何时刻只能处于一种状态
新建(new):创建后尚未启动的线程处于这种状态
运行(Runable):包括操作系统线程状态Running和ready,就是正在执行或者等待cup为他分配时间
无限期等待(waiting):这种状态的线程不会被分配cup执行时间,需要其他线程显示唤醒
限期等待(Timed Waiting):同样不会分配cup执行时间,但过一段时间自动唤醒
阻塞(blocked):等待获取一个排它锁,等待另一个线程放弃这个锁
结束(terminated):已终止线程的线程状态,线程已经结束执行。
线程状态转换关系:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_26564827/article/details/80717405