《java7核心技术与最佳实践》读书笔记之 multi-thread

原创
2014/03/24 10:23
阅读数 136

chapter10 多线程与并发编程

    操作系统中,进程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终止运行。

    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 实际执行顺序

         2 cpu采用层次结构的多级缓存架构。提供L 1,L2,L3三级缓存,有时读取数据时会直接从cache中读取,导致数据的可见性问题 

       

 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,以保证读取和写入操作的完整性。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部