调度原理
线程调度模型分为分时调度模型(均分CPU时间)和抢占式调度模型(优先级高的获取,JVM采用)
Android的线程调度由nice值和cgroup来决定。
- nice值在Process中定义,值越小,优先级越高,默认是0
- cgroup:借鉴了Linux,实现更严格的群组调度策略,保证前台线程可以获取更多的CPU。既允许后台线程执行任务,也不会对前台线程造成太大影响。手动设置优先级较低和不在前台运行的应用程序线程会被移到后台群组中。
- 线程过多会导致CPU频繁切换,降低线程运行效率
- 正确认识任务重要性决定哪种优先级
- 优先级具有继承性。所以尤其在UI线程中创建子线程的时候要记得设置优先级。
异步方式
- new Thread:不推荐。频繁创建销毁开销大,复杂场景不易使用
- handlerThread 自带消息循环。串行执行。长时间运行,不断从队列中获取任务
- intentService:基于handlerThread。异步,不占用主线程,优先级较高
- asyncTask:内部基于线程池实现,不需要自己处理线程切换
- 线程池:易复用,功能强大,定时,并发数等
- RXJava:不同类型区分:自带IO密集型和CPU密集型线程区分
线程使用准则
- 禁止new Thread
- 提供基础线程池给各个业务线,避免各个业务线维护一套线程池,导致线程数过多
- 根据任务类型选择合适的异步方式:如优先级低,长时间执行,可以使用handlerThread.定时任务时可以优先用线程池来操作
- 创建线程必须命名:Thread.currentThread.setName()运行时动态的修改从线程池统一创建的名字
- 关键异步任务监控:异步不等于不耗时。如果一些关键的任务异步后导致失败率增加,则需要进行调整。具体监控方式可以采用AOP
- 重视优先级设置:Process.setThreadProority();,可以设置多次
锁定收敛
- 背景:项目变大之后要收敛线程
- 项目源码,三方库,aar中都有线程创建
- 避免恶化的一些监控预防手段 方案:
引入epic库
implementation 'me.weishu:epic:0.11.1'
HOOK thread 的构造函数,并且将相应的堆栈信息打出来,从而可以知道具体的线程创建和堆栈
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
ThreadUtils.threadList.add(new WeakReference<Thread>(thread));//将所有的thread统一收拢到同一个集合中
LogUtils.i(thread.getName()+" stack "+Log.getStackTraceString(new Throwable()));
}
});
把所有线程收拢到一个统一的集合中。然后将集合中的线程按照优先级排序。
Collections.sort(ThreadUtils.threadList, new MyComparator());
public class MyComparator implements Comparator<WeakReference<Thread>> {
@Override
public int compare(WeakReference<Thread> o1, WeakReference<Thread> o2) {
if (o1.get() != null && o2.get() != null) {
return o1.get().getPriority() - o2.get().getPriority();
}
return 0;
}
}
在项目中的线程完成使用准则改造之后,集合中的每个线程就可以定位到具体的开发负责人。对threadList进行抽样(用户抽样和节点抽样两个维度)上传,具体上传触发节点可以是具体的业务节点:如直播间开启PK时候,电商APP进入到详情页之后,或者检测到app比较卡顿的时候等。这个时候将APP目前正在存活的具体的thread信息上传到服务端,然后进行具体分析。
线程优化
减少业务线程
线上用户Thread信息采集是为了APP性能优化做铺垫,具体优化点可包括
1.了解APP一些关键节点时候APP线程开启情况,并且将每个线程对应到具体的业务。看是否有线程使用不当,是否及时销毁,是否可降级融合等优化手段等。从而优化因线程开启销毁,线程切换等造成的性能问题。
2.线程降级。在一个大型项目中,总是会有很多次要线程。这些线程的特点一般是长期在后台存活,轮训做着一些check的工作等。比如HandlerThread就经常用来做长时间在后台运行的线程,但是这类线程往往是辅助地位的。当项目中线程过多,并且造成主线程无法及时获得CPU时间片时,这个时候需要手动关闭一些次要线程,释放资源,以提高APP性能。
具体哪些线程要动态降级需要结合业务。下面以一个轮训县城举例。
HandlerThread handlerThread = new HandlerThread("testThread");
handlerThread.start()
handlerThread.setPriority(Thread.*MIN_PRIORITY*);
mHandler = new Handler(handlerThread.getLooper()) {
@Override
public void dispatchMessage(Message msg) {
super.dispatchMessage(msg);
// check 逻辑
if (msg.what == 1) {
handler.sendEmptyMessageDelayed(1, 2000);
}
}
};
mHandler.sendEmptyMessage(1);
如果检测到APP卡顿严重,CPU比较难获取到时间片。这个时候可以选择关闭一些次要线程。
for (int i = 0; i < ThreadUtils.*threadList*.size(); i++) {
if (ThreadUtils.*threadList*.get(i).get() == null) {
continue;
}
if ("testThread".equals(ThreadUtils.*threadList*.get(i).get().getName())) {
WeakReference<Thread> threadWeakReference = ThreadUtils.threadList.get(i);
if (threadWeakReference.get() == null) {
continue;
}
if (threadWeakReference.get().isAlive() && threadWeakReference.get() instanceof HandlerThread) {
HandlerThread handlerThread = (HandlerThread) threadWeakReference.get();
handlerThread.quitSafely();
Log.i("tag5", "关闭线程testThread");
}
}
if ("testThread2".equals(ThreadUtils.threadList.get(i).get().getName())) {
WeakReference<Thread> threadWeakReference = ThreadUtils.threadList.get(i);
if (threadWeakReference.get() == null) {
continue;
}
if (threadWeakReference.get().isAlive()) {
Thread thread2 = threadWeakReference.get();
thread2.interrupt();
Log.i("tag5", "关闭线程testThread2");
}
}
}
同时在执行完线程退出逻辑之后,将列表中的销毁的线程移除。
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
Iterator<WeakReference<Thread>> it = ThreadUtils.threadList.iterator();
while (it.hasNext()) {
WeakReference<Thread> weakReference = it.next();
Thread x = weakReference.get();
try {
if (!x.isAlive()) {
it.remove();
Log.i("tag5", "遍历删除" + x.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, 1500);
在分析线程的时候,发现有一个线程SharedPreferencesImpl-load出境率比较高。 Android源码在getSharedPreferences(String name,int model)获取sp对象的时候会开启一个线程来从磁盘读取数据。同时,如果model==Context.MODE_MULTI_PROCESS,会再次开启一个名为 SharedPreferencesImpl-load的线程。所以如果sp不是在多进程中使用的话,mode建议改成MODE_PRIVATE。同时因为大型项目中各个业务线基本都会用到SP。所以SharedPreferencesImpl-load的创建和销毁相比其它线程会比较频繁。建议可以使用MMKV替换SP方案。MMKV在跨进程数据存储,性能和定性方面都有不错的表现。
//Android 10 SP源码
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
优化线程上下文切换
线程的7种状态
- 新建
- 就绪
- 运行
- 终止
- 等待
- 定时等待
- 阻塞
当一个线程的时间片用完,或者因自身原因被迫暂停运行,此时另一个线程会被操作系统选中来占用处理器。切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是上下文。 包括
- 寄存器的存储内容:CPU寄存器负责存储已经、正在和将要执行的任务
- 程序计数器存储的指令内容:程序计数器负责存储CPU正在执行的指令位置、即将执行的下一条指令的位置
当CPU数量远远不止1个的情况下,操作系统将CPU轮流分配给线程任务,此时的上下文切换会变得更加频繁,并且存在跨CPU的上下文切换,更加昂贵。
切换带来的开销有:
- 操作系统保存和恢复上下文
- 调度器进行线程调度
- 处理器高速缓存重新加载
- 可能导致整个高速缓存区被冲刷,从而带来时间开销 优化线程上下文切换:
- 锁优化减少竞争
- 减少锁持有时间,将与锁无关的代码移出同步代码块
- 锁分离:读写锁。减少锁的颗粒度。如ConcurrentHashMap采用的分段锁
- 非阻塞乐观锁代替竞争锁。volatile 读写不会导致上下文切换, CAS无锁算法,也不会导致上下文切换
- wait/notify优化,wait/notify使用导致了过多的上下文切换,建议使用Lock+Condition(更灵活,更精确)代替synchronized+wait/notify/notifyAll,来实现等待通知
- 设置合理的线程数量。区分IO密集型和CPU密集型线程池,发挥线程的最大效率
- 减少GC频率
- 使用协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
系统级优化--协程(Coroutine)
在操作系统早起,没有线程的概念。而初期的线程是在用户空间下实现的。应用程序自己在用户空间实现线程的创建,维护和调度。这种好处是操作系统不支持线程,也可以通过库函数来支持线程。在JDK 1.1中的绿色线程,就是在用户态完成调度。但是这种方式虽然减少了因为线程调度而引发的用户态和内核态上下文切换造成的性能损耗,但会因为某个线程的耗时阻塞进程。所以在JDK 1.2的时候,Linux中的JVM基于pthread实现,java中的线程和操作系统的线程实现1:1的关系。
每个线程都有自己的上下文信息,上下文包括线程ID,栈,程序计数器等。而每创建一个线程,单栈资源就要消耗1M左右,再加上因为线程阻塞和运行切换导致的性能损耗一直也是优化的重点。当线程运行在用户空间处于用户态,运行内核空间时候处于内核态。内核态下,运行的代码不受任何限制,可以自由访问任何有效地址。在用户态时,代码要受到CPU的很多检查,比如:用户只能访问映射地址空间页表中规定的用户态下可访问的虚拟地址。
系统接口调用,进程管理,内存管理,虚拟文件系统,网络协议栈,硬件驱动等都属于内核态。 应用程序调用C函数库(glibc)属于用户态。
以32位操作系统为例,它的寻址空间为2*32次方=4G。以linux操作系统为例,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间
协程是运行在线程之上,同一线程之上许多协程互相切换,比起线程阻塞损耗切换效果好很多。协程和系统的线程是m:n的映射关系。而且协程按需加载资源,相比线程要优化不少。
- 协程的性能消耗对比Thread微乎其微
- 更重要的是它带来了一种新的编程方式,让异步编程不再复杂
所以相比于线程,我们在开发中还是与时俱进,kotlin中的协程是一大开发利器,我们可以在开发中逐步的替换线程,这样可以将Google提供的系统级的优化手段应用到项目中。
一个线程上可以运行多个协程。协程支持挂起.且协程在执行的时候可以指定具体的线程。这样可以更好的区分CPU密集型和IO密集型线程,从而保证线程的更高效。如一个thread1中执行了coroutine1和coroutine2.在coroutine1执行了deley(2000),指定延迟2秒后再执行,thread1不会因为coroutine1的延迟而阻塞,而是thread1去执行coroutine2。然后等延时时间到了再去执行coroutine1。而如果是Thread.sleep()的话,则线程会进入阻塞态,而用户态和内核态之间的转换是较消耗性能的。
线程Proxy
前面epic方案虽然能hook掉new Thread的创建,但是还有很多不完善的地方。比如AsyncTask的创建,比如线程池中正在运行的runnable所对应的堆栈等。对于这些,epic无法准确的获取,所以为了更精确的获取项目中线程的信息,需要借助ASM进行字节码插桩,来获取更精确的线程运行信息。
对于线程池的Runnable
通过ASM进行字节码插桩,代理线程池中的Runnable,Callable。复写run()方法,在run方法前后获取对应的具体的thread信息。
override fun run() {
val info = updateThreadInfo()
(any as Runnable).run()
info.callStack = ""
}
对于Thread
建议代理Thread.复写其中的start方法和run方法。在start方法中获取其线程信息。在run方法中移除队列中的线程id信息,因为Runnable的时候还会再次添加。
@Synchronized
override fun start() {
val callStack = TrackerUtils.getStackString()
Log.d(LOG_TAG, "proxy callStack $callStack")
super.start()
val info = ThreadInfoManager.INSTANCE.getThreadInfoById(id)
info?.also {
it.id = id
it.name = name
it.state = state
if (it.callStack.isEmpty()) {
it.callStack = callStack
it.callThreadId = currentThread().id
}
Log.d(LOG_TAG, "proxy callStack id $id")
} ?: apply {
val newInfo = ThreadInfo()
newInfo.id = id
newInfo.name = name
newInfo.callStack = callStack
newInfo.callThreadId = currentThread().id
newInfo.state = state
newInfo.startTime = SystemClock.elapsedRealtime()
ThreadInfoManager.INSTANCE.putThreadInfo(id, newInfo)
Log.d(LOG_TAG, "proxy callStack newInfo id $id")
}
Log.d(LOG_TAG, "proxy callStack end ")
}
override fun run() {
super.run()
ThreadInfoManager.INSTANCE.removeThreadInfo(id)
}
新建MonitorThreadTransform在其中完成对模块目录和jar包中thread的替换。
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
def startTime = System.currentTimeMillis()
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
if (outputProvider != null)
outputProvider.deleteAll()
System.out.println("transform threads...")
inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput directoryInput ->
System.out.println("handleDirectoryInput")
handleDirectoryInput(directoryInput, outputProvider)
}
input.jarInputs.each { JarInput jarInput ->
System.out.println("handleJarInputs")
handleJarInputs(jarInput, outputProvider)
}
def cost = (System.currentTimeMillis() - startTime) / 1000
System.out.println("ThreadTrackerTransform cost : $cost s")
}
}
通过复写ClassVisitor修改项目和jar包中的代码,动态替换thread的父类,并将其重新打包
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
changingSuper = false;
buildingPackage = false;
className = name;
Log.i("tag5","MonitorThreadClassVisitor visit ");
System.out.println("MonitorThreadClassVisitor visit");
if (filterClass(className)) {
super.visit(version, access, name, signature, superName, interfaces);
return;
}
if (!buildingPackage && (access & Opcodes.ACC_SUPER) > 0) {
switch (superName) {
case S_Thread:
changingSuper = true;
System.out.println("MonitorThreadClassVisitor visit 222");
super.visit(version, access, name, signature, S_ProxyThread, interfaces);
return;
}
}
super.visit(version, access, name, signature, superName, interfaces);
}
修改thread的继承关系,让其继承自我们自己定义的proxyThread.
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (name.equalsIgnoreCase("<init>")) {
switch (owner) {
case S_Thread:
System.out.println("visitMethodInsn 改继承");
mv.visitMethodInsn(opcode, S_ProxyThread, name, descriptor, false);
return;
}
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
对于线程池
复写线程池,将四种创建线程池的方法进行替换。同时替换掉对应的runnable。 对于new线程池的创建方式,也是采用类似替换new Thread的方式。 复写runnable的run方法,方便定位具体的runnable是来自什么堆栈。通过对runnable的代理,从而可以详细分析出线程池中运行线程的runnable的堆栈,从而可以对应到具体的业务
引用方式:
apply plugin: 'com.yxy.monitorthread'
classpath 'com.github.flyingbrave.monitorthread:monitorplugin:1.0.6'
implementation 'com.github.flyingbrave.monitorthread:monitormodel:1.0.6'
目前项目demo只列举了new Thread一种替换,后续会不断完善线程池,AsyncTask,HandlerThread等多种线程代理。方便全方位了解我们的项目