线程、多线程、线程池、进程

线程

线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,
即不同的线程可以执行同样的函数。

  • 同步:

在一个线程执行,先执行完了前面的代码,才会执行后面的代码,是阻塞的。

  • 异步:

开启一个新的线程执行,不会等前面的代码执行完,就会执行后面的代码,是非阻塞的。

  • 什麽是main(UI)线程:

android启动的第一个线程。主要负责处理ui和事件的工作。

特别注意
更新ui只能在ui线程进行,不可以在其他线程更新ui,否则会崩溃。
在ui线程不可以做耗时操作,比如网络请求等,如果做耗时操作,就会阻塞ui线程,就会导致界面卡顿。会出现ANR(application not response、应用无响应)。

  • 异步通讯:

那么我们要请求网络或者其他耗时操作的时候怎么办?这就涉及到异步通讯或者叫线程通讯。

先在子线程加载数据,做耗时操作,然后把取得的数据传递给ui线程,让ui线程来更新ui。

  • 线程通讯的方式:

Handler(可以理解成消息获取的工具,非官方翻译)

sleep阻塞

来自Thread,没有释放锁,只能等时间到自动唤醒,或者用interrupt()强行打断,必须捕获异常

面试题:Thread.sleep()会导致ANR吗?

首先,先明白一个问题:什么是 ANR

Application Not Responding,意思是” 应用没有响应 “

以前我的理解就是 “在主线程做了耗时操作” 就会引起 ANR,现在我觉得我是错误的,ANR 的意思是应用没有响应,耗时操作实际上 并不一定会导致没有响应,我对没有响应的理解是

有人(事件或操作)发出了一个请求,但是主线程没有对这个人进行反馈(可能是没时间、可能是不想理、可能是手被绑住了没有办法理你),
这个叫没有响应

那举个例子

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // do some blablabla...
  
        Log.d("test", "准备sleep30秒")
        Thread.sleep(30000)
        Log.d("test", "sleep30秒完成")

        // do some blablabla...

    }

这段代码在 onCreate 中 sleep 了 30 秒,会出现 ANR 吗?

答案是

可能会,也可能不会

当主线程在 Sleep 的时候,如果 UI 线程不需要进行操作,也就是说没有消息会发送给 UI 线程并要求 UI 线程进行处理的时候,Sleep 30 秒就不会导致 ANR,因为没有出现 ANR(应用没有响应)的情况啊,没有人向线程请求什么东西,也就不需要响应了,既然没有响应了,那怎么会有 ANR 呢?

但是,线程在 Sleep 的时候,主线程有接收到需要处理的请求的时候

需要注意的是,需要处理的请求,不一定只是用户的手动触摸,也有可能是其他线程需要对线程进行 UI 更新的请求,这个时候 UI 线程正在 Sleep,根本没有办法理你(不想理你),这就符合了ANR的条件,所以会出现 ANR(比如说在这 30 秒内,点击了 返回按钮,就会出现 ANR)。

wait阻塞

来自Object,释放了锁,需要notify或者notifyAll唤醒,不需要捕获异常

面试题:怎么中断一个线程?

1、调用stop方法,不安全,已弃用
2、调用interrupt方法,通知线程应该中断了

  • 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出了一个InterruptedException异常。
  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将正常运行,不受影响。
    请添加图片描述

线程间通信

  • handler
  • runOnUiThread
  • View.post(Runnable r)
  • AsyncTask.onPostExecute

handler机制

handle的post方法,会传递一个runnable进去,然后会调用getpostmessage方法
在这里插入图片描述
这个方法里会调用message的obtain方法获取到一个message对象,然后将这个runnable封装进这个message
在这里插入图片描述
在这里插入图片描述
然后回到前面,会调用sendMessageDelayed方法
在这里插入图片描述
会调用sendMessageAtTime方法
在这里插入图片描述
这时候会拿到messageQueue对象,里面会调用handler的enqueueMessage方法,入参就是这个messageQueue对象和message对象

在这里插入图片描述
在handler的enqueueMessage方法里给这个message打个标签,然后调用messageQueue的enqueueMessage方法
在这里插入图片描述
然后判断一下message的标签是不是为空和message是不是在使用,然后就把message放到messageQueue里
在这里插入图片描述
可以看handler.post方法的注解,无非就是把执行ui操作的runnablw封装成mesaage,再添加到messagequene中,添加成功就返回true,添加失败返回false

looper就是不停的检查是否有新消息,有就调用handleMessage提取并处理消息,只有主线程默认开启。

Looper.loop():让Looper开始工作,从消息队列里取消息,处理消息。注意:因为loop()方法是个循环,所以写在loop方法后面的代码不会执行,可以通过mHandler.getLooper().quit()结束循环,之后的代码才会执行。
在这里插入图片描述

面试题:主线程和子线程怎么用Handler进行通信?

1、如果主线程需要将消息发送到子线程,需要创建子线程的Handler,启用子线程Looper,在主线程调用子线程的Handler对象发送消息。

  Thread thread = new Thread(){
            @Override
            public void run() {
                super.run();
                //初始化Looper
                Looper.prepare();
                //在子线程内部初始化handler即可,发送消息的代码可在主线程任意地方发送
                handler=new Handler(){
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                      //所有的事情处理完成后要退出looper,即终止Looper循环
                        //这两个方法都可以,有关这两个方法的区别自行寻找答案
                        handler.getLooper().quit();
                        handler.getLooper().quitSafely();
                    }
                };
              
                //启动Looper循环
                Looper.loop();
            }
        };
        thread.start();

