第一次写blog,主要是感觉每天自己总结自己学习的知识,复习程度不够。所以就想着分享一些每天学习时思考的问题,一般只分享解决了的问题。其实我学习并不喜欢死板的跟着网课或者书一步一步的做,我喜欢加入自己的思考。比如今天学习的线程池实际上我在之前学习java基础时已经学习过了,但是当时类似那种硬灌进去的,知道了知识,并没有理解,我认为这是没有深度的,这只能算是会用,了解,知道。并不能算是掌握,搞懂了,接下来是我今天的思考:
首先我昨天学习了异步任务AsyncTask,理解了他的用法并有一些自己的思考。然后因为它过时废弃了,虽然你现在初始化它上面并没有划横,但他下面源码是有废弃注解的如图:
所以就有了今天的学习。我首先搜索了AsyncTask废弃后的替代方法
得到了Executor,ThreadPoolExecutor,FutureTask的结果
由此,正片开始:(下面的思考基于Android 线程池框架、Executor、ThreadPoolExecutor详解_苍痕的博客-CSDN博客这篇博文的介绍)
今天的思考主要基于这两段代码及其改编:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
}
});
}
以及:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
在去尝试去实现前两个方法的测试代码时对线程的执行有了疑问,不是很清楚到底是怎么执行的
经过思考,有了一个大概的运行流程:
主线程与分线程是相互独立的
这句话意思我指的是他们执行的任务是相对独立的,主线程就执行主线程也就行正常的onCreate里面的语句。而分线程只执行run方法里面的语句
因此在1的代码模版下for循环中
Thread.sleep永远都是主线程执行,分线程永远只执行run方法里面的那一句打印log
免的回去往上看,我直接粘过来代码:
for (int i = 0 ; i < 10 ; i++){
final int index = i;
try {
Log.d("yang线程池execute前",index+Thread.currentThread().getName());
Thread.sleep(index*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
Log.d("yang",index+Thread.currentThread().getName());
}
});
}
目前我的认知是这样的,一会询问一下师傅
按照这个逻辑去测试2的代码模块,也就是for循环里直接就是分线程,我们稍微优化一下:
for (int i = 0 ; i < 10 ; i++){
final int index = i;
Log.d("yang分线程执行前",index+Thread.currentThread().getName());
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Log.d("yang",index+Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
好的,那么先不运行,我先预测一手结果
第一个Log语句永远都是主线程
然后第二个语句因为用的cachedPool,那么他会连着初始化10个线程,而且一瞬间执行完
果然如此(流量不够不传图了)
那么在此基础上我加点东西,我把主线程每次执行完一次睡1000ms怎么样
直接加,老规矩预测一波:
我猜是肯定不是10个,绝对少,而且估计只有3或4个,来看一下结果
最后出来,只开了俩线程,一下就顺通了对吧,一个分线程睡2s,主线程一次睡1s,那她自然只能开2个分线程
那么改为2000如何,直接猜,只有一个分线程,结果却是两个分线程,为什么?
我怀疑是上次刚睡完主线程刚好新建,改为2001如何,还是俩线程,那说明理解不太对
重新捋一下逻辑:
主线程第一次执行,睡完2s创建了第一个分线程,然后主线程开始睡第二个2s,分线程开始睡第一个2s。我觉得是主线程与分线程抢占中间的时间肯定大于1ms,我去看下大的log
没贴图是没流量,不过没关系,我来报数字
main第二次睡眠时间为47.232
分线程第一次打印时间也为47.232(Thread1)
主线程第三次的睡眠时间为49.235
分线程第二次睡眠时间为49.235(Thread2)
中间差了几ms估计是执行其他语句的耗时
那么按照这个逻辑他应该不需要开第二个分线程
我稍微思考,可能是执行完第一次for循环的时候,第一个分线程还没醒,那么我去在每次for循环结束时,打印一下log,主要是看for循环时间
然后惊喜的时间发生了,只剩下一个分线程了
那么应该就理清楚了,新加的这个for语句,成功实现了下一次主线程去执行execute语句时,上一次睡眠的分线程醒了
这么光说可能有点蒙,我直接报数字
但是思考了下逻辑不是很好解释清楚,稍微改下代码,现在是这样:
for (int i = 0 ; i < 10 ; i++){
final int index = i;
try {
Thread.sleep(2001);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Log.d("yang分线程执行前",index+Thread.currentThread().getName());
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Log.d("yang",index+Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Log.d("yang",i+"for循环执行完毕");
}
突然想起来我可以不用截图,粘贴结果也行,尬住了:
2023-09-20 14:41:22.767 2232-2232 yang分线程执行前 com.example.day46__androidstudy D 0main
2023-09-20 14:41:22.767 2232-2232 yang com.example.day46__androidstudy D 0for循环执行完毕
2023-09-20 14:41:22.767 2232-2264 yang com.example.day46__androidstudy D 0pool-2-thread-1
2023-09-20 14:41:24.772 2232-2232 yang分线程执行前 com.example.day46__androidstudy D 1main
2023-09-20 14:41:24.773 2232-2232 yang com.example.day46__androidstudy D 1for循环执行完毕
看着有点蒙?我来解释一下:首先看第二行,可以知道分线程的sleep是从22.767之前执行的那么应该在最晚24.767醒来对吧,而第二次执行创建分线程的时间为24.772,那么此时分线程已经醒来,自然不需要创建新的线程了,那么我们就成功解释了上面的结果
这下清楚了吧,还不清楚我就只能用纸笔画图了,不过应该没啥问题
那么换成fixed,结果如下:2023-09-20 14:45:56.352 2651-2651 yang分线程执行前
2023-09-20 14:45:56.352 2651-2651 yang分线程执行前 com.example.day46__androidstudy D 0main
2023-09-20 14:45:56.353 2651-2651 yang com.example.day46__androidstudy D 0for循环执行完毕
2023-09-20 14:45:56.353 2651-2681 yang com.example.day46__androidstudy D 0pool-3-thread-1
2023-09-20 14:45:58.359 2651-2651 yang分线程执行前 com.example.day46__androidstudy D 1main
2023-09-20 14:45:58.360 2651-2651 yang com.example.day46__androidstudy D 1for循环执行完毕
2023-09-20 14:45:58.361 2651-2684 yang com.example.day46__androidstudy D 1pool-3-thread-2
2023-09-20 14:46:00.367 2651-2651 yang分线程执行前 com.example.day46__androidstudy D 2main
2023-09-20 14:46:00.367 2651-2651 yang com.example.day46__androidstudy D 2for循环执行完毕
2023-09-20 14:46:00.368 2651-2688 yang com.example.day46__androidstudy D 2pool-3-thread-3
这个可能你就问了,哎这个不是按刚才说的,58.359的时候第一次的子线程已经醒了,为什么会用新的线程?
哎,我也不知道,不过我猜是线程已经在初始化的时候创建好了
那你还会问,哎这个123顺序是一直这样吗,我本来以为是偶然,试了好几次,发现前三次稳定123,可能依次执行吧,我也不清楚
你可能会说,你这啥都不会的还搁在写博客,别笑死人了,笑死,就是因为啥都不会才写,写完知道自己哪里概念不清楚去问师傅,我有师傅可以问,你~~
那么师傅给我了解答,首先第一个,线程池是为了最大化利用系统资源,所以说,你传进来的任务需要执行时,他优先让没有执行过任务的线程去执行任务
还有更好的解释就是去看线程池的执行策略
第二个问题:
师傅给的解释:
FixedThreadPool 是一个固定大小的线程池,它的核心线程数是固定的,因此在刚开始的时候,如果有任务提交,会使用核心线程来处理这些任务。但是,如果任务数量超过了核心线程数,那么多余的任务将会被放入等待队列中。
在 FixedThreadPool 中,如果核心线程都在忙于处理任务,多余的任务会被放入等待队列中等待执行。当核心线程空闲时,它们会从等待队列中取出任务并执行。因此,如果你一次性提交了多个任务,而且这些任务超过了核心线程数,它们会被依次放入等待队列,然后核心线程会逐个执行等待队列中的任务。
由于线程的执行速度不一定相同,所以你可能会观察到线程的执行顺序没有规律。核心线程池会根据任务的到达顺序和核心线程的可用性来决定执行的顺序。如果某个任务的执行时间较短,核心线程可能会在下一个任务到达之前执行完毕,这可能会导致任务的执行顺序看起来没有规律。
总结一下,FixedThreadPool 中的核心线程会按照任务的到达顺序来执行任务,但任务的执行顺序可能会因为核心线程的忙碌程度而产生不规律性。如果你希望任务按照某种规定的顺序执行,可以使用其他类型的线程池,如 SingleThreadExecutor 或 ScheduledThreadPoolExecutor。
师傅否定了我线程随机抢占任务的说法,应该是有某种判断空闲的状态策略
让我来尝试打印线程状态
那么先来看下线程状态:
Java 线程的生命周期中,在 Thread 类里有一个枚举类型 State,定义了线程的几种状态,分别有:
-
New
-
Runnable
-
Blocked
-
Waiting
-
Timed Waiting
-
Terminated
(图与下面状态说明来自菜鸟)
各个状态说明:
1. 初始状态 - NEW
声明:
public static final Thread.State NEW
实现 Runnable 接口和继承 Thread 可以得到一个线程类,new 一个实例出来,线程就进入了初始状态。
2. RUNNABLE
声明:
public static final Thread.State RUNNABLE
2.1. 就绪状态
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的 start() 方法,此线程进入就绪状态。
当前线程 sleep() 方法结束,其他线程 join() 结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的 yield() 方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。
2.2. 运行中状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
3. 阻塞状态 - BLOCKED
声明:
public static final Thread.State BLOCKED
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
4. 等待 - WAITING
声明:
public static final Thread.State WAITING
处于这种状态的线程不会被分配 CPU 执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
5. 超时等待 - TIMED_WAITING
声明:
public static final Thread.State TIMED_WAITING
处于这种状态的线程不会被分配 CPU 执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
6. 终止状态 - TERMINATED
声明:
public static final Thread.State TERMINATED
当线程的 run() 方法完成时,或者主线程的 main() 方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用 start() 方法,会抛出 java.lang.IllegalThreadStateException 异常。
打印出来发现执行log的线程都是runnable状态
再次修改,如下:
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Log.d("yang",index+Thread.currentThread().getName()+" "+Thread.currentThread().getState());
// 获取线程池中的线程对象
Thread[] threads = new Thread[((ThreadPoolExecutor)fixedThreadPool).getPoolSize()];
int threadCount = Thread.enumerate(threads);
System.out.println("线程池中的线程:");
for (int i = 0; i < threadCount; i++) {
Log.d("yang执行分线程sleep前",threads[i].getName()+threads[i].getState());
}
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Log.d("yang",i+"for循环执行完毕");
}
直接给结果吧:
主线程是wait状态(因为睡着了)
其他分线程都是Runnable状态
那么就按师傅说的来理解了
第三个问题,这个附带的index到底是怎么回事
就是主线程执行后,i的值已经改变,但是后面分线程去执行的时候,i还是按照正常的顺序去递增,这个怎么理解
在Java中,基本数据类型(如int)是值类型,而不是引用类型,因此它们不具有内存地址。final int是一个不可变的常量,其值在编译时就确定了,因此没有可获取的地址。在Java中,基本数据类型的变量直接包含它们的值,而不是指向内存中的对象或地址。
如果你需要获取对象的地址(引用类型的地址),可以使用System.identityHashCode(Object obj)方法。但对于基本数据类型,没有直接的方法来获取它们的地址,因为它们不是对象,不具有内存地址
最后发现,分线程虽然sleep了,但是主线程已经把对应的index常量传进去了,他的log打印语句已经编辑好了
思考了一下,不对
因为用的fixed
最多就仨线程,那么应该是线程堵塞了
然后三个线程轮流执行,谁执行到了,谁就打印相应log,也就是说index是确定的,但是后面的Thread不确定
师傅肯定了我的想法,没有问题!
那么今天的学习与思考就到这里了,对线程池以及线程执行任务的理解更深了
最后放一些我整理的知识:
线程池:
public ThreadPoolExecutor(int corePoolSize , int maximumPoolSize , long keepAliceTime , TImeUnit unit,BlockingQueue<Runnable> wokQueue,)
通过ThreadPoolExecutor类自定义。
ThreadPoolExecutor类提供了4种构造方法,可根据需要来自定义一个线程池。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 省略...
}
1、共7个参数如下:
(1)corePoolSize:核心线程数,线程池中始终存活的线程数。
(2)maximumPoolSize: 最大线程数,线程池中允许的最大线程数。
(3)keepAliveTime: 存活时间,线程没有任务执行时最多保持多久时间会终止。
(4)unit: 单位,参数keepAliveTime的时间单位,7种可选。
参数 | 描述 |
TimeUnit.DAYS | 天 |
TimeUnit.HOURS | 小时 |
TimeUnit.MINUTES | 分 |
TimeUnit.SECONDS | 秒 |
TimeUnit.MILLISECONDS | 毫秒 |
TimeUnit.MICROSECONDS | 微妙 |
TimeUnit.NANOSECONDS | 纳秒 |
(5)workQueue: 一个阻塞队列,用来存储等待执行的任务,均为线程安全,7种可选。
参数 | 描述 |
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列。 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列。 |
SynchronousQueue | 一个不存储元素的阻塞队列,即直接提交给线程不保持它们。 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列。 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列。 |
较常用的是LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
(6)threadFactory: 线程工厂,主要用来创建线程,默及正常优先级、非守护线程。
(7)handler:拒绝策略,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。
参数 | 描述 |
AbortPolicy | 拒绝并抛出异常。 |
CallerRunsPolicy | 重试提交当前的任务,即再次调用运行该任务的execute()方法。 |
DiscardOldestPolicy | 抛弃队列头部(最旧)的一个任务,并执行当前任务。 |
DiscardPolicy | 抛弃当前任务。 |
2、线程池的执行规则如下:
(1)当线程数小于核心线程数时,创建线程。
(2)当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
(3)当线程数大于等于核心线程数,且任务队列已满:
若线程数小于最大线程数,创建线程。
若线程数等于最大线程数,抛出异常,拒绝任务。