以前我们知道创建线程的两种方式:
本文再来讲讲另外两种创建线程的方式:
callable接口 1、有什么特点? 2、为什么要用Callable接口?
3、怎么用?
思考:Thread类的构造只能接受Runnable接口,并不能接口Callable接口,怎么办? 接下来看看编码实现:
main方法:
计算出的结果为: ![]() 这个结果是正确的。那么问题来了:
大家注意看运行结果中的两个 hello 是什么时候输出的: ![]()
也就是说,输出两个 hello 并不需要用到 call 方法 的返回值,所以即使还没算完,也应该可以正常输出,而不是被阻塞。所以,get方法的调用要放在最后并且等计算完了再get。那么如何保证计算完了呢?看如下代码:
把get方法的调用放在最后面,并且用while进行自旋操作,如果没计算完,就会一直在while循环中。看看这次的运行结果: ![]() 可以看到,这次main线程并没被阻塞,运行后立即完成了自己该做的事。 注意上面的call方法里有一行注释掉的输出语句,以及main方法里有一个注释掉的线程BB。如果把注释放开,其实也还是只有AA线程会进去,BB线程根本就调不到call方法。也就说,多个线程共用一个 futureTask,只会进去一次。 线程池 1、为什么要用线程池? 2、如何使用线程池?
第一个是线程池中线程数固定的,第二个就是线程池中只有一个线程,第三个就是带缓存的,线程数量可变。这三个底层用的都是ThreadPoolExecutor。
运行结果: ![]()
3、线程池的7大参数:
`
可以发现,最后两个参数用的默认值,不需要我们传,所以我们刚才只看到5个。下面来说说这7大参数都是干嘛的。
那么这些参数到底什么意思呢?举个生活当中的例子让你秒懂: 今天星期天,你去银行办理业务。由于星期天,所以只开放了两个窗口,所以corePoolSize就是2。如果刚好来了两个人,那么能满足需求。如果不止两个人,那么其他人就得在等待区等着,这个等待区就是workQueue。如果等待区也坐满了人,那么当值人员就会打电话给经理,让经理叫人加班多开窗口,假如这个银行总共有5个窗口,那么这个5就是maximumPoolSize。如果开了5个窗口还是忙不过来,等待区还是爆满,那么大堂经理就会对办理业务的人说:不好意思,我们这里忙不过来了,请您去别的网点吧。这就是拒绝策略。当办业务的人慢慢的少了,来加班的那几个窗口如果超过了keepAliveTime时间都还没有人来办理业务,那么他们就会下班。也就是说,两个窗口忙得过来就不会劳烦别人加班。 4、线程池的拒绝策略:
5、上面说到三个最常见的线程池,生产中使用哪个? 因为FixedThreadPool和 SingleThreadExecutor 底层用的阻塞队列是 LinkedBlockingQueue,这个队列是有界,但是最大值是 int 的最大值,21亿多,相当于无界。也就是说可能会在这里堆积大量的任务,造成OOM。CachedThreadPool本身线程数就可变,允许的最大线程数也是 int 的最大值,所以可能会创建大量的线程,最终造成OOM。 既然都不用, 那我们如何创建线程池?答案是我们使用 ThreadPoolExecutor,手动配置那7个参数。如下:
既然我们说人家Executors创建的线程池不行,那么我们创建的怎么就行呢?那些参数怎么来的?corePoolSize设置为多少、maximumPoolSize又设置为多少才合适?请看下面的线程池参数配置。 6、线程池参数配置:
CPU密集型要尽可能的减少线程数量,一般公式:
IO密集型则应尽可能多的配置线程,一般公式:
获取CPU核心数的方式:
死锁问题 1、什么是死锁? 2、写一个死锁:
测试:
看运行结果: ![]()
3、死锁定位分析:
这条命令可以查看进程号,Java也提供了类似的命令,那就是:
用这条命令查看到Java进程号后,找到可能出现异常的进程,再输入:
这样就可以查看到详细信息了。以上面的代码为例,先在cmd中进入项目路径,输入上面的命令。 ![]() 这样就说明这是死锁了。 |
|