美文网首页
知识点梳理3 多线程

知识点梳理3 多线程

作者: 48d1753082c0 | 来源:发表于2018-07-27 12:46 被阅读4次

    在 Android 开发中实现多线程操作,常用方法主要有:

    继承Thread类
    实现Runnable接口
    实现callable 接口
    Handler
    HandlerThread
    IntentService
    AsyncTask

    image.png

    JVM 内存模型
    Thread 类
    runnable ,callable,task 接口
    synchronized ,volatile 这2个同步关键字
    reentrantLock,condition 重入锁这一对
    CopyOnWriteArrayList、ConcurrentHashMap 这2个并发集合容器
    然后线程池,阻塞队列

    JVM回顾

    JVM = 类加载器(classloader) + 执行引擎(execution engine) + 运行时数据区域(runtime data area)

    image.png

    运行时数据区域

    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

    image.png

    程序计数器(Program Counter Register)

    • 线程私有,它的生命周期与线程相同。
    • 可以看做是当前线程所执行的字节码的行号指示器。
    • 在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、跳转、异常处理、线程恢复(多线程切换)等基础功能。
    • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(undefined)。
    • 程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,所以此区域不会出现OutOfMemoryError的情况。

    Java虚拟机栈(JVM Stacks)

    • 线程私有的,它的生命周期与线程相同。
    • 虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型),它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
    • 该区域可能抛出以下异常:
      1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
      2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

    本地方法栈(Native Method Stacks)

    • 与虚拟机栈非常相似,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
    • 与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

    Java堆(Heap)

    • 被所有线程共享,在虚拟机启动时创建,用来存放对象实例,几乎所有的对象实例都在这里分配内存。* 对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。* Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;新生代又有Eden空间、From Survivor空间、To Survivor空间三部分。* Java 堆不需要连续内存,并且可以通过动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
    image

    方法区(Method Area)

    • 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
    • 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代(Permanent Generation)来进行垃圾回收。
    • 方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

    运行时常量池(Runtime Constant Pool)

    • 运行时常量池是方法区的一部分。
    • Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
    • 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。
    image.png

    注:

    • 在 JDK1.7之前,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;
    • 从 JDK1.7 开始HotSpot 开始移除永久代。其中符号引用(Symbols)被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。
    • 在 JDK1.8 中,永久代已完全被元空间(Meatspace)所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

    直接内存(Direct Memory)

    • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。
    • 在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和 Native 堆中来回复制数据。

    参考:https://juejin.im/post/5ad5c0216fb9a028e014fb63

    Thread


    image.png image.png image.png

    线程优先级特性:

    继承性
    比如A线程启动B线程,则B线程的优先级与A是一样的。
    规则性
    高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
    随机性
    优先级较高的线程不一定每一次都先执行完。

    守护线程

    在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
    Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

    守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:

    thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)

    在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)

    不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。

    参考: Java多线程干货系列—(一)Java多线程基础

    同步

    synchronized

    总结下对象锁的阻塞范围:
    对象锁的阻塞先于自身的同步方法,同步方法没有数量限制,一个线程正在调用对象的摸某一个同步方法,那么此时另一个线程调用这个对象的另一个同步方法也是会被阻塞的
    对象锁的不会阻塞非同步的阻塞方法,即使此时一个线程正在调用这个对象的同步方法,其他线程这个时候也是可以调用这个对象的非同步方法的
    对象锁的范围仅限自身,对象的成员变量不受外部对象锁的阻塞影响,这符合一个对象一把锁的设计思路
    静态同步方法属于类本身,不管这个类有多少个实例,同一时刻只能有一个线程操作这个类的这个静态的同步方法,和对象实例没关系,只和类有关系
    同步代码块使用 Object.class 等同于把方法标记为静态同步的
    同步代码块使用 this.class 等同于把方法标记为同步的
    synchronized 扯了半天,但是只要我们把 synchronized 搞清楚了,同步基本就没问题了,实际编码时,同步我们都是使用 synchronized 的,synchronized 玩好了就差不多成了。

    并发编程中的三原则
    可见性
    在 Java 中 volatile、synchronized 和 final 实现可见性
    原子性
    在 Java 中 synchronized 同步操作可以保证原子性
    有序性

    volatile
    保证可见性、 非同步、保证有序性、不保证原子性
    i++

    reentrantLock
    reentrantLock 、 condition 是 JAVA 1.6 时推出的

    Condition 的 await() 会阻塞当前线程,并释放锁、signal() 方法唤醒 wait 阻塞的线程。

    Condition.awiat() = Object.wait()
    Condition.signal() = Object.notify()
    Condition.signalAll() = Object.notifyAll()

    Lock类分公平锁和不公平锁,公平锁是按照加锁顺序来的,非公平锁是不按顺序的,也就是说先执行lock方法的锁不一定先获得锁

    CopyOnWriteArrayList
    读写分离的 list 集合
    ConcurrentHashMap
    多锁结构的 map 集合

    CopyOnWriteArrayList 在写操作时,先把集合数据 copy 于一份出来,然后在这个副本上对集合进行操作,计算结速后再把用副本数据覆盖原始数据,写操作是线程安全的,是同步的,同一时刻只能有一个线程操作。

    ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,另外 ConcurrentHashMap 也是读写分离的,get() 是不加锁的,put 加锁。

    image.png

    Looper,Handler,Message

    image.png

    更多内容:Android 消息机制 Handler (Java&Native)

    HandlerThread

    HandlerThread 的用法

            // 创建 HandlerThread 对象并启动 HandlerThread 所属线程,构造方法需要传线程的名字进去
            HandlerThread handlerThread = new HandlerThread("AAAAAAAA");
            handlerThread.start();
    
            // 通过 HandlerThread 对象内部的 looper 构建用于通讯的 handle
            Handler otherHandle = new Handler(handlerThread.getLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    if (msg.what == 1) {
                        Log.d("AAA", Thread.currentThread().getName() + "接受消息" + System.currentTimeMillis());
                    }
                }
            };
    
            // 执行线程间通讯任务
            otherHandle.sendMessage(Message.obtain());
    
            // 不需要了就关闭线程
            handlerThread.quit();
    

    这是 HandlerThread 声明的成员变量

    public class HandlerThread extends Thread {
        int mPriority;
        int mTid = -1;
        Looper mLooper;
        private @Nullable Handler mHandler;
        //...
    }
    

    这是核心 run 方法

        public void run() {
            mTid = Process.myTid();
            Looper.prepare();
            synchronized (this) {
                mLooper = Looper.myLooper();
                notifyAll();
            }
            Process.setThreadPriority(mPriority);
            onLooperPrepared();
            Looper.loop();
            mTid = -1;
        }
    

    在线程启动时把 looper 消息队列跑起来

    有意思的地方来了

        public Looper getLooper() {
            if (!isAlive()) {
                return null;
            }
            
            // If the thread has been started, wait until the looper has been created.
            synchronized (this) {
                while (isAlive() && mLooper == null) {
                    try {
                        // 会阻塞
                        wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
            return mLooper;
        }
    

    大家注意 getLooper() 方法是给别的线程调用的,因为 handle 的构造方法不能接受 null 的 looper 对象,要不会抛异常,所以这里在其他线程获取 HandlerThread 的 looper 对象时,若是发现此时 looper 对象是 null 的,那么就会阻塞调用 getLooper() 方法的外部线程。

    直到 run 的初始化同步代码段跑完,此时 looper 初始化完成,会主动唤醒所有阻碍在 looper 对象身上的 线程,我们再来看看 HandlerThread 的run 方法

        @Override
        public void run() {
            mTid = Process.myTid();
            Looper.prepare();
            synchronized (this) {
                mLooper = Looper.myLooper();
                // 主动唤醒所有阻碍在 looper 对象身上的 线程
                notifyAll();
            }
            Process.setThreadPriority(mPriority);
            onLooperPrepared();
            Looper.loop();
            mTid = -1;
        }
    

    好了,HandlerThread 很简单的,这里就基本完事了。我们看 HandlerThread 源码一定要理解 HandlerThread 为啥要 wait,什么时候 notifyAll 。这个是 HandlerThread 里面最值得学习的点,学会了很有用的。

    IntentService

    image.png

    AsyncTask

    先是一段线程池的参数设置,比如:
    CORE_POOL_SIZE 核心线程数,
    MAXIMUM_POOL_SIZE 最大线程数,
    KEEP_ALIVE_SECONDS 空闲线程存活时间,
    ThreadFactory 线程创建工厂,
    BlockingQueue 线程池任务队列

    然后是 2个线程池对象 sDefaultExecutor 和 THREAD_POOL_EXECUTOR,历史原因,AsyncTask 历次改版多次,得考虑版本兼容
    再后面是2个 handle 对象,大家也会问为啥有2个了把,这个也是后面说
    静态代码块中,初始化了 THREAD_POOL_EXECUTOR 这个线程池

    总结

    AsyncTask 的2个线程池,SerialExecutor 类型的 sDefaultExecutor 对象负责存储,分发任务;
    ThreadPoolExecutor 类型的 THREAD_POOL_EXECUTOR 对象负责执行任务 sDefaultExecutor 线程池会给任务对象加点料,既在任务结束时添加获取下一个任务去执行的代码,然后把这个加了料的任务抛给 THREAD_POOL_EXECUTOR 线程池对象去执行,这样一个串行循环就跑起来了,一个执行完了再去取下一个任务执行。
    ThreadPoolExecutor 类型的这线程池空有多个核心线程,其实每次都是在单线程在跑,要不怎么串行的起来,浪费了,为啥会这样恩,历史原因呗

    AsyncTask 的历史

    1.6 之前 AsyncTask 是串型执行的
    1.6 时改成并行执行的
    3.0 时改回串行执行了,因为并发执行在 刷新 UI 时可能会有问题。

    3.0 开始提供了 executeOnExecutor 方法,重点在于替换 AsyncTask 里面的默认线程池对象,使用 AsyncTask 的常量 AsyncTask.THREAD_POOL_EXECUTOR 这个线程池。

    会用 UI 线程的 looper 把 sHandler 这个对象 new 出来
    然后把 sHandler 的值赋 给 mHandler

    相关文章

      网友评论

          本文标题:知识点梳理3 多线程

          本文链接:https://www.haomeiwen.com/subject/luiwmftx.html