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


前言

从本篇博文开始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种操作来完成,虚拟机实现时必须保证每一种操作都是原子的、不可再分的。

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

8种操作的实现规则:

  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字段的值,无须同步就能被其他线程正确访问

  1. 时序性问题

    线程在引用变量时不能直接从主内存引用,如果工作内存中内有该变量,则会从主内存拷贝一个副本到工作内 存中,即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多线程与锁


此致,敬礼

Android并发编程 开篇的更多相关文章

  1. Android并发编程 多线程与锁

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的. 前言 前一篇Android并发编程开篇呢,主要是简单介绍一下线程以及 ...

  2. Android并发编程 原子类与并发容器

    在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的. 前言 上一篇博文中,主要说了些线程以及锁的东西,我们大多数的并发开发需求,基本上可以用synch ...

  3. Android并发编程之白话文详解Future,FutureTask和Callable

    从最简单的说起Thread和Runnable 说到并发编程,就一定是多个线程并发执行任务.那么并发编程的基础是什么呢?没错那就是Thread了.一个Thread可以执行一个Runnable类型的对象. ...

  4. Android并发编程

    Android的并发编程,即多线程开发,而Android的多线程开发模型也是源于Java中的多线程模型.所以本篇也会先讲一些Java中的多线程理念,再讲解具体涉及的类,最后深入Android中的并发场 ...

  5. Java并发指南开篇:Java并发编程学习大纲

    Java并发编程一直是Java程序员必须懂但又是很难懂的技术内容. 这里不仅仅是指使用简单的多线程编程,或者使用juc的某个类.当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中 ...

  6. Qt on Android 核心编程

    Qt on Android 核心编程(最好看的Qt编程书!CSDN博主foruok倾力奉献!) 安晓辉 著   ISBN 978-7-121-24457-5 2015年1月出版 定价:65.00元 4 ...

  7. Java并发编程实战---第六章:任务执行

    废话开篇 今天开始学习Java并发编程实战,很多大牛都推荐,所以为了能在并发编程的道路上留下点书本上的知识,所以也就有了这篇博文.今天主要学习的是任务执行章节,主要讲了任务执行定义.Executor. ...

  8. 《C++ 并发编程》- 第1章 你好,C++的并发世界

    <C++ 并发编程>- 第1章 你好,C++的并发世界 转载自并发编程网 – ifeve.com 本文是<C++ 并发编程>的第一章,感谢人民邮电出版社授权并发编程网发表此文, ...

  9. 并发编程(一):从头到脚解读synchronized

    一.目录 1.多线程启动方式 2.synchronized的基本用法 3.深度解析synchronized 4.同步方法与非同步方法是否能同时调用? 5.同步锁是否可重入(可重入锁)? 6.异常是否会 ...

随机推荐

  1. [leetcode]99. Recover Binary Search Tree恢复二叉搜索树

    Two elements of a binary search tree (BST) are swapped by mistake. Recover the tree without changing ...

  2. 【转载】Sql Server参数化查询之where in和like实现详解

    文章导读 拼SQL实现where in查询 使用CHARINDEX或like实现where in 参数化 使用exec动态执行SQl实现where in 参数化 为每一个参数生成一个参数实现where ...

  3. javaweb开发.调试

    一.快速调试一个类 1.类里面写public static void main(String[] args) throws Exception{}方法 2.该类上右键->Run As->J ...

  4. vue-if和v-show区别

    vue-if和vue-show都是对条件进行判断,一直不明白他们的区别,知道看到了官方文档中的解释: v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和 ...

  5. Linux 下编译 有多个子程序文件的Fortran程序

    第一种方法 ifort -o outprogram Source1.f90 Source2.f90 第二种 在主程序中include 'Source2.f90' program main call p ...

  6. 2019.02.11 bzoj3165: [Heoi2013]Segment(线段树)

    传送门 题意简述:要求支持两种操作: 插入一条线段. 询问与直线x=kx=kx=k相交的线段中,交点最靠上的线段的编号. 思路: 直接上李超线段树即可. 代码: #include<bits/st ...

  7. APP微信支付报错《商户号该产品权限未开通,请前往商户平台>产品中心检查后重试》

    问题 最近项目使用MUI,HBuilder.开发打包H5的app 在开发H5 plus支付的时候,遇到以下问题: App微信支付调官方的统一下单接口返回错误信息 {return_msg=商户号该产品权 ...

  8. sshpass 使用方法

    首先下载sshpass并且安装 $ tar -zxvf sshpass-1.06.tar.gz $ cd sshpass-1.06 $ ./configure --prefix=/usr/local/ ...

  9. orabbix监控oracle

    Orabbix 是一个用来监控 Oracle 数据库实例的 Zabbix 插件.下载地址: http://www.smartmarmot.com/product/orabbix/download/ O ...

  10. web服务器原理(作业四)

      Web服务器简介:Web服务器是指驻留于因特网上某种类型计算机的程序.当web浏览器(客户端)连到服务器上并请求文件时,服务器将处理该请求并将文件发送到该浏览器上,附带的信息会告诉浏览器如何查看该 ...