码迷,mamicode.com
首页 > 编程语言 > 详细

Java内存模型与线程

时间:2015-06-06 00:31:27      阅读:214      评论:0      收藏:0      [点我收藏+]

标签:


 

工作内存与主内存

 

Java内存模型(jmm)的出现是为了各种操作系统和硬件的内存访问的差异。

Java内存模型规定了变量不含局部变量,因为局部变量线程私有,不存在共享问题)都得存放在主内存中,而每个线程对这些变量的操作都必须是从主内存中取出来并在工作内存中完成(如读取、写入的操作),不同线程之间不能访问对方的工作内存。如下图,展现了线程、主内存、工作内存之间的交互关系:

技术分享

加入一个工作内存的目的很明显,就是为了加快在内存中的操作数据的速度,因为工作内存优先存储在寄存器和高速缓存中,这两个操作的速度都远远快于主内存。

主内存和工作内存之间交互的主要操作为:

 

Lock(锁定):作用于主内存的变量,将主内存该变量标记成当前线程私有的,其他线程无法访问。

Unlock(解锁):作用于主内存的变量,解除主内存中该变量的锁定状态,让他变成线程共享变量。

Read(读取):作用于主内存的变量,将该变量读取到当前线程的工作内存中,以便进行load操作。

Load(加载):作用于工作内存中的变量,将read获取到的变量载入工作内存的变量副本中。

Use(使用):作用于工作内存中的变量,虚拟机执行引擎在执行字节码指令的时候,碰到了一个变量就会执行该操作,使用该变量。

Assgin(赋值):作用于工作内存中的变量,虚拟机执行引擎在执行字节码指令的时候,碰到了变量赋值的指令就会执行该操作。

Store(存储):作用于工作内存中的变量,将工作内存中的变量放入主内存,以便进行write操作。

Write(写入):作用于主内存中的变量,将store得到的变量放入主内存的变量中。

 

很明显,如果两个线程AB访问同一个变量2的时候,都没有锁定。

A在工作内存改了变量值+1,而B此时并不能看到这个操作,B还以为没人改动这个值,就认为自己是再原来的值上进行操作-1。算出来的值变成了多少呢?噢,并不知道,因为可能是3,也可能是1。这时,就出现了并发访问的线程安全问题。

 

Volatile变量

        

         Volatilejvm提供的最轻量级的同步机制,但是要正确使用不容易。

         Volatile可以保证两个线程对变量操作的可见性,即一个线程操作的操作可以被另一线程看到。但是如果操作不是原子的,依然没法保证volatile同步的正确性。只有在下述情况,才可以使用这个关键字:

对变量的写入操作不依赖于该变量的当前值(比如a=0;a=a+1的操作,整个流程为a初始化为0,将a的值在0的基础之上加1,然后赋值给a本身,很明显依赖了当前值),或者确保只有单一线程修改变量。

该变量不会与其他状态变量纳入不变性条件中。(当变量本身是不可变时,volatile能保证安全访问,比如双重判断的单例模式。但一旦其他状态变量参杂进来的时候,并发情况就无法预知,正确性也无法保障)


    package org.project.loda.test;

/**
 * 
* @ClassName: Singleton 
* @Description: 基于双重判断的单例模式(广泛使用) 
* @author minjun
* @date 2015年6月5日 下午10:20:37 
*
 */
public class Singleton {

	private static volatile Singleton s;

	public static Singleton getInstance() {
		if (s == null) {
			synchronized (Singleton.class) {
				if (s == null) {
					s = new Singleton();
				}
			}
		}

		return s;
	}
}




Volatile还有个特性就是,他可以禁止指令进行重排序优化。什么是重排序?看看java并发编程实战中的解释:

各种操作延迟或者看似乱序执行的不同原因,都可以归为重排序。
这是一个没有间接性解释,因为直接解释很难以理解。下面看看这个程序:


技术分享

如果按上到下顺序判断,线程one执行顺序为:a=1,x=b,线程other执行顺序应该为:b=1,y=a。然后完成计算,打印其值。但是,事实可能是:

技术分享

我的天哪?x=b竟然在a=1之前执行了!!!发生了什么?其实,这就是底层优化多线程程序的时候进行了重排序操作,导致乱序出现。最可怕的是,这还不是必然的,他可能发生,可能不发生,你根本预料不到!

所以,volatile本身强大的地方就是他还能预防这种情况发生,虽然牺牲了一点性能,但是大大增强了程序的可靠性。但是记住,不要依赖于volatile,在合适的时候才使用他(上文已经说明),如果情况不合适,就使用传统的synchronized关键字同步共享变量的访问,用来保证程序正确性(这个关键字的性能会随着jvm不断完善而不断提升,将来性能会慢慢逼近volatile)。

 

Happens-Before原则

         说了这么多,发现可以使用volatilesynchronized关键字进行同步。但是,你不会无缘无故就使用它们,存在竞争、线程安全问题的时候才应该考虑使用,但是如何判断是否存在这些问题呢?下面介绍java内存模型中的一个重点原则——先行发生原则(Happens-Before),使用这个原则作为依据,来指导你判断是否存在线程安全和竞争问题

l  程序顺序规则:在程序中,如果A操作在B操作之前(比如A代码在B代码上面,或者由A程序调用B程序),那么在这个线程中,A操作将在B操作之前执行。

l  管理锁定规则:一个unlock操作先于后面对同一个锁的lock操作之前执行。

Volatile变量规则:对一个volatile变量的写操作必须在对该变量的读操作之前发生。

l  线程启动规则:线程的Thread.start必须在该线程所有其他操作之前发生

l  线程终止规则:线程中所有操作都先行发生于该线程的终止检测。可以通过Thread.join()方法结束、Thread.isAlive()的返回值判断线程是否终止。

l  线程中断规则:对线程interrupt()方法的调用必须在被中断线程的代码检测到interrupt调用之前执行。

l  对象终结规则:对象的初始化(构造函数的调用)必须在该对象的finalize()方法完成。

l  传递性:如果A先行发生于BB先行发生于C,那么A先行发生于C

这些操作是无需使用任何同步手段就能保证成立的先行发生规则。如果要线程AB,需要B能看到A操作的结果(无论两者是否在一个线程当中),需要AB满足Happens-Before关系,如果两个操作不存在Happens-Before关系,JVM会对他们进行任意重排序。当AB在同一个线程中,或者两个线程使用同样的锁,他们就能满足Happens-Before,如果使用不同锁,就不满足。

线程

         线程也叫作轻量级进程,是大多现代操作系统的基本调度单位。在同一个进程中,多个线程共享内存空间,因此需要足够的同步机制才能保证正常访问。每个线程本身都有各自的程序计数器、栈和局部变量等。在java中使用线程调度的方式是抢占式的,需要由操作系统分配执行时间,线程本身无法决定(例如java中,只有Thread.yield()可以让出自己的执行时间,但是并没有提供可以主动获取执行时间的操作)。虽然java中线程调度由系统执行,但是还是可以通过设置线程优先级来“建议”操作系统多给某些线程分配执行时间(然后,这并不一定就能保证高优先级的先执行,所以不太靠谱...)。

         Java定义了如下几种线程状态,一个线程有且仅有一个:

技术分享

上图指定了几种状态以及达到这些状态所需要经历的过程,这里就不给出解释了,有基础的同学应该很容易看懂这些状态。

 参考文献:java并发编程实战

                深入java虚拟机(JVM高级特性与最佳实践

Java内存模型与线程

标签:

原文地址:http://my.oschina.net/u/1378920/blog/425566

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!