文章目录
创建线程
1. 实现Runnable接口
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
2. 实现Callable接口
Callable使用FutureTask包装,可以在当前线程获取Callable线程的返回值,get()方法会阻塞当前线程
,直到获取到返回值
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
3. 继承Thread类
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
继承Thread类和实现Runnable接口哪个更好?
实现接口会更好一些,因为:
Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
类可能只要求可执行就行,继承整个 Thread 类开销过大。
Thread类解析
1. 线程优先级
- 使用thread.
getPriority
(),thread.setPriority
(int)来获取、设置线程优先级 - priority的取值为
1到10
的整数,默认值为5。 - 优先级高的线程分配时间片的数量要
多于
优先级低的线程。 - 设置线程优先级时,针对
频繁阻塞
(休眠或者I/O操 作)的线程需要设置较高
优先级,而偏重计算
(需要较多CPU时间或者偏运算)的线程则设置较低
的优先级,确保处理器不会被独占。 - 操作系统可能会
忽略
线程优先级的设置。
2. Daemon(守护)线程
- 通过thread.isDaemon,thread.setDaemon(boolean)来获取、设置是否为守护线程
- 守护线程的作用是在程序运行时
在后台为其他线程提供服务
,不属于
程序中不可或缺的部分,因此当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。 - main线程属于非守护线程
- Daemon属性需要在
启动线程之前设置
,不能在启动线程之后设置 - 在构建守护线程时,
不能依靠finally块
中的内容来确保执行关闭或清理资源
的逻辑,因为守护线程可能会被JVM立即终止。
3. sleep方法
- 通过
Thread.sleep()
调用 - 休眠当前线程指定的秒数,让出CPU占用,时间到后,返回RUNNABLE状态。
4. yield方法
- 通过静态方法
Thread.yield()
调用。 通知
线程调度器,当前线程需要让出对CPU的占用,但调度器也有可能忽略
这个通知- 该方法很少使用,一般在
设计并发框架
或者调试
时使用。
5. start和run方法
- 启动新线程应当调用start方法,不能调用run方法
- 因为启动线程的过程是:线程创建后进入NEW状态,此时在当前线程调用线程2的start方法,该方法会进行一些准备操作,然后由
JVM来调用线程2的run方法
,这样两个线程就会并发执行。 - run方法知识thread的一个
普通方法调用
,还是会在原线程执行。
6. interrupted和interrupt方法
- 通过调用一个线程的
interrupt()
来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出InterruptedException
,从而提前结束该线程
。但是不能中断 I/O 阻塞和 synchronized 锁阻塞,为了能够响应中断,线程内部需要实现响应中断的逻辑,如下面第7点所示。 - 调用线程的
interrupt()
方法后,该线程的isInterrupted()
方法会返回true,相当于做了一个标记位。 - 如果线程被中断(即抛出
InterruptedException
),那么该标记位会复原,即isInterrupted()
方法会返回false.
7. 安全的终止线程
- 当外部调用线程的
interrupt()
方法后,该线程可以在内部通过Thread.currentThread.isInterrupted()
来获取中断标志位,从而实现相关中断逻辑。 - 也可以公开一个方法,供外部调用。
private static class Runner implements Runnable {
private volatile boolean on = true;
@Override
public void run() {
//获取中断标志位
while (on && !Thread.currentThread().isInterrupted())
{
//do something
}
}
//公开一个方法
public void cancel() {
on = false;
}
}
8.其他属性和方法
属性/方法 | 解释 |
---|---|
name | 线程名,方便调试 |
id | 线程id,方便调试 |
getState | 返回6个状态之一 |
Thread.currentThread() | 获取当前线程引用 |
线程间通信
1.join方法
- 当多个线程的执行结果有依赖关系时,需要调用join去协调线程的执行
- 例:在当前线程调用线程2的
join
方法,可以理解为将线程2的执行插入
到这里,然后顺序执行,当前线程必须等待线程2执行完成后,当前线程才继续执行。
public static void main(String[] args) throws Exception {
Thread thread2 = new Thread(() -> {
System.out.println("thread2");
});
thread2.start();
thread2.join();
System.out.println("main");
}
//输出为
//thread2
//main
- join(millis)可以添加一个时间参数,表示
超时时间
,如果线程2在指定的时间内没有结束,那么当前线程就不再等待
了,但还是继续并行执行
2. 等待通知机制
- notify() 通知
随机一个
在对象上等待的线程从wait()方法返回,返回的前提是该线程获取了对象的锁
,如果调用notify()的线程没有释放锁,那么调用wait()方法的线程需要等待锁的释放,获取锁后,才从wait()方法返回。 - notifyAll():通知
全部
在对象上等待的线程从wait()方法返回,返回的前提是该线程获取了对象的锁
,跟上面一样。 - wait():使线程进入WAITING状态,
并释放锁
,等待另外线程的通知或中断才返回。 - wait(millis):等待一段时间,没有通知自动返回。
public class TestWaitNotify {
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
Object lock = new Object();
Runnable waitRunnable = () -> {
synchronized (lock) {
try {
System.out.println(Thread.currentThread() + "Wait");
lock.wait();
System.out.println(Thread.currentThread() + "notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable notifyRunnable = () -> {
synchronized (lock) {
try {
System.out.println(Thread.currentThread() + "Sleep");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "notify");
lock.notify();
//while (true) {
//}
}
};
threadPool.execute(waitRunnable);
threadPool.execute(waitRunnable);
threadPool.execute(notifyRunnable);
threadPool.shutdown();
threadPool.awaitTermination(2000, TimeUnit.SECONDS);
}
}
这个例子中,两个线程等待,另外一个线程1秒后通知。
- 当使用notify()时,其中一个等待线程的wait()方法被返回。
- 当使用notifyAll()时,两个等待线程wait()方法都被返回。
- 当notify()后,不释放锁,等待线程继续等待。
线程池
1.好处
- 降低资源消耗:重复利用已创建的线程降低线程
创建和销毁
造成的消耗。 - 提高响应速度:当任务到达时,任务可以
不需要等到线程创建
就能立即执行。 - 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用线程池可以进行统一
分配、调优和监控
。
2.ThreadPoolExecutor
- ThreadPoolExecutor是Java线程池的实现类,它实现了
ExecutorService
接口,该接口定义类管理线程和线程池的方法。 - 线程池的生命周期
生命周期 | 解释 |
---|---|
RUNNING | 运行中,接受新任务,处理排队任务 |
SHUTDOWN | 关闭,不接受新任务,处理排队任务和处理中的任务 |
STOP | 停止,不接受新任务,不处理排队任务,中断处理中的任务 |
TIDYING | 整理,所有任务终止时触发 |
TERMINATED | 终止,terminal()方法执行完成 |
- 成员变量
- private final AtomicInteger
ctl
= new AtomicInteger(ctlOf(RUNNING,0))
ctl
变量通过二进制高位和地位同时表达了workerCount
和runState
的含义;workCount表示Worker线程的数量,runState表示线程池状态。 - private volatile ThreadFactory
threadFactory
;
线程工厂,用于生成Worker线程 - private volatile int
maximumPoolSize
最大线程数
- private volatile RejectedExecutionHandler
handler
拒绝任务的处理策略,具体见下文 - private volatile long
keepAliveTime
非核心线程空闲后,可以存活多久 - private volatile int
corePoolSize
核心线程数
- private int
largestPoolSize
曾经同时运行过线程的最大数量 - private final BlockingQueue<Runnable>
workQueue
;
等待处理的任务队列
- private final HashSet<Worker>
workers
;
所有的工作线程
- 线程池的执行过程(execute方法)
- 调用execute方法提交任务时,首先判断工作线程数(
workerCount
)是否已达到核心线程数(corePoolSize
),没达到,表明核心线程池未满,可以直接创建线程,执行任务,否则进行下一步。 - 判断等待队列(
BlockingQueue
)是否满,没满的话,加入等待队列,否则,进行下一步。 - 判断工作线程数(
workerCount
)是否已经达到线程池的最大容量(maximumPoolSize
),没达到,创建线程,已达到,进行下一步。 - 此时线程池满了,等待队列也满了,需要按照指定的饱和策略(
RejectedExecutionHandler
),处理任务。
- 饱和策略
- 在工作线程数量达到最大线程数量,并且等待队列也满的情况下,需要执行
饱和策略(拒绝执行处理)
,有4种策略
策略 | 解释 |
---|---|
AbortPolicy | 终止策略,抛出RejectedExecutionException异常 |
CallerRunsPolicy | 直接在调用execute方法的线程执行,如主线程,执行时,主线程会被阻塞 |
DiscardPolicy | 不处理,直接丢弃,不抛异常,不执行 |
DiscardOldestPolicy | 从队列中弹出头部元素,然后继续执行execute方法 |
- 也可以自定义策略,实现如
日志记录、持久化储存
等
static class MyRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//自己的策略,如日志记录、持久化储存等
}
}
- 其他方法
- submit
提交异步任务,Futrue对象获取返回值 - shutdown
关闭线程池,不再接受新任务,但队列中的任务会继续执行完 - shutdownNow
关闭线程池,不再接受新任务,队列中的任务不会执行,返回队列中等待的任务,正在执行的任务中断 - isShutdown
调用shutdown后, isShutdown为true - isTerminated
调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
3.ScheduledThreadPoolExecutor
- 主要用来在给定的延迟后运行任务,或者定期执行任务
比Timer好在哪?
- Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor不是;
- Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。ScheduledThreadPoolExecutor 可以配置任意数量的线程。
- 在TimerTask 中抛出的运行时异常会杀死一个线程,从而导致计划任务将不再运行。ScheduledThreadExecutor 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 afterExecute 方法ThreadPoolExecutor),抛出异常的任务将被取消,但其他任务将继续运行。
4.Executors
- Executors是一个工具类,可以方便的创建常见的线程池,如FixedThreadPool、SingleThreadExecutor、CachedThreadPool
线程池 | 解释 |
---|---|
FixedThreadPool | 固定大小的线程池 |
SingleThreadExecutor | 单线程的线程池 |
CachedThreadPool | 根据需要创建新线程的线程池 |
- FixedThreadPool和SingleThreadExecutor使用
无界队列
(队列容量为Integer.MAX_VALUE),不会拒绝任何任务,在任务比较多时可能会OOM
- CachedThreadPool允许创建的
线程数量为 Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。 - 因此
创建线程池不要使用Executors,应该直接使用ThreadPoolExecutor的构造方法
5. 确定线程池大小
- 线程池大小对系统的影响?
- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的!CPU 根本没有得到充分利用。
- 但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
- 怎样确定线程池大小?
-
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。