2、如果子线程需要将消息发送到主线程,需要获取主线程的Handler对象,在子线程调用主线程的Handler对象发送消息。

//在主线程中初始化Handler
  private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        //在此处处理消息
        }
    };


 Thread thread = new Thread(){
            @Override
            public void run() {
                super.run();
                //在子线程中发送消息
                handler.sendEmptyMessage(0);
            }
        };
  thread.start();

上述代码中,在初始化handler前后均有两句关键代码,一定不能遗忘,否则你会收到以下异常

Can't create handler inside thread " + Thread.currentThread()+ " that has not called Looper.prepare()
面试题:为什么主 / 子线程通信,必须用对方线程的Handler或者对方线程的Looper创建Handler来发送消息?

因为message是存放在MessageQuene中的,Looper是用来不断轮询处理message的,而Looper是用 ThreadLocal 修饰的,ThreadLocal:即线程隔离,只被自己所属线程使用,所以必须用对方线程的Handler或者对方线程的Looper创建Handler来发送消息。

面试题:子线程中new Handler会发生什么?

会报异常(RuntimeException)。原因是:子线程中默认没有创建Looper对象,必须调用Looper.prepare()启用Looper。

面试题:handler的post()和sendMessage()的区别?

没区别。post方法只是多了个将Runnable封装成Message的过程。

面试题:Message的new方法和obtain方法的区别?
  • new方法
    每次需要Message对象的时候都创建一个新的对象,每次都要去堆内存开辟对象存储空间,对象使用完后,jvm又要去对这个废弃的对象进行垃圾回收

  • obtain方法
    采用单链表,减少了每次获取Message时去申请空间的时间。同时,这样也不会永无止境的去创建新对象,减小了Jvm垃圾回收的压力,提高了效率

原理图解:
在这里插入图片描述
咱们对着源码来看:
假设该链表初始状态如下
在这里插入图片描述
执行Message m = sPool;就变成下图
在这里插入图片描述
继续sPool = m.next;
在这里插入图片描述
然后m.next = null;
在这里插入图片描述
接下来m.flags=0;sPoolSize–;return m;便是表示m指向的对象已经从链表中取出并返回了。

再看回收recycle():
然后再看看sPoolSize是什么时候自增的。按图索骥便可找到recycle()方法和recycleUnchecked()方法。前者供开发者调用进行回收,后者执行回收操作。来看看回收操作都干了啥:

void recycleUnchecked() {    // Mark the message as in use while it remains in the recycled object pool.    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = -1;    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {       if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

前半段不必多说,显然是“重置”改对象的个个字段。后半段又是一个同步代码段,同样用图来解释一下(假设当前代码为message.recycle(),则需要被回收的则是message对象)。
假设当前链表如下:
在这里插入图片描述
执行next=sPool;
在这里插入图片描述
执行sPool=this;
在这里插入图片描述
现在可以很清楚的看到,Message类本身就组织了一个栈结构的缓冲池。并使用obtain()方法和recycler()方法来取出和放入。

面试题:asyncTask、service、intentService、handler、thread和handlerThread的区别?
  • asyncTask
    适用于数据轻量,且需要及时在主线程刷新UI的时候

  • service和intentService
    service不是单独的进程,它和应用程序在同一个进程,它也不是一个线程,所以它不能直接处理耗时任务,需要开启一个单独的线程来处理。如果在onStartCommand()方法里处理耗时任务,很容易引起ANR。

    intentService继承Service,内部自动开启一个工作线程来处理耗时任务,省去了Service手动开线程的麻烦;而且它处理完任务还会自动停止,不用像Service那样需要手动停止服务。

    适用于没有UI操作,有大量数据需要同步到本地的时候。例如同步用户数据、聊天数据等。

    如果对IntentService的了解仅限于此,会有种IntentService很鸡肋的观点,因为在Service中开线程进行耗时操作也不麻烦

    但是IntentService还有一个特点,就是多次调用onHandleIntent函数(也就是有多个耗时任务要执行),多个耗时任务会按顺序依次执行。原理是其内置的Handler关联了任务队列,Handler通过looper取任务执行是顺序执行的。

    这个特点就能解决多个耗时任务需要顺序依次执行的问题。而如果仅用service,开多个线程去执行耗时操作,就很难管理

  • handler、thread和handlerThread
    handler负责处理消息,通过它可以实现子线程与主线程通讯
    thread是一个线程
    handlerThread继承thread,内部封装了handler。
    适用于长时间的任务。例如加载数据到界面。

并发编程的关键

  • 分工
    关于分工,常见的 Executor,生产者-消费者模式,Fork/Join 等,这都是分工思想的体现。

    强调的是性能。

  • 同步
    一个线程执行完任务,如何通知后续线程执行。

    强调的是性能。

  • 互斥
    当多个线程同时访问一个共享变量/成员变量时,就可能发生不确定性,造成不确定性主要是有可见性、原子性、有序性这三大问题,而解决这些问题的核心就是互斥,即:同一时刻,只允许一个线程访问共享变量。

    强调的是正确性。

并发编程中的三个概念

原子性问题,可见性问题,有序性问题。

1.原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

同样地反映到并发编程中会出现什么结果呢?

举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

i = 9;

假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

2.可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

举个简单的例子,看下面这段代码:

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

3.有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

关于指令重排

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4

这段代码有4个语句,那么可能的一个执行顺序是:

那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值