第十一章 Java内存模型与线程
一 Java内存模型
1. 主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。
2. volatile变量
volatile变量具有可见性,可见性是指当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。但是并发下并不保证是安全的。而下面的场景就很适合使用volatile变量来控制并发
volatile boolean shutdownRequested; public void shutdown(){ shutdownRequested = true; } public void doWork(){ while(!shutdownRequested){ //do stuff } }
使用volatile变量的第二个语义是禁止指令重排序优化,例如标准的单例设计模式代码:
class Singleton{ private volatile static Singleton instance; public static Singleton getInstance(){ if(instance == null){ synchronized (Singleton.class) { if(instance == null){ instance = new Singleton(); } } } return instance; } }
上面的代码中volatile变量可以使得在一个CPU的Cache写入内存时,引起别的CPU或者内核无效化其Cache,使修改对其他CPU立即可见。如果上述代码不用volatile变量会发生什么问题?
在执行instance=new Singleton();时,并不是原子语句,实际是包括了三大步骤:1.为对象分配内存;2.初始化实例对象 ;3.把引用instance指向分配的内存空间
这三个步骤并不能保证按序执行,处理器会进行指令重排序优化,存在这样的情况:优化重排后执行顺序为:1,3,2, 这样在线程1执行到3时,instance已经不为null了,线程2此时判断instance!=null,则直接返回instance引用,但现在实例对象还没有初始化完毕,此时线程2使用instance可能会造成程序崩溃。
另外,双重检查的原因:进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
二 Java与线程
线程的实现方式主要有三种:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程混合实现。而对于Java(Sun JDK)来说,Windows版和linux版都是使用一对一的线程模型实现的,一条java线程就映射到一条轻量级进程之中。
线程调度的方式有两种:协同式线程调度和抢占式线程调度。java使用的调度方式就是抢占式调度。
java语言定义了以下这些线程状态:新建;运行;无限期等待;限期等待;阻塞;结束。