懂线程者,更胜于面试,更优于性能,与君共勉;
线程
进程和线程:一个进程包括多个线程。把视频软件理解为一个进程,而把视频+音频+弹幕可以理解为是视频软件中互相协作的三个线程;
并行与并发:并行指的是多个线程同时执行各自的任务;并发则是指,单个线程抢占时间片,高速来回切换执行不同的任务。表面上多个线程持续运行,实际上单个线程走走停停。
线程的状态
1.1 new() ,创建线程,该线程进入新建状态;
1.2 start(), 创建线程后,调用线程的start()方法,线程会被加入到一个可运行的线程池中,线程进入**就绪(可运行)**状态;
1.3 可运行线程池中的线程被分配到时间片,从可运行状态变为运行状态;
1.3.1 此状态下,线程如果调用yield()方法,则会让出时间片,重新回到**就绪**状态;
1.4 线程运行期间,如果调用wait(),sleep(),join()方法,线程变成了等待状态需要等待唤醒或者其他线程的调度;
1.5 针对有些情况,不希望线程一直等待靠别的线程唤醒,可以设置超时时间,此时线程则是超时等待状态;
1.6 多线程间访问同步块或者共享资源,没有拿到对应的资源对象锁,则进入阻塞状态,直到拥有锁的线程释放出锁;
1.7 线程执行结束或者抛出异常,进入终止状态;
线程阻塞与线程等待的区别:
线程等待是线程主动发起的,比如等待子线程执行完某些任务;线程阻塞是被动发生的;
线程等待一般是已经拿到了锁,进入了同步代码块内;线程阻塞一般是再拿到锁进入绒布代码块之前,因为
没有拿到锁所以一直阻塞等待;
线程的创建方式
继承Thread类
public class TestThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("三秒过去了~");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TestThread thread = new TestThread();
thread.start();
}
}
实现Runnable接口
public class TestRunnable implements Runnable{
@Override
public void run() {
System.out.println("uzi yyds");
}
public static void main(String[] args) {
TestRunnable runnable = new TestRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
实现Callable接口配合FutureTask
public class TestCallable implements Callable<List<Integer>> {
List<Integer> list = new ArrayList<>();
@Override
public List<Integer> call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"正在加入:"+i);
list.add(i);
}
return list;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable callable = new TestCallable();
FutureTask<List<Integer>> task = new FutureTask(callable);
Thread thread = new Thread(task);
thread.start();
System.out.println(task.get());// 此处的get()方法,会使得主线程阻塞等待子线程的任务结果
}
}
// 输出
Thread-0正在加入:0
Thread-0正在加入:1
Thread-0正在加入:2
Thread-0正在加入:3
Thread-0正在加入:4
Thread-0正在加入:5
Thread-0正在加入:6
Thread-0正在加入:7
Thread-0正在加入:8
Thread-0正在加入:9
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
几种创建方式对比:
首先,实现Runnable接口相对于继承Thread类更灵活,毕竟java有单继承多实现的限制,如果继承了Thread类,就无法再去继承其他类;
其次,实现Callable接口配合FutureTask使用,能够接口线程的返回值,对于某些需要拿到线程执行结果的情况更适合;
再来看看FutureTask
已知实现Callable接口方式创建的线程有返回值,能更灵活的应对业务场景。那么相应的,我们也看看FutureTask这个类里面有没有什么可以为我们所用的东西;
创建一个类,实现Callable接口
public class TestCallable2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(3000);
System.out.println("子线程执行任务");
return 1;
}
}
Q: 已知get()方法会使得主线程阻塞等待子线程,那么如果我不希望一直等下去怎么办?
A: get(long, TimeUnit) 超时等待
get()方法可以使得主线程等待子线程的任务执行结束,但是更多的场景是不希望一直等待,get(long,TimeUnit)方法就是设置一个等待时间,如果超时后,则抛出异常,我们可以根据自己的业务需求,做其他处理;
值得注意的是:如果多个线程同时提交了多个任务,get()方法如果不设置超时时间,那么主线程的阻塞时间是以多个任务中耗时最高的时长为准!!!借用这个机制,可以在我们业务逻辑编写时做一些性能优化。详情参见java性能优化
Q: get(long, TimeUnit) 方法下,主线程不会一直等待,但是子线程的任务仍旧会执行结束,那么在超时后,我们不想继续执行这个任务,以节省资源了,怎么办?
A: cancel(boolean)方法
当我们等待超时后,捕捉到异常,可以用cancel()方法取消子任务,最后调用isCancelled()方法发现确实被取消了;
Q: 如果我不想在获取的时候根据有没有异常来判断有没有执行结束,就像稳一手,先判断有没有执行结束,然后在获取结果怎么办?
A: IsDoned()方法
如上图所示,我们主线程休眠1秒后,我们通过isDone()方法手动判断子任务是否执行结束,如果执行结束了就打印,没有执行结束就取消任务;
此方式与get(long, TimeUnit)方法都是避免主线程一直等待,区别在于,此方式可以优雅的判断任务是否执行结束,没结束可以做取消任务处理;get(long, TimeUnit)不论任务是否执行结束,都会执行获取方法,如果没有执行结束,则抛出异常
线程池
线程池的7个核心参数
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程的存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 线程工作队列
ThreadFactory threadFactory, // 创建线程的工厂
RejectedExecutionHandler handler // 拒绝策略) {...}
参数及工作流程详解:
-
corePoolSize
核心线程数,当线程池初始化后,如果当前有任务提交,并且目前线程池中的线程数(包括空闲线程)小于核心线程数,则会新建一条核心线程执行任务;任务执行完成后,就算当前线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut=true;
-
maximumPoolSize
最大线程数,线程池的最大容量,线程池中的线程数量不得超过这么多个;
-
keepAliveTime
空闲线程的存活时间,如果当前线程池中的线程数超过corePoolSize,那么超出的线程在存活时间之后就会被自动销毁;
-
unit
空闲时间的单位;
-
workQueue
队列,线程池中的线程数超过corePoolSize后,新的任务会进入到这个工作队列中。目前有四种队列类型:
5.1 ArrayBlockingQueue
基于数组的有界队列,遵循FIFO原则,当线程池中的线程数超过了corePoolSize,此时有新任务进来,会添加到队列的尾部等待 被调度,如果此时队列的容量已经满了,则会创建一个新的线程,如果线程数超过了maximumPoolSize,则会执行拒绝策略。 此方式可以控制线程数以及任务书,进而有效的控制资源利用;
5.2 LinkedBlockingQuene
基于链表的无边界队列,遵循FIFO原则,当线程池中的线程数超过了corePoolSize,再有新任务加入时,也会先加入到这个队列 中。但是,由于队列时近似无边界的(实际容量为Interger.MAX),所以任务几乎都会一直存在这个队列中,不会再去创建新的 线程,所以maximumPoolSize参数不会起作用;
5.3 SynchronousQuene
同步队列(不缓存任务的队列),为什么叫不缓存,因为当线程池中线程数超过了corePoolSize,并且有新任务加进来时,不会 堆积在队列里等待线程调度,而是直接新建线程。或者说,当创建了一个任务进入队列时,对应的会立即新建一个线程来执行这 个任务。如果线程数大于maximumPoolSize,则执行拒绝策略;
5.4 PriorityBlockingQueue
无界的基于数组的优先级阻塞队列,数组的默认长度是11,虽然指定了数组的长度,但是可以无限的扩充,直到资源消耗尽为止,每次出队都返回优先级别最高的或者最低的元素;
-
handler
拒绝策略:当线程池中的队列任务书已满,并且线程池的数量达到了maximumPoolSize,则会触发拒绝策略;
6.1 CallerRunsPolicy
该策略下,会将run()方法的执行主体转移给调用方,换句话说,那个线程递交任务给线程池,该拒绝策略下就会把这个任务回退 给对应线程执行(除非线程池shutdown);
6.2 AbortPolicy(默认拒绝策略)
该策略下,直接丢弃任务,抛出RejectedExecutionException异常;
6.3 DiscardPolicy
该策略下,直接丢弃任务,不做任何处理;
6.4 DiscardOldestPolicy
该策略下,会抛弃进入队列最早的那一个任务,并尝试将该被拒绝的任务加入队列中;
线程池工作流程:
假设当前设置核心线程数为X, 最大线程数为Y,,队列容量为Z,任务数量为M,线程池初始化后:
- 当有新任务加入时,首先会先创建新线程执行任务,直到线程数达到了核心线程数X------ M<=X;
- 当线程池中的线程数达到核心线程数,此时再有任务进来,会优先加入到队列中------------X<M<=Z;
- 当队列中存在的任务数量达到了容量Z,此时如果有新任务提交,则会创建普通线程--------Z<M<= Y;
- 当队列中的线程数已经达到了最大线程数,此时有新任务提交,触发拒绝策略----------------M>Y;
线程池的四种创建方式
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
此种方式下创建的线程,核心线程数为0,普通线程数近乎无限大;这类线程处理任务时,如果当前线程池的大小超出了任务处理需求,则会优先使用当前已存在的线程,不会额外新建线程;换句话说,线程池初始化后,如果有任务提交,会新建一个线程处理任务。后续又有其他任务提交时,会优先用原先已存在的线程执行任务,而不是优先新建线程;
上述代码中,我们循环向线程池中提交任务,但是每次提交前都确保上一个任务已经执行结束,在这个前提下,线程池不会新建线程,而是一直用已经存在的那个线程,所以多个任务打印的都是同一个线程名称;
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
此种方式下会根据参数创建一个固定大小的线程池,超过核心线程数的任务会全部放到队列中。而这边用到的是LinkedBlockingQueue,所以最大线程数实际不起作用,因为队列是无界的,只要线程数超过了核心线程数,此时新加任务都是放到队列中,而队列又是无界的,所以不会再与核心线程数做比较;
上述代码中,我们创建线程池时指定核心线程数量为2,有多个任务提交时,超出核心线程数的任务会存入到队列中,并最终由两个核心线程依次处理;
newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
此种方式下会创建一个固定核心线程数的,可以定时执行任务的线程池;
提交任务的几种方式:
- schedule()方法提交任务
schedule(Runnable command,long delay,TimeUnit unit){...}
该方法下,会在指定的延迟时间后执行任务;
-
scheduleAtFixedRate()方法提交任务
scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit){...} 该方法下,会在给定的延时后,每个一段时间执行一次任务;
上述代码可以看到,首次执行任务时,延迟了3s,后续每次执行都间隔1s;
-
scheduleWithFixedDelay()方法提交任务
scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) 该方法下,也会在一段延迟后,按照固定的间隔循环执行任务;
注意此处的时间,这也是与scheduleAtFixedRate区别点:假设任务执行耗时需要4s,设置间隔时间2s
scheduleAtFixedRate:首次执行任务后,后续任务的执行时间是4s后,换句话说,下次执行任务的实际间隔取任务耗时与间隔时间的较大值;
scheduleWithFixedDelay:首次执行任务后,后续人物的执行时间是6s,换句话说,执行下一个任务的前提是:上一个任务完全执行结束,并且再等待固定的间隔,下次执行任务的实际间隔时间,为任务耗时+设置的间隔;
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
此种方式会创建一个唯一线程的线程池,多个任务提交到队列后,能保证多个任务的顺序执行;
上述代码可见,一个线程执行多个任务,并且任务的返回值也是按照任务的提交顺序返回的;
execute()与submit()方法
// Executor是提供了一个无返回值execute方法
public interface Executor {
void execute(Runnable command);
}
// ExecutorService 继承了Executor,并且自定义了有返回值的sumbit方法
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
}
execute()与submit()方法
// Executor是提供了一个无返回值execute方法
public interface Executor {
void execute(Runnable command);
}
// ExecutorService 继承了Executor,并且自定义了有返回值的sumbit方法
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
}
submit()方法
// submit方法
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
// newTaskFor方法
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
由源码可见,submit()先根据传入的Callable接口的实现类创建了一个FutureTask对象,最终还是调用了execute()方法,并且返回了FututeTask对象,我们也是以此来拿到线程的返回值结果;