Callable接口
Runnable和Callable的区别
- Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
- Callable规定的方法是call(),Runnable规定的方法是run()
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)
- call方法可以抛出异常,run方法不可以
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
- 加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。
Callable接口
callable
有个<V>
,这个V
就是call
函数的返回值类型
1 | package java.util.concurrent; |
FutureTask类
1 | /** |
1 | /** |
RunnableFuture接口
1 | package java.util.concurrent; |
测试
1 | public class CallableTest { |
当某个请求需要在后端完成 N 次统计结果时,我们就可以使用该方式创建 N 个线程进行(并行)统计,而不需要同步等待其他统计操作完成后才统计另一个结果。
线程池
线程池3个常用方式
Executors.newFixedThreadPool
1 |
|
Executors.newSingleThreadExecutor
1 | /** |
Executors.newCachedThreadPool
1 | /** |
线程池7大参数
Java 多线程开发之 Callable 与线程池(三) - 后端 - 掘金
int corePoolSize 线程池核心线程个数,默认线程池线程个数为 0,只有接到任务才新建线程
int maximumPoolSize 线程池最大线程数量
long keepAliveTime 线程池空闲时,线程存活的时间,当线程池中的线程数大于 corePoolSize 时才会起作用
TimeUnit unit 时间单位
BlockingQueue
workQueue 阻塞队列,当达到线程数达到 corePoolSize 时,将任务放入队列等待线程处理 ThreadFactory threadFactory 线程工厂
RejectedExecutionHandler handler 线程拒绝策略,当队列满了并且线程个数达到 maximumPoolSize 后采取的策略
1 |
|
线程池的合理配置
获取系统设备处理器核数
1 | Runtime.getRuntime().availableProcessors() |
考虑因素
CPU密集型
线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间
IO密集型
线程池的工作原理
execute方法执行逻辑
核心线程数
如果当前运行的线程少于corePoolSize
,则会创建新的线程来执行新的任务
加入阻塞队列
如果运行的线程个数等于或者大于corePoolSize
,则会将提交的任务存放到阻塞队列workQueue
中
扩容
如果当前workQueue
队列已满的话,则会创建新的线程来执行任务
拒绝策略
如果线程个数已经超过了maximumPoolSize
,则会使用饱和策略RejectedExecutionHandler
来进行处理
JVM
常见故障
StackOverflowError
1 | public class StackOverflowErrorDemo { |
结果
1 | Exception in thread "main" java.lang.StackOverflowError |
Java heap space
1 | import java.util.Random; |
运行
配置参数
结果
1 | Exception in thread "main" java.lang.OutOfMemoryError: Java heap space |
GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead limit 错误会发生什么情况呢?
那就是GC清理的这么点内存很快会再次填满,迫使GC再次致谢,这样就形成了恶性循环,CPU使用率一直是100%,而GC却没有任何成果
1 | import java.util.ArrayList; |
运行参数配置
结果
1 | [GC (Allocation Failure) [PSYoungGen: 2048K->480K(2560K)] 2048K->916K(9728K), 0.0043924 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
Direct buffer memory
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后统一一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一下场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
ByteBuffer.allocate(capability)第一种方式分配JVM堆内存,属于GC管辖范围,由于需要拷贝索引速度相对较慢。
ByteBuffer.allocateDirect(capability)第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝索引速度相对较快。
但如果不断分配本地内存,堆内存很少使用,那么JVM久不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemory,那程序就直接崩溃了。
1 | import java.nio.ByteBuffer; |
运行参数配置
1 | -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m |
结果
1 | 配置的maxDirectMemory:5.0MB |
unable to create new native thread
高并发请求服务器时,经常出现如下异常: java.lang.OutOfMemoryError: unable to create new native thread,准确地讲该native thread异常与对应的平台有关
导致原因:
你的应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载极限
你的应用创建超过这个数量,就会报java.lang.OutOfMemoryError: unable to create new native thread
解决办法:
想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
对于有的应用,确实需要创建很多线程,远超过Linux系统的默认1024个线程的限制,可以通过修改Linux服务器配置,扩大Linux默认限制
1 | public class UnableCreateNewThreadDemo { |
编译运行
1 | [es@192 tmp]$ javac -d . UnableCreateNewThreadDemo.java |
以root用户登录开启另外一个终端
1 | [root@192 ~]# jps |
修改用户线程数
1 | [root@192 ~]# ulimit -u |
Metaspace
Java8及之后的版本使用Metaspace来替代永久代。
Metaspace是方法区在HotSpot的实现,它与永久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存,也即在java8中,classe metadata(the virtual machines internal presentation of java class),被存储在叫做Metaspace的native memory
永久代(java8后被元空间Metaspace取代了)存放了一下信息:
- 虚拟机加载类信息
- 常量池
- 静态变量
- 即时编译后的代码
模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的
1 | import net.sf.cglib.proxy.Enhancer; |
运行配置参数
1 | -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m |