《面试补习》- 多线程知识梳理
一、基本概念
1.1、进程
进程是系统资源分配的最小单位。由 文本区域
,数据区域
和堆栈
组成。
- 文本区域存储处理器执行的代码
- 数据区域存储变量和进程执行期间使用的动态分配的内存;
- 堆栈区域存储着活动过程调用的指令和本地变量。
涉及问题: cpu抢占
,内存分配(虚拟内存/物理内存)
,以及进程间通信
。
1.2、线程
线程是操作系统能够进行运算调度的最小单位。
一个进程可以包括多个线程,线程共用
进程所分配到的资源空间
涉及问题: 线程状态
,并发问题
,锁
1.3、协程
子例程
: 某个主程序的一部分代码,也就是指某个方法,函数。
维基百科:执行过程类似于 子例程
,有自己的上下文,但是其切换由自己控制。
1.4、常见问题
- 1、进程和线程的区别
进程拥有自己的资源空间,而线程需要依赖于进程进行资源的分配,才能执行相应的任务。
进程间通信需要依赖于 管道,共享内存,信号(量)和消息队列等方式。
线程不安全,容易导致进程崩溃等
- 2、什么是多线程
线程是运算调度的最小单位,即每个处理器在某个时间点上只能处理一个线程任务调度。
在多核cpu 上,为了提高我们cpu的使用率,从而引出了多线程的实现。
通过多个线程任务并发调度,实现任务的并发执行。也就是我们所说的多线程任务执行。
二、Thread
2.1、使用多线程
2.1.1、继承 Thread 类
class JayThread extends Thread{
@Override
public void run(){
System.out.println("hello world in JayThread!");
}
}
class Main{
public static void main(String[] args){
JayThread t1 = new JayThread();
t1.start();
}
}
2.1.2、实现 Runnable 接口
class JayRunnable implements Runnable{
@Override
public void run(){
System.out.println("hello world in JayRunnable!")
}
}
class Main{
public static void main(String[] args){
JayRunnable runnable = new JayRunnable();
Thread t1 = new Thread(runnable);
t1.start();
}
}
2.1.3、实现 Callable 接口
class JayCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("run in JayCallable " + Thread.currentThread().getName());
return "Jayce";
}
}
class Main{
public static void main(String[] args) {
Thread.currentThread().setName("main thread");
ThreadPoolExecutor executor =new ThreadPoolExecutor(10,20,60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
Future<String> future = executor.submit(new JayCallable());
try {
future.get(10, TimeUnit.SECONDS);
}catch (Exception e){
System.out.println("任务执行超时");
}
}
}
2.1.4、常见问题
- 1、使用多线程有哪些方式
常用的方式主要由上述3种,需要注意的是 使用
,而不是创建线程,从实现的代码我们可以看到,Java 创建线程只有一种方式, 就是通过 new Thread()
的方式进行创建线程。
- 2、
Thread()
,Runnable()
与Callable()
之间的区别
Thread
需要继承,重写run()
方法,对拓展不友好,一个类即一个线程任务。
Runnbale
通过接口的方式,可以实现多个接口,继承父类。需要创建一个线程进行装载任务执行。
Callable
JDK1.5 后引入, 解决 Runnable 不能返回结果或抛出异常的问题。需要结合 ThreadPoolExecutor
使用。
- 3、
Thread.run()
和Thread.start()
的区别
Thread.run()
public static void main(String[] args){
Thread.currentThread().setName("main thread");
Thread t1 = new Thread(()->{
System.out.println("run in "+Thread.currentThread().getName());
});
t1.setName("Jayce Thread");
t1.run();
}
输出结果:
Thread.start()
public static void main(String[] args){
Thread.currentThread().setName("main thread");
Thread t1 = new Thread(()->{
System.out.println("run in "+Thread.currentThread().getName());
});
t1.setName("Jayce Thread");
t1.start();
}
输出结果:
start() 方法来启动线程,使当前任务进入 cpu 等待队列(进入就绪状态,等待cpu分片),获取分片后执行run方法。
run() 方法执行,会被解析成一个普通方法的调用,直接在当前线程执行。
2.2、线程状态
线程状态,也称为线程的生命周期, 主要可以分为: 新建
,就绪
,运行
,死亡
,堵塞
等五个阶段。
图片引用 芋道源码
2.2.1 新建
新建状态比较好理解, 就是我们调用 new Thread()
的时候所创建的线程类。
2.2.2 就绪
就绪状态指得是:
1、当调用 Thread.start
时,线程可以开始执行, 但是需要等待获取 cpu 资源。区别于 Thread.run
方法,run
方法是直接在当前线程进行执行,沿用其 cpu 资源。
2、运行状态下,cpu 资源
使用完后,重新进入就绪状态,重新等待获取 cpu 资源
. 从图中可以看到,可以直接调用Thread.yield
放弃当前的 cpu资源,进入就绪状态。让其他优先级更高的任务优先执行。
2.2.3 运行
在步骤2
就绪状态中,获取到 cpu资源
后,进入到运行状态, 执行对应的任务,也就是我们实现的 run()
方法。
2.2.4 结束
1、正常任务执行完成,run() 方法执行完毕
2、异常退出,程序抛出异常,没有捕获
2.2.5 阻塞
阻塞主要分为: io等待,锁等待,线程等待 这几种方式。通过上述图片可以直观的看到。
io等待: 等待用户输入,让出cpu资源,等用户操作完成后(io就绪),重新进入就绪状态。
锁等待:同步代码块需要等待获取锁,才能进入就绪状态
线程等待: sleep()
, join()
和 wait()/notify()
方法都是等待线程状态的阻塞(可以理解成当前线程的状态受别的线程影响)
二、线程池
2.1 池化技术
池化技术,主要是为了减少每次资源的创建,销毁所带来的损耗,通过资源的重复利用提高资源利用率而实现的一种技术方案。常见的例如: 数据库连接池,http连接池以及线程池等。都是通过池同一管理,重复利用,从而提高资源的利用率。
使用线程池的好处:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2.2 线程池创建
2.2.1 Executors (不建议)
Executors 可以比较快捷的帮我们创建类似 FixedThreadPool ,CachedThreadPool 等类型的线程池。
// 创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor();
// 创建固定数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads);
// 创建带缓存的线程池
public static ExecutorService newCachedThreadPool();
// 创建定时调度的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 创建流式(fork-join)线程池
public static ExecutorService newWorkStealingPool();
存在的弊端:
FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
2.2.2 ThreadPoolExecuotr
构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
几个核心的参数:
- 1、
corePoolSize
: 核心线程数 - 2、
maximumPoolSize
: 最大线程数 - 3、
keepAliveTime
: 线程空闲存活时间 - 4、
unit
: 时间单位 - 5、
workQueue
: 等待队列 - 6、
threadFactory
: 线程工厂 - 7、
handler
: 拒绝策略
与上述的 ExecutorService.newSingleThreadExecutor
等多个api
进行对比,可以比较容易的区分出底层的实现是依赖于 BlockingQueue
的不同而定义的线程池。
主要由以下几种的阻塞队列:
- 1、
ArrayBlockingQueue
,队列是有界的,基于数组实现的阻塞队列 - 2、
LinkedBlockingQueue
,队列可以有界,也可以无界。基于链表实现的阻塞队列 对应了:Executors.newFixedThreadPool()
的实现。 - 3、
SynchronousQueue
,不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将一直处于阻塞状态。对应了:Executors.newCachedThreadPool()
的实现。 - 4、
PriorityBlockingQueue
,带优先级的无界阻塞队列
拒绝策略主要有以下4种:
- 1、
CallerRunsPolicy
: 在调用者线程执行 - 2、
AbortPolicy
: 直接抛出RejectedExecutionException异常 - 3、
DiscardPolicy
: 任务直接丢弃,不做任何处理 - 4、
DiscardOldestPolicy
: 丢弃队列里最旧的那个任务,再尝试执行当前任务
2.3 线程池提交任务
往线程池中提交任务,主要有两种方法,execute()
和submit()
1、 execute()
无返回结果,直接执行任务
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> System.out.println("hello"));
}
2、submit()
submit()
会返回一个 Future
对象,用于获取返回结果,常用的api 有 get()
和 get(timeout,unit)
两种方式,常用于做限时处理
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
System.out.println("hello world! ");
return "hello world!";
});
System.out.println("get result: " + future.get());
}
三、线程工具类
3.1 ThreadlLocal
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
3.2 Semaphore
Semaphore ,是一种新的同步类,它是一个计数信号. 使用示例代码:
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 0; index < 50; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println("Accessing: " + NO);
Thread.sleep((long) (Math.random() * 6000));
// 访问完后,释放
semp.release();
//availablePermits()指的是当前信号灯库中有多少个可以被使用
System.out.println("-----------------" + semp.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
3.3 CountDownLatch
可以理解成是一个栅栏,需要等所有的线程都执行完成后,才能继续往下走。
CountDownLatch
默认的构造方法是 CountDownLatch(int count)
,其参数表示需要减少的计数,主线程调用 #await()
方法告诉 CountDownLatch
阻塞等待指定数量的计数被减少,然后其它线程调用 CountDownLatch
的 #countDown()
方法,减小计数(不会阻塞)。等待计数被减少到零,主线程结束阻塞等待,继续往下执行。
3.4 CyclicBarrier
CyclicBarrier
与 CountDownLatch
有点相似, 都是让线程都到达某个点,才能继续往下走, 有所不同的是 CyclicBarrier
是可以多次使用的。 示例代码:
CyclicBarrier barrier;
public TaskThread(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(getName() + " 到达栅栏 A");
barrier.await();
System.out.println(getName() + " 冲破栅栏 A");
Thread.sleep(2000);
System.out.println(getName() + " 到达栅栏 B");
barrier.await();
System.out.println(getName() + " 冲破栅栏 B");
} catch (Exception e) {
e.printStackTrace();
}
}
四、总结
最后贴一个新生的公众号 (Java 补习课
),欢迎各位关注,主要会分享一下面试的内容(参考之前博主的文章),阿里的开源技术之类和阿里生活相关。 想要交流面试经验的,可以添加我的个人微信(Jayce-K
)进群学习~