1、线程池的优点? 四种线程池的使用场景,线程池的几个参数的理解?
1) 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或则“过度切换”的问题,归纳总结就是
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
2)Android 中的线程池都是直接或间接通过配置ThreadPoolExecutor 来实现不同特性的线程池.Android 中最常见的类具有不同特性的线程池分别为:
a. newCachedThreadPool:只有非核心线程,最大线程数非常大,所有线程都活动时会为新任务创建新线程,否则会利用空闲线程 ( 60s 空闲时间,过了就会被回收,所以线程池中有 0 个线程的可能 )来处理任务.
优点:任何任务都会被立即执行(任务队列SynchronousQuue 相当于一个空集合);比较适合执行大量的耗时较少的任务.
b. newFixedThreadPool:只有核心线程,并且数量固定的,所有线程都活动时,因为队列没有限制大小,新任务会等待执行,当线程池空闲时不会释放工作线程,还会占用一定的系统资源。
优点:更快的响应外界请求
c. newScheduledThreadPool:核心线程数固定,非核心线程(闲着没活干会被立即回收数)没有限制.
优点:执行定时任务以及有固定周期的重复任务
d. newSingleThreadExecutor:只有一个核心线程,确保所有的任务都在同一线程中按序完成
优点:不需要处理线程同步的问题
3) 通过源码可以了解到上面的四种线程池实际上还是利用ThreadPoolExecutor 类实现的
2、Android 中还了解哪些方便线程切换的类?
1 ) AsyncTask:底层封装了线程池和 Handler,便于执行后台任务以及在子线程中进行 UI 操作。
2 ) HandlerThread:一种具有消息循环的线程,其内部可使用Handler。
3 ) IntentService:是一种异步、会自动停止的服务,内部采用HandlerThread。
3、AsyncTask 的原理
1 ) AsyncTask 中有两个线程池(SerialExecutor 和THREAD_POOL_EXECUTOR)和一个 Handler
(InternalHandler),其中线程池 SerialExecutor 用于任务的排队,而线程池 THREAD_POOL_EXECUTOR 用于真正地执行任务,InternalHandler 用于将执行环境从线程池切换到主线程。
2 ) sHandler 是一个静态的 Handler 对象,为了能够将执行环境切换到主线程,这就要求 sHandler 这个对象必须在主线程创建。由于静态成员会在加载类的时候进行初始化,因此这就变相要求 AsyncTask 的类必须在主线程中加载,否则同一个进程中的 AsyncTask 都将无法正常工作。
4、IntentService 的作用
IntentService 可用于执行后台耗时的任务,当任务执行完成后会自动停止,同时由于 IntentService 是服务的原因,不同于普通 Service,IntentService 可自动创建子线程来执行任务,这
导致它的优先级比单纯的线程要高,不容易被系统杀死,所以IntentService 比较适合执行一些高优先级的后台任务。
5、直接在 Activity 中创建一个 thread 跟在 service 中创建一个 thread 之间的区别?
1) 在 Activity 中被创建:该 Thread 的就是为这个 Activity 服务的,完成这个特定的 Activity 交代的任务,主动通知该 Activity一些消息和事件,Activity 销毁后,该 Thread 也没有存活的意义了。
2) 在 Service 中被创建:这是保证最长生命周期的 Thread 的唯一方式,只要整个 Service 不退出,Thread 就可以一直在后台执行,一般在 Service 的 onCreate()中创建,在 onDestroy()中
销毁。所以,在 Service 中创建的 Thread,适合长期执行一些独立于 APP 的后台任务,比较常见的就是:在 Service 中保持与服务器端的长连接。
6、ThreadPoolExecutor 的工作策略
ThreadPoolExecutor 执行任务时会遵循如下规则
1)如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务。
2) 如果线程池中的线程数量已经达到或则超过核心线程的数量,那么任务会被插入任务队列中排队等待执行。
3) 如果在第 2 点无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果在线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务。
4)如果第 3 点中线程数量已经达到线程池规定的最大值,那么就拒绝执行此任务ThreadPoolExecutor 会调用RejectedExecutionHandler 的 rejectedExecution 方法来通知调用者。
7、Handler、Thread 和 HandlerThread 的差别?
1) Handler:在 android 中负责发送和处理消息,通过它可以实现其他支线线程与主线程之间的消息通讯。
2) Thread:Java 进程中执行运算的最小单位,亦即执行处理机调度的基本单位。某一进程中一路单独运行的程序。
3)HandlerThread:一个继承自 Thread 的类 HandlerThread,Android 中没有对 Java 中的 Thread 进行任何封装,而是提供了一个继承自 Thread 的类 HandlerThread 类,这个类对 Java的 Thread 做了很多便利的封装。HandlerThread 继承于Thread,所以它本质就是个 Thread。与普通 Thread 的差别就在于,它在内部直接实现了 Looper 的实现,这是 Handler 消息机制必不可少的。有了自己的 looper,可以让我们在自己的线程中分发和处理消息。如果不用 HandlerThread 的话,需要手动去调用 Looper.prepare()和 Looper.loop()这些方法。
8、ThreadLocal 的原理
1) ThreadLocal 是一个关于创建线程局部变量的类。使用场景如下所示:
a. 实现单个线程单例以及单个线程上下文信息存储,比如交易 id 等。
b. 实现线程安全,非线程安全的对象使用 ThreadLocal 之后就会变得线程安全,因为每个线程都会有一个对应的实例。 承载一些线程相关的数据,避免在方法中来回传递参数。
2)当需要使用多线程时,有个变量恰巧不需要共享,此时就不必使用 synchronized 这么麻烦的关键字来锁住,每个线程都相当于在堆内存中开辟一个空间,线程中带有对共享变量的缓冲区,通过缓冲区将堆内存中的共享变量进行读取和操作,ThreadLocal 相当于线程内的内存,一个局部变量。每次可以对线程自身的数据读取和操作,并不需要通过缓冲区与 主内存中的变量进行交互。并不会像 synchronized 那样修改主内存的数据,再将主内存的数据复制到线程内的工作内存。ThreadLocal 可以让线程独占资源,存储于线程内部,避免线程堵塞造成 CPU 吞吐下降。
3)在每个 Thread 中包含一个 ThreadLocalMap,ThreadLocalMap 的 key 是 ThreadLocal 的对象,value 是独享数据。
9、多线程是否一定会高效(优缺点)
1) 多线程的优点:
a 方便高效的内存共享 - 多进程下内存共享比较不便,且会抵消掉多进程编程的好处
b 较轻的上下文切换开销 - 不用切换地址空间,不用更改CR3 寄存器,不用清空 TLBc 线程上的任务执行完后自动销毁
2) 多线程的缺点:
a 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512KB)
b 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
c 线程越多,cpu 在调用线程上的开销就越大
d 程序设计更加复杂,比如线程间的通信、多线程的数据共享
3)综上得出,多线程不一定能提高效率,在内存空间紧张的情况下反而是一种负担,因此在日常开发中,应尽量
a 不要频繁创建,销毁线程,使用线程池
b 减少线程间同步和通信(最为关键)
c 避免需要频繁共享写的数据
d 合理安排共享数据结构,避免伪共享(false sharing)
e 使用非阻塞数据结构/算法
f 避免可能产生可伸缩性问题的系统调用(比如 mmap)
g 避免产生大量缺页异常,尽量使用 Huge Page
h 可以的话使用用户态轻量级线程代替内核线程
10、多线程中,实现一个单例
1)多线程中建立单例模式考虑的因素有很多,比如线程安全 -延迟加载-代码安全:如防止序列化攻击,防止反射攻击(防止反射进行私有方法调用) -性能因素
2)实现方法有多种,饿汉,懒汉(线程安全,线程非安全),双重检查(DCL),内部类,以及枚举
11、开启线程的三种方式?
1)继承 Thread 类,重写 run()方法,在 run()方法体中编写要完成的任务 new Thread().start();
2) 实 现 Runnable 接 口 , 实 现 run() 方 法 new Thread(new MyRunnable()).start();
3)实现 Callable 接口 MyCallable 类,实现 call()方法,使用 FutureTask 类来 包装 Callable 对象,使用 FutureTask 对象作为 Thread 对象的 target 创建并启 动线程;调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值。
12、run()和 start()方法区别 ?
run()方法只是线程的主体方法,和普通方法一样,不会创建新的线程。只有调 用 start()方法,才会启动一个新的线程,新线程才会调用 run()方法,线程才会 开始执行。
13、在 Java 中 wait 和 seelp 方法的不同
1)wait()方法属于 Object 类,调用该方法时,线程会放弃对象锁,只有该对象调 用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
2)sleep()方法属于 Thread 类,sleep()导致程序暂停执行指定的时间,让出 CPU, 但它的监控状态依然保存着,当指定时间到了又会回到运行状态,sleep()方法 中线程不会释放对象锁。
14、谈谈 wait/notify 关键字的理解
1)notify: 唤醒在此对象监视器上等待的单个线程
2)notifyAll(): 通知所有等待该竞争资源的线程 wait: 释放 obj 的锁,导致当前的线程等待,直接其他线程调用此对象的 notify() 或 notifyAll()方法
3)当要调用 wait()或 notify()/notifyAll()方法时,一定要对竞争资源进行加锁,一般放到synchronized(obj)代码中。当调用 obj.notify/notifyAll 后,调用线程 依旧持有 obj 锁,因此等待线程虽被唤醒,但仍无法获得 obj 锁,直到调用线程 退出 synchronized 块,释放 obj 锁后,其他等待线程才有机会获得锁继续执行。
15、什么导致线程阻塞?
(1)一般线程阻塞
1)线程执行了 Thread.sleep(int millsecond)方法,放弃 CPU,睡眠一段时间, 一段时间过后恢复执行;
2)线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等 到获取到同步锁,才能恢复执行;
3)线程执行了一个对象的 wait()方法,直接进入阻塞态,等待其他线程执行 notify()/notifyAll()操作;
4)线程执行某些 IO 操作,因为等待相关资源而进入了阻塞态,如 System.in, 但没有收到键盘的输入,则进入阻塞态。
5)线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会 让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行 态,随时可能再次分得 CPU 时间。线程自闭,join()方法,在当前线程调用另一个线程的 join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前 线程再由阻塞转为就绪态。
6)线程执行 suspend()使线程进入阻塞态,必须 resume()方法被调用,才能使 线程重新进入可执行状态。
16、线程如何关闭?
1 ) 使用标志位
2)使用 stop()方法,但该方法就像关掉电脑电源一样,可能会发生预料不到的 问题
3)使用中断 interrupt()
但调用 interrupt()方法只是传递中断请求消息,并不代表要立马停止目标线程。
17、Java 中对象的生命周期
1)创建阶段(Created):为对象分配存储空间,开始构造对象,从超类到子 类对 static 成员初始化;超类成员变量按顺序初始化,递归调用超类的构造方法, 子类成员变量按顺序初始化,子类构造方法调用。
2)应用阶段(In Use):对象至少被一个强引用持有着。
3)不可见阶段(Invisible):程序运行已超出对象作用域
4)不可达阶段(Unreachable):该对象不再被强引用所持有
5)收集阶段(Collected):假设该对象重写了 finalize()方法且未执行过,会 去执行该方法。
6)终结阶段(Finalized):对象运行完 finalize()方法仍处于不可达状态,等待 垃圾回收器对该对象空间进行回收。
7)对象空间重新分配阶段(De-allocated):垃圾回收器对该对象所占用的内 存空间进行回收或再分配,该对象彻底消失。