人不好分心,机器必须分心。
Java内存模型的主要目标为定义Java程序中各个变量的访问规则,即在虚拟机中如何存储和读取变量。这里的变量是针对所有Java变量的。变量存储在虚拟机的主内存上,所有。工作内存为线程的内存,包括局部变量,都是从主内存的变量中拷贝而来。线程不可以直接读写主内存的变量,线程之间的工作变量也不可以随意访问。
主内存与工作内存之间通过协议交互,非常严格。Java规定了工作内存可以执行的8种操作,每种都需要原子性。
lock:作用于主内存,将变量标志为某一线程独占。
unlock:作用于主内存,从lock中释放出来。
read:作用于主内存,将主内存变量传输到工作内存。
load:作用于工作内存,把read出的变量值放入变量副本中。(工作内存只能用拷贝)
use:作用于工作内存,把工作内存中的变量值传输给执行引擎,就是用工作内存中的拷贝值。
assign:作用于工作内存,把执行引擎收到的值赋给工作内存中的变量(拷贝来的)。
store:作用于工作内存,把工作内存的变量值传输给主内存。
write:作用于主内存,把工作内存store的值写入主内存的变量中。
read load use
主内存 <----------------> 工作内存 <--------------> 执行引擎
write store assign
与主内存相关的操作,不允许单独出现。read必须与load成对出现,write必须与store成对出现,且先后顺序必须对。lock与unlock也必须成对出现。
与synchronized相比,volatile更轻量。变量被定义为volatile后,所有线程可见---初值可见,被更改后也可以被所有线程得知最新的值。但不能保证并发安全。
public static volatile int race = 0;
public static void inc(){
race++;
}
public static void main(String[] args){
Thread[] threads = new Thread[20];
for(int i = 0; i < 20; i++){
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i< 1000; i++){
inc();
}
}
});
threads[i].start();
}
while(Thread.activeCount() > 1) Thread.yield();
System.out.println(race);
}
按作者说法,这段代码会出现比20000小的数,在实际操作中,只会在一个线程中卡死。作者想表达race++镶嵌在increase()中,操作非原子性,会出现指令出现检查不符的情况,导致数值无法更新。按作者的说法,volatile的实用性在于可以实现可见性,但不可以依赖当前变量的值,且变量的不变约束不依赖于其他变量。简单说就是不能指望这个变量的值是可靠的场景,可以使用volatile,比如不计算,只当成flag。
volatile变量禁止指令重排序优化。在Java编译中,为了优化,会改变一些语句的执行顺序,但不会影响赋值等重要操作,但提前执行可能会影响代码逻辑。这种问题会比较致命---明明逻辑是对的,咋就先跑了后面的语句呢?所以遇到逻辑有问题的时候,可以用volatile把变量拴起来,这样至少针对该变量的语句是符合顺序的。在volatile变量中,load必须和use牢牢连在一起,并在之前要刷新;assign和store要牢牢连在一起,立刻write。
volatile在读变量值时与普通变量速度类似,写的时候因为涉及lock,会慢。但会比最靠谱的synchronized快。volatile不能算锁。
特别的,long和double两个长类型在load、store、read、write可以不保证原子性。就是说在未经保护的情况下,几个线程同时操作,有可能读取出谁也不认识的值,理论存在,实际没有。
好像到处都有这个原则)规定了一些Java中一定要遵循的逻辑顺序:
程序次序规则(Program Order Rule):在一个线程内要按照代码顺序(?),准确来说是按照控制流顺序(√)。线程间才存 在无序的概念。
管程锁定规则(Monitor Lock Rule):同一个锁,unlock的操作要在lock之后。
volatile变量规则(Volatile Variable Rule):volatile修饰的变量的写操作要在之后对这个变量的读操作之前,改完才能读。
线程启动规则(Thread Start Rule):Thread对象的start()要在这个线程所有动作之前。
线程终止规则(Thread Termination Rule):线程所有动作都在终止之前,终止包括join,isAlive()。
线程中断规则(Thread Interruption Rule):线程对interrupt()的调用要早于该线程中检测到中断事件前。
对象终结规则(Finalizer Rule):一个对象的初始化(构造函数执行结束)要先行发生于finalize()之前。
传递性(Transitivity):A先行发生于B,B先行发生于C,则A先行发生于C。
其实看起来就是一些非常理所当然的逻辑顺序,多的确实保证不了,靠双手。
Java的Thread类的关键方法都声明为Native,Native的方法未使用或无法使用平台无关的手段实现。
1.使用内核线程实现:
内核线程(Kernel-Level Thread,KLT)就是操作系统的内核支持的线程。 内核通过调度器(Scheduler)分配一个线程哪里给谁,将线程的任务映射到各个处理器上。多核就是多线程。程序一般不会直接使用系统线程,而是使用内核线程的一种搞机接口,轻量级进程(Light Weight Process,LWP),这是常见的线程。内核线程和轻量级进程是一个1:1的关系。由系统内核线程实现,导致在线程操作时,会进行系统调用,成本较高,且消耗系统资源。
2.使用用户线程实现:
广义上讲,非内核线程的线程都可以被称为用户线程(User Thread,UT),轻量级“进程”也可以属于用户线程。狭义来说,用户线程是建立在用户控件的线程库上,内核不能感知线程的存在。操作都在用户态中完成,不需要系统内核的帮助。进程与线程可以达到1:N的效果,性能较高。由于缺少系统内核的支持,很多操作如阻塞,线程映射到其他处理器上等问题需要解决,很复杂。Java是不用了。
3.使用用户线程加轻量级进程混合实现:
在组合的实现中,用户线程以及有关操作依然在用户态中,避免了系统调用。轻量级进程则作为用户线程与内核线程之间交流的桥梁,完成线程的调度和处理器的映射。在这种关系中,内核线程与轻量级进程依然是1:1的关系,但轻量级进程与用户线程是一种不定的N:M的关系。一个轻量级进程可以服务多个用户线程。
线程调度这里指系统为线程分配处理器的调度过程,分为“协同式线程调度”(Cooperative Thread-Scheduling)和“抢占式线程调度”(Preemptive Threads-Scheduling)。协同式属于人民当家做主,线程自己安排自己的起止,在完成后主动让给下一个线程。实现简单,看似合理,但岂不是乱了套?自行安排会导致执行时间不可控,一旦阻塞,将连任到死,导致系统崩溃。抢占式由系统分配时间,线程本身无法决定。Java使用抢占式线程调度。当一个进程阻塞过长,直接杀掉。
Java定义了线程的6种原子级状态,任意时间点每个线程只能处于一种状态:
1.新建(new):创建后为启动。
2.运行(Runnable):包括了准备和运行中的状态,所以runnable翻译成“运行”可能一点点不合适。执行中或等待分配都算。
3.无限期等待(Waiting):线程处于睡眠状态,不会被CPU分配时间,除非有其他线程将其唤醒。(无timeout的wait(),join(),以及LockSupport.park()方法)
4.限期等待(Timed Waiting):不会被CPU分配时间,但当过了时限后会被自动唤醒。(sleep(),设置了timeout的wait(),join()等)
5.阻塞(Blocked):阻塞状态的线程等待其他线程中排他锁的释放,在同步区的线程会进入此状态。
6.结束(Terminated):线程已终止。