概述
为什么需要计算机同时去做几件事情?
由于计算机的运算速度与它的存储和通信子系统速度之间差距太大,所以大量时间都花费在了磁盘I/O,网络通信或者数据库访问上,然而让计算机同时去处理多个事情可以使得处理器运算能力被压榨出来。
硬件的效率与一致性
计算机的存储设备和处理器的运算速度之间差了几个数量级,所以现代计算机系统不得不加入一层读写速度尽可能解决处理器运算速度的高速缓存(cache)来作为内存和处理器之间的缓冲。
Java内存模型
首先来看一下线程、工作内存、主内存之间的关系:
Java内存模型规定了所有的变量都应该存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory,可类比于处理器和内存之间的cache),工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作必须在工作内存中进行,而不能直接读写主内存中的变量(包括volatile,不要误以为volatile修饰的变量就是直接读写主内存的,只是由于它特殊的操作顺序性规定,使得它看起来就像直接从主内存中读写一样)。
对volatile型变量的特殊规则
关键字volatile是JVM提供的最轻量级的同步机制。当一个变量被定义为volatile之后,它将具备两种特性:
a、保证此变量对所有线程的可见性。可见性是指一条线程修改了这个变量的值之后,新值对于其他线程来说是可以立即得知的。volatile变量在各个线程中是一致的,但是volatile变量运算在并发下不一定安全,因为Java里的运算并非原子操作。
b、禁止指令重排优化。普通的变量仅仅会保证在该方法执行过程中所依赖赋值结果的地方都能获取到正确的结果,但是不能保证变量赋值的操作和程序代码中执行顺序一致。有可能使得某行代码被提前执行(重排序优化是机器级的优化操作,提前执行指的是这句话对应的汇编代码被提前执行)。观察volatile变量对应的JIT汇编代码可以发现赋值之后多出了一个"lock add1 $0x0,(%esp)"操作,这个操作相当于内存屏障(Memory Barrier或者Memory Fence,指重排序时不能把后面的指令排序到内存屏障之前的位置),只有一个cpu访问内存的时候,不需要内存屏障,如果有多个cpu访问一块内存,且其中一个在观测另外一个,就需要内存屏障来保障一致性。这行汇编代码的含义在于lock前缀,作用时使得本cpu的cache写入了内存,该写入动作会引起别的CPU或者别的内核无效化其cache。通过这样的操作就可以使得volatile变量的修改对其他CPU立即可见。
Java内存模型的特征
原子性:大致可以认为基本数据类型的访问读写是具备原子性的(long double的读写大部分JVM已经实现为原子操作了)。
可见性:可见性指一个线程修改了共享变量的值的时候,其他线程能够立刻得知这个修改。volatile的特殊规则保证了新值能够立即同步到主内存,用前立即从主内存刷新,另外synchronized和final也可以保证可见性。
有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程内观察另一个线程,所有操作都是无序的。前半句是"线程内表现为串行的语义"的体现,后半句是"指令重排序"和"工作内存和主内存同步延迟"现象的体现。
先行发生(happens-before)原则
Java中有一个先行发生原则,这是判断数据是否存在竞争,线程是否安全的主要依据。
先行发生原则指的是:如果操作A先行发生于操作B,那么操作A产生的影响能被操作B观察到。
线程生命周期的5种状态:
新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
例如:Thread t1=new Thread();
就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();
运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行run()方法后终止
异常终止:调用stop()方法让一个线程终止运行
堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用wait()方法。(调用notify()方法回到就绪状态)
被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)