Android并发编程 开篇

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽可能按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深刻理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其余的优质博客,在此向各位大神表示感谢,膜拜!!!java


前言

从本篇博文开始Android并发编程系列。因为笔者水平有限,若是博文之中有任何错误或者纰漏之处,还请不吝赐教。程序员

Java线程

在Android SDK中并无提供新颖的线程实现方案,使用的依旧是JDK中的线程。在Java中开启新线程有3中常见的方式编程

  1. 继承自Thread类,重写run()方法
public class ThreadA extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getName());
    }
}
//测试的主线程
public class Main {
    public static void main(String[] args){
        ThreadA threadA = new ThreadA();
        threadA.setName("threadA");
        threadA.start();
        System.out.println("主线程"+Thread.currentThread().getName());
    }
}
  1. 实现Runnable接口,实现run()方法
public class ThreadB implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

//测试的主线程
public class Main {
    public static void main(String[] args){
        ThreadB threadB = new ThreadB();
        //注意这里启动的方式跟方式1不同
        Thread thread = new Thread(threadB);
        thread.setName("threadB");
        thread.start();
        System.out.println("主线程"+Thread.currentThread().getName());
    }
}
  1. 实现Callable接口,实现call()方法
public class ThreadC implements Callable<String> {
    @Override
    public String call() throws Exception {
        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Thread.currentThread().getName();
    }
}

