知识点梳理3 多线程

2018-07-27  本文已影响4人  48d1753082c0

在 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虚拟机栈(JVM Stacks)

本地方法栈(Native Method Stacks)

Java堆(Heap)

image

方法区(Method Area)

运行时常量池(Runtime Constant Pool)

image.png

注:

直接内存(Direct Memory)

参考: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

上一篇下一篇

猜你喜欢

热点阅读