对于Java并发编程,通常来讲有如下的关注点:java
线程安全性,正确性。web
线程的活跃性(死锁,活锁)编程
性能缓存
其中线程的安全性问题是首要解决的问题,线程不安全,运行出来的结果和预期不一致,那就连基本要求都没达到了。安全
保证线程的安全性问题,本质上就是保证线程同步,实际上就是线程之间的通讯问题。咱们知道,在操做系统中线程通讯有如下几种方式:多线程
信号量并发
信号app
管道jvm
共享内存socket
消息队列
socket
java中线程通讯主要使用共享内存的方式。共享内存的通讯方式首先要关注的就是可见性和有序性。而原子性操做通常都是必要的,因此主要关注这三个问题。
原子性是指操做是不可分的。其表如今于对于共享变量的某些操做,应该是不可分的,必须连续完成。例如a++,对于共享变量a的操做,实际上会执行三个步骤:
读取变量a的值
a的值+1
将值赋予变量a 。
这三个操做中任何一个操做过程当中,a的值被人篡改,那么都会出现咱们不但愿出现的结果。因此咱们必须保证这是原子性的。Java中的锁的机制解决了原子性的问题。
可见性是值一个线程对共享变量的修改,对于另外一个线程来讲是不是能够看到的。
为何会出现这种问题呢?
咱们知道,java线程通讯是经过共享内存的方式进行通讯的,而咱们又知道,为了加快执行的速度,线程通常是不会直接操做内存的,而是操做缓存。
java线程内存模型:
实际上,线程操做的是本身的工做内存,而不会直接操做主内存。若是线程对变量的操做没有刷写会主内存的话,仅仅改变了本身的工做内存的变量的副本,那么对于其余线程来讲是不可见的。而若是另外一个变量没有读取主内存中的新的值,而是使用旧的值的话,一样的也能够列为不可见。
对于jvm来讲,主内存是全部线程共享的java堆,而工做内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。
那么咱们怎么知道何时工做内存的变量会刷写到主内存当中呢?
这就涉及到java的happens-before关系了。
在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必须存在happens-before关系。
这我的的博客写的不错:http://ifeve.com/easy-happens-before/。
简单来讲,只要知足了happens-before关系,那么他们就是可见的。
例如:
线程A中执行i=1,线程B中执行j=i。若是线程A的操做和线程B的操做知足happens-before关系,那么j就必定等于1,不然j的值就是不肯定的。
happens-before关系以下:
程序次序规则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做;
锁定规则:一个unLock操做先行发生于后面对同一个锁额lock操做;
volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做;
传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C;
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个一个动做;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
从上面的happens-before规则,显然,通常只须要使用volatile关键字,或者使用锁的机制,就能实现内存的可见性了。
有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。
为何会出现不一致的状况呢?
这是因为重排序的缘故。
在Java内存模型中,容许编译器和处理器对指令进行重排序,可是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
举个例子:
线程A:
context = loadContext();
inited = true;
线程B:
while(!inited ){
sleep
}
doSomethingwithconfig(context);
若是线程A发生了重排序:
inited = true;
context = loadContext();
那么线程B就会拿到一个未初始化的content去配置,从而引发错误。
由于这个重排序对于线程A来讲是不会影响线程A的正确性的,而若是loadContext()方法被阻塞了,为了增长Cpu的利用率,这个重排序是可能的。
若是要防止重排序,须要使用volatile关键字,volatile关键字能够保证变量的操做是不会被重排序的。