chapter10 多线程与并发编程java
操做系统中,进程process和线程thread,process是一个计算机程序中运行实例。一个计算机能够建立多个process,这些process运行状态各不相同,有本身的独立的地址空间,包括程序内容和数据。它们间是互相隔离的。process拥有各类资源和状态信息,包括打开的文件,子进程和信号处理器等。thread表示的是程序的执行流程,是cpu调度执行的基本单位unit。thread有本身的程序计数器,寄存器,堆栈和帆等。同一个进程中的线程共享相同的地址空间,同时共享进程所拥有的内在和其余资源。引入thread来提升程序的运行性能。一个程序中主要存在cpu计算和io操做。io操做相对cpu操做来讲比较耗时,并且不少是阻塞式的。当一个thread所执行的io操做被阻塞时,同一进程中的其余thread可使用cpu来进行计算。不一样os和编程语言中对thread的使用方式有很大的区别,因此对于跨平台的多线程程序来讲是一个很大的挑战。但java平台经过jvm解决了跨平台的问题,使得相同api开发的程序在不一样平台上都可以正确运行。
编程
java.lang.Process /java.lang.ProcessBuilder类是进程,java.lang.Thread是表示线程。在JVM启动后,一般只有一个普通的thread来运行程序的代码,这个主要用来启动java类的main方法来运行。程序在运行时能够根据须要建立新的thread并启动它来执行。除了普通thread外还有一类是守护thread(daemon thread)。daemon thread通常在后台运行,提供程序运行时的服务,当jvm中运行的全部thread都是 daemon thread时,jvm终止运行。
api
thread表示一段程序的执行过程。继承 thread类并重写run方法。另外一种是实现runnable接口中的run方法。thread启动是调用start运行,当thread代码逻辑执行完后,thread自动结束。
缓存
10.1. 可见性 多线程
在一个多thread程序中,multi thread经过共同协做来完成指定的任务。thread间须要进行数据交换以协调各自的状态。同一process中各个thread经过共享内存方式进行通讯。这些thread共享进程的地址空间,因此均可以自由的访问所需的内存位置。互相协做的thread间对共享内存位置达成一致。一个thread在适当时候修改该内存的值,另一个thread在后续操做中经过读取相同内存位置来获得修改后的值。java中代码没法直接操做内存,而是经过不一样类型的变量来间接访问。thread a ,thread b共同协做完成任务,thread b须要等待threada完成后才能继续运行,两个thread可使用一个boolean类型的变量来协调状态。when threada完成后修改该变量的值为true,经过threadb。threadb运行时若是读到该变量的值为true,就开始本身的操做。使用共享内存方式在multi-thread中可能形成可见性相关的问题,即一个thread所做的修改对其余thread并不可见,致使其余thread仍然使用错误的值。好比threadb看不见threada对boolan状态值的修改,形成threadb一直等待下去。
架构
单thread来讲,可见性很容易理解和验证。先改变一个变量的值,再读取该变量的值,那么所读取的值必定是上次写入操做写的值。也就是说前面的写入操做结果对后面的读取操做是确定可见的。但在multi-thread中则不必定成立,若是不使用某些互斥或同步机制,则不能保证一个thread所写入的值对另外一个thread是可见的,若是可见性条件不能成立,则程序运行过程当中就会出现问题。
并发
可见性问题总结 1 多thread 实际执行顺序
app
2 cpu采用层次结构的多级缓存架构。提供L 1,L2,L3三级缓存,有时读取数据时会直接从cache中读取,致使数据的可见性问题 jvm
10.2 java memeory model java内存模型是一个抽象模型,只关注主存中的共享变量,屏蔽cpu及缓存等细节,简化内存模型自身的定义。对象的实例域,静态域和数据元素存储在堆内存中,能够被多个thread共享。编程语言
multithread中thread所执行的动做分为两类,一类是thread内部动做,好比操做方法听局部变量等,另一种是thread间的动做。thread间动做由一个thread产生,同时被另外一个thread检测至或受另外一thread的直接影响。包括读取和写入共享变量以及加锁和解锁等同步操做。thread间的动做不会产生可见性相关的问题,所以内存模型只考虑thread间动做。
常见同步关系以下所示,A和B保持同步含义是A必然发生在B前。
1 在一个监视器对象上的解锁动做与相同对象上后续成功的加锁动做保持同步
2对一个声明volatile变量的写入动做与同一变量的后续读取操做保持同步
3 启动一个thread的动做与该thread执行的第一个动做保持同步
4向thread共享变量写入默认值动做与该thread执行的第一个动做保持同步。这种同步关系的含义是在thread运行以前,该thread所使用的所有对象从概念上说已经被建立出来,而且对象中变量被设置为默认值。这种同步关系是保证thread所看到的变量值是肯定的。变量的值多是根据变量类型肯定的默认值,也多是其余thread所设置的值,但不多是其余的值
5threada运行时最后一个动做与另外一thread中任何能够检测到threada终止的动做保持同步
6若是threada中断threadb,则threada的中断动做与任何其余thread检测至threadb处于被中断状态的动做保持同步
还有一种同步关系是 在以前发生happens-before。它包括
1若是两个动做a和b在一个thread中执行,同时在程序顺序中a出如今b以前,则a在b以前发生
2一个对象的构造方法的结束在该对象的finalize方法运行以前发生
3若是动做a和动做b保持同步,则a在b以前发生
4 若是动做a在动做b以前发生,同时动做b在动做c以前发生,则a在c以前发生,也就是说“在以前发生”顺序是传递性的。
数据竞争的存在是多thread运行时发生错误的根源。编写多thread首要任务是找出并解决程序中存在的数据竞争。
10.3. volatile volatile用来对共享变量的访问进行同步。对一个volatile变量的上一次写入和下一次读取之间存在“在以前发生”的顺序。也就是说上一次写入的操做结果对下一次读取的操做是确定可见的。在写入volatile变量值以后,cpu缓存中的内容会被写回主存,在读取volatile变量时,cpu缓存中的对应内容被置为失效状态,从新从主存中进行读取。将变量声明为volatile至关于为单个变量的读取和写入添加了同步操做。但volatile在使用时不须要利用锁机制,所以性能要优于synchronized关键词。关键词volatile的主要做用是确保对一个变量的修改能正确被传播到其余thread中。最多见的使用场景是把循环结束的判断条件声明为volatile。
如threada和threadb,threada在一个循环中不断进行处理,threadb负责向threada发送中止处理信号。threada调用worker类的对象work方法,开始执行具体任务。在适当的时候,threadb会调用同一worker类的对象setdone方法来声明终止任务的执行。把done声明为volatile是很重要的。只有这样才能保证threadb对done变量所作的修改对于threada的后续操做是可见的。不然threada可能因为没法看到done变量值的变化而一直运行下去。
public class Worker{
private volatile boolean done;
public void setDone(boolean done){
this.done = done;
}
public void work(){
while(!done){
//perfome task here
}
}
}
使用volatile场景受限,写入的变量新值与该变量当前值没有关系,可使用,其它场景不可使用。
10.4 final
final声明为类时没法被继承,声明为方法时没法在字类中被重写。从内存模型角度来讲,final关键词最重要的是声明一个域的值只能被初始化一次,并在初始化以后,该域的值没法被修改。在multi-thread中final一般用来实现不可变对象immutable object。当对象中共享变量的值不可能发生变化时,在multi-thread访问时就不会出现问题,也就不须要使用thread间的同步机制来进行处理。java中最多见的不可变对象是String类的对象。在多thread中应该尽可能可能使用不可变对象,以免使用同步机制。如下代码把类中全部域声明为final,并在构造方法中进行init。
public class User{ private final String name; private final String email; public User(String name,String email){ this.name = name; this.email = email; } }
在构造方法成功完成以前,要确保正在建立的对象的引用不会被其余thread访问到,不然其余thread可能看到部分建立完成的对象。下面是一个错误的示例。在构造方法中,把当前对象的引用赋值给另外一类的静态变量会致使另外一类的thread看到还没有建立完成的类的对象。该对象包含的变量可能没有被初始化成正确的值。
public class WrongUser { private final String name; public WrongUser(String name){ UserHolder.user = this; this.name = name; } }
若是一个thread是在对象的构造方法成功完成以后才经过该对象的引用来进行访问,则该thread确定能够看到对象中final域被初始化以后的值。若是域没有被声明为final,则在构造方法完成以后,其余thread不必定能够看到这个域被初始化以后的值,而有可能看到域的默认址。因为final域具备这些特征,编译器对final域的处理很灵活。这些域可能被随意地与其余代码进行从新排列。在代码执行时,final域的值能够被保存在寄存器中,而不用在主存中频繁从新读取。
10.5 atom operation 原子操做
在java中,对于非long型和double型的域的读取和写入操做是原子操做。对象引用的读取和写入操做也是原子操做。好比读取一个int类型的域时,该域对应的内存地址中32位内容会被完整读取,在读取过程当中不会被其余thread打断。在进行写入时也不会被中断。在写入非volatile的long型和double型的域时,分红两次操做来完成,一个long型或double型的域长度是64位,每次写入32位,在一个thread写入long型或double型的域的前32位以后,在写入后32位以前,另外thread有可能访问到这个域的值,从而读取只完成部分写入操做的错误值。所以在多thread中使用long型和double型的共享变量时,须要把变量声明为volatile,以保证读取和写入操做的完整性。