物理内存 :
由于计算机的存储设备与处理器的运算速度有几个数量级的差距, 所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓存: 将运算需要使用到的数据复制到缓存中, 让运算能快速进行, 当运算结束后再从缓存同步回内存中, 这样处理器就无须等待缓慢的内存读写了;
缓存一致性 :
在多处理器系统中, 每个处理器都有自己的高速缓存, 而它们又共享同一主内存. 当多个处理器的运算任务都涉及同一块主内存区域时, 将可能导致各自的缓存数据不一致.
指令重排序 :
为了使得处理器内部的运算单元能尽量被充分利用, 处理器可能会对输入代码进行乱序执行优化, 处理器会在计算之后将乱序执行的结果重组, 保证该结果与顺序执行的结果是一致的, 但并不保证程序中各个语句计算的先后顺序与输入代码的顺序一致, 因此, 如果存在一个计算任务依赖另一个计算任务的中间结果, 那么其顺序性并不能靠代码的先后顺序来保证;
java内存模型 :
1、java内存模型的主要目标是定义程序中各个变量的访问规则, 此处的变量包括了实例字段, 静态字段和构成数组对象的元素, 但不包括局部变量与方法参数, 因为后者是线程私有的, 不会被共享, 不会存在竞争的问题;
2、java内存模型规定了所有的变量都存储在主内存中. 每条线程还有自己的工作内存, 线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝, 线程对变量的所有操作(读取, 赋值等)都必须在工作内存中进行, 而不能直接读写主内存中的变量.
3、不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值得传递均需要通过主内存来完成.
4、工作内存对应虚拟机栈中的部分区域, 主内存对应于物理硬件的内存;
内存间交互 :
主内存与工作内存之间具体的交互协议 : java内存模型中定义了以下8种操作来完成, 一个变量在主内存和工作内存之间的交互的实现细节, 虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的;
- 1、lock(锁定) : 作用于主内存的变量, 它把一个变量标识为一条线程独占的状态;
- 2、unlock(解锁) : 作用于主内存的变量, 它把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其他线程锁定;
- 3、read(读取) : 作用于主内存的变量, 它把一个变量的值从主内存传输到线程的工作内存中, 以便随后的load动作使用;
- 4、load(载入) : 作用于工作内存的变量, 它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
- 5、use(使用) : 作用于工作内存的变量, 它把工作内存中一个变量的值传递给执行引擎, 每当虚拟机遇到一个需要使用的变量的值得字节码指令时将会执行这个操作;
- 6、assign(赋值) : 作用于工作内存的变量, 它把一个从执行引擎接收到的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
- 7、store(存储) : 作用于工作内存的变量, 它把工作内存中一个变量的值传送到主内存中, 以便随后的write操作使用;
- 8、write(写入) : 作用于主内存的变量, 它把store操作从工作内存中得到的变量的值放入主内存的变量中;
volatile :
1、可见性 : 保证变量对所有线程可见, 当一条线程修改了这个变量的值, 新值对于其他线程来说是可以立即得知的;
2、禁止指令重排序 : 指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理.
分别用两段常见代码来阐述volatile这两个特性 :
可见性(但不能保证原子性):
public class Instance {
private static int i;
public static void increase() {
i++;
}
}
上面代码实际要分为以下几个步骤:
1. 将变量i读取到工作内存中;
2. int var = i + 1;操作赋值给var变量;
3. var变量再赋值给i;
4. i回写到主内存中;
所以当多个内存同时调用increase()方法时, 可能线程_1执行到步骤2时, 线程_2也执行到步骤2, 就会导致increase()被两个线程分别调用一次, 但是i实际为1的线程;
禁止指令重排序 :
public class Instance {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized(Instance.class) {
if (instance == null ) {
instance = new Instance();
}
}
}
return instance;
}
}
instance = new Instance()可以分为以下几个步骤 :
- 1、栈内存开辟空间给instance使用;
- 2、堆内存开辟空间准备初始化对象;
- 3、初始化对象;
- 4、栈中引用指向这个堆内存空间地址;
但是由于指令重排序, 上述步骤也可能实际执行顺序为1_2_4_3;按照这个顺序, 当线程1执行到步骤4时, 线程2调用getInstance()方法发现instance此时已经被赋值一个引用 != null, 直接走return方法, 但是此时instance所指向的Instance对象并没有初始化完成, 即内部变量都还是默认值;
线程的实现 :
实现线程主要有3种方式 : 使用内核线程、使用用户线程和使用用户线程加轻量级进程混合;
1、使用内核线程实现 :
线程的状态 :
java语言定义了6种线程状态, 在任意一个时间点, 一个线程只能有且仅有其中的一个状态:
- 1、新建(New) : 创建后尚未启动的线程处于这种状态;
- 2、运行(Runnable) : Runnable包括了操作系统线程状态中的Running和Ready, 也就是处于此状态的线程有可能正在执行, 也有可能正在等待着CPU为它分配执行时间;
- 3、无限期等待(Waiting) : 处于这种状态的线程不会被分配CPU执行时间, 它们要等待被其他线程显示的唤醒. 以下方法会让线程陷入无限期的等待状态;
1. 没有设置Timeout参数的Object.wait()方法;
2. 没有设置Timeout参数的Thread.join()方法;
3. LockSupport.park()方法;
- 4、限期等待(Time Waiting) : 处于这种状态的线程也不会被分配CPU执行时间, 不过无须等待被其他线程显示的唤醒, 在一定时间之后它们会由系统自动唤醒. 以下方法会让线程进入限期等待状态:
1. Thread.sleep()方法;
2. 设置了Timeout参数的Object.wait()方法;
3. 设置了Timeout参数的Thread.join()方法;
4. LockSupport.parkNanos()方法;
5. LockSupport.parkUnitl()方法;
- 5、阻塞(Blocked) : 线程被阻塞了, "阻塞状态"与"等待状态"的区别是: "阻塞状态"在等待着获取到一个排他锁, 这个事件将在另外一个线程放弃这个锁的时候发生; 而"等待状态"则是在等待一段时间, 或者唤醒动作的发生. 在程序等待进入同步区域的时候, 线程将进入这种状态;
- 6、结束(Terminated) : 已终止线程的线程状态, 线程已经结束执行;