public class Main {
    public static void main(String[] args){
        ThreadC threadC = new ThreadC();
        //FutureTask 后续会讲到,先知道有怎么个实现方式
        FutureTask<String> feature = new FutureTask<>(threadC);
        //注意启动方式有点不同;
        Thread thread1 = new Thread(feature);
        thread1.setName("threadC");
        thread1.start();
        //注意细细体会这个,只有主线程get了,主线程才会继续往下面执行
        try {
            System.out.println(feature.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("主线程"+Thread.currentThread().getName());
    }
}

JMM(Java 内存模型)

上面简单的介绍了3种开启线程的方式,接下来咱们来看一下Java的内存模型,由于后续文章讲到的许多知识都须要这个做为基础。缓存

主内存与工做内存

JMM规定JVM有主内存(Main Memory)和工做内存(Working Memory),主内存其实就是咱们日常所说的Java堆内存,存放全部类实例变量等,这部份内存是多个线程共享的;工做内存里存放的则是线程从主内存拷贝过来的变量以及访问方法获得的临时变量,这部份内存为线程私有,其余的线程不能访问。多线程

注:上面所说的拷贝并非拷贝整个对象实例到工做内存,虚拟机可能拷贝对象引用或者对象字段,而不是整个对象。

主内存与工做内存的关系以下图所示
并发

主内存与工做内存间的交互操做

主内存与工做内存之间具体的交互协议,被定义了如下8种操做来完成,虚拟机实现时必须保证每一种操做都是原子的、不可再分的。app

  1. lock,锁定,所用于主内存变量,它把一个变量标识为一条线程独占的状态。
  2. unlock,解锁,解锁后的变量才能被其余线程锁定。
  3. read,读取,所用于主内存变量,它把一个主内存变量的值,读取到工做内存中。
  4. load,载入,所用于工做内存变量,它把read读取的值,放到工做内存的变量副本中。
  5. use,使用,做用于工做内存变量,它把工做内存变量的值传递给执行引擎,当JVM遇到一个变量读取指令就会执行这个操做。
  6. assign,赋值,做用于工做内存变量,它把一个从执行引擎接收到的值赋值给工做内存变量。
  7. store,存储,做用域工做内存变量,它把工做内存变量值传送到主内存中。
  8. write,写入,做用于主内存变量,它把store从工做内存中获得的变量值写入到主内存变量中。

8种操做的实现规则:ide

  1. 不容许read和load、store和write操做之一单独出现,即不容许加载或同步工做到一半。
  2. 不容许一个线程丢弃它最近的assign操做,即变量在工做内存中改变了以后,必须吧改变化同步回主内存。
  3. 不容许一个线程无缘由地(无assign操做)把数据从工做内存同步到主内存中。
  4. 一个新的变量只能在主内存中诞生。
  5. 一个变量在同一时刻只容许一条线程对其进行lock操做,但lock操做能够被同一条线程重复执行屡次,,屡次lock以后必需要执行相同次数的unlock操做,变量才会解锁。
  6. 若是对一个对象进行lock操做,那会清空工做内存变量中的值,在执行引擎使用这个变量前,须要从新执行load或assign操做初始化变量的值。
  7. 若是一个变量事先没有被lock,就不容许对它进行unlock操做,也不容许去unlock一个被其余线程锁住的变量。

对一个变量执行unlock操做以前,必须将此变量同步回主内存中(执行store、write)。函数

并发编程中的根本问题以及JMM提供的解决方案

整个并发编程所遇到的问题能够说是如下三个问题的变种。测试

  1. 原子性问题
    由Java内存模型提供的8个原子性操做所支持,Long和Double的读写大部分商业虚拟机上已实现为原子性操做,更大范围的原子性操做,Java内存模型还提供了lock和unlock操做来支持,在字节码层次提供了monitorenter和monitorexit来隐式的使用这两个操做,反映到java代码中就是同步代码块了 synchronize。
  2. 可见性问题
    由上图主内存与工做内存的关系图可知,线程不与主内存进行直接交互,而是把主内存的实例变量拷贝一份到线程的工做内存中进行操做,而后再同步给主内存。之因此这样作,是由于工做内存大都由高速缓存、寄存器这类比主内存存取速度更快的内存担当,以便弥补CPU速度与主内存存取速度不在一个数量级的差距。

    注:当线程操做某个对象时,执行顺序以下:
    1 从主存复制变量到当前工做内存(read -> load)
    2 执行代码改变共享变量的值(use -> assign)
    3 用工做内存的数据刷新主存相关内容(store -> write)
    因此单个线程与线程的工做内存之间就有了相互的隔离效果,专业术语称之为“可见性问题”

    可见性是指当一个线程修改了共享变量的值,其余线程可以当即得知这个修改,可见性由volatile支持,除了volatile之外,synchronize和final关键字,synchronize的可见性是由”对一个变量执行unlock操做以前,必须先把此变量同步回主内存中“这条规则保证的,而final关键字是指当final修饰的字段在构造函数中一旦初始化完成,而且构造器没有把this的引用传递出去,那在其余线程中就能看见final字段的值,无须同步就能被其余线程正确访问

  3. 时序性问题
    线程在引用变量时不能直接从主内存引用,若是工做内存中内有该变量,则会从主内存拷贝一个副本到工做内 存中,即read -> load ,完成后线程会引用该副本。当同一个线程再度引用该字段时,有可能从新从主内存获取变量副本(read -> load -> use),也有可能直接引用原来的副本(use),也就是说read、load、use 顺序能够有JVM实现系统决定。这个时候线程与线程之间操做的前后顺序,就会决定你程序对主内存最后的修改是否是正确的,专业术语称之为“时序性问题”。
    Java提供了volatile和synchronize两个关键字来保证线程之间操做的有序性,synchronize是由“一个变量在同一时刻只容许一条线成对其进行lock操做”。

HP(happens-before)

在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做之间必需要存在happens-before关
系。这里提到的两个操做既能够是在一个线程以内,也能够是在不一样线程之间。
与程序员密切相关的happens-before规则以下。

  • 程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:若是A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:若是线程A执行操做ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操做happens-before于线程B中的任意操做。
  • join()规则:若是线程A执行操做ThreadB.join()并成功返回,那么线程B中的任意操做happens-before于线程A从ThreadB.join()操做成功返回。
注意
两个操做之间具备happens-before关系,并不意味着前一个操做必需要在后一个操做以前执行!happens-before仅仅要求前一个操做(执行的结果)对后一个操做可见,且前一个操做按顺序排在第二个操做以前(the first is visible to and ordered before the second)。

JMM的happens-before规则不但简单易懂,并且也向程序员提供了足够强的内存可见性保证


本篇总结

本篇文章简单分析了下线程的启动方式以及JMM模型,为后面的文章铺垫一下。


下篇预告

Java多线程与锁


此致,敬礼

相关文章
相关标签/搜索