JUC 面试知识总结
整理了一下之前面试,学习,总结的知识如果觉得有用的话可以关注一下我的的语雀文档 https://www.yuque.com/g/hnsqls/rkzi78/ovpy9dgdnuw0yu04/collaborator/join?token=dVoOmnNfF0y47fLv&source=doc_collaborator# 以及 Github:https://github.com/hnsqls/interview
最近一个月的主要内容就是找工作面试,所以分享出来一是督促自己记录学习,二是希望产出点有用的东西,让大家共同学习进步。
1.并发与并行的区别
并发(concurrent)是同一时间内执行、
并行(parallel)是同一时刻执行
对于单核cpu来说
-
单核CPU下线程实际还是串行执行的
-
操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。
-
总结为一句话就是: 微观串行,宏观并行
一般会将这种线程轮流使用CPU的做法称为并发(concurrent)、
对于多核cpu来说
每个核(core)都可以调度运行线程,这时候线程可以是并行的。
2.创建线程的方式有那些?
总体来说有四种方法:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。
继承Thread类
▼java复制代码public class MyThread extends Thread { @Override public void run() { System.out.println("MyThread...run..."); } public static void main(String[] args) { // 创建MyThread对象 MyThread t1 = new MyThread() ; MyThread t2 = new MyThread() ; // 调用start方法启动线程 t1.start(); t2.start(); } }
实现Runnable接口 重写run方法,并将这个自定义类作为参数给Thread类
▼java复制代码public class MyRunnable implements Runnable{ @Override public void run() { System.out.println("MyRunnable...run..."); } public static void main(String[] args) { // 创建MyRunnable对象 MyRunnable mr = new MyRunnable() ; // 创建Thread对象 Thread t1 = new Thread(mr) ; Thread t2 = new Thread(mr) ; // 调用start方法启动线程 t1.start(); t2.start(); } }
实现Callable接口 重写call方法,并利用Fast接口的FutureTask实现类接收线程返回的结果 。
▼java复制代码public class MyCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println("MyCallable...call..."); return "OK"; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建MyCallable对象 MyCallable mc = new MyCallable() ; // 创建F FutureTask<String> ft = new FutureTask<String>(mc) ; // 创建Thread对象 Thread t1 = new Thread(ft) ; Thread t2 = new Thread(ft) ; // 调用start方法启动线程 t1.start(); // 调用ft的get方法获取执行结果 String result = ft.get(); // 输出 System.out.println(result); } }
通过线程池创建
▼java复制代码public class MyExecutors implements Runnable{ @Override public void run() { System.out.println("MyRunnable...run..."); } public static void main(String[] args) { // 创建线程池对象 ExecutorService threadPool = Executors.newFixedThreadPool(3); threadPool.submit(new MyExecutors()) ; // 关闭线程池 threadPool.shutdown(); } }
3.线程的 run()和 start()有什么区别?
start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
run(): 封装了要被线程执行的代码,可以被调用多次
4.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
代码举例:
为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成
▼java复制代码public class JoinTest { public static void main(String[] args) { // 创建线程对象 Thread t1 = new Thread(() -> { System.out.println("t1"); }) ; Thread t2 = new Thread(() -> { try { t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2"); }) ; Thread t3 = new Thread(() -> { try { t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t3"); }) ; // 启动线程 t1.start(); t2.start(); t3.start(); } }
5.在 java 中 wait 和 sleep 方法的不同?
共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
-
方法归属不同
-
sleep(long) 是 Thread 的静态方法
-
而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
-
-
醒来时机不同
-
执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
-
wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
-
它们都可以被打断唤醒
-
-
锁特性不同(重点)
-
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
-
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
-
而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
-
6.notify()和 notifyAll()有什么区别?
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个 wait 线程
▼java复制代码public class WaitNotify { static boolean flag = false; static Object lock = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock){ while (!flag){ System.out.println(Thread.currentThread().getName()+"...wating..."); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"...flag is true"); } }); Thread t2 = new Thread(() -> { synchronized (lock){ while (!flag){ System.out.println(Thread.currentThread().getName()+"...wating..."); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"...flag is true"); } }); Thread t3 = new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " hold lock"); lock.notifyAll(); flag = true; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); t3.start(); } }
7.volatile了解吗?
volatile是java中的关键字。作用在变量上,目的是保证变量在多线程的可见性和禁止指令重排序。
可见性
使用volatile表示的变量,会在更新后,立刻刷新到主内存上(JMM的知识),其他的线程在读取变量时可以立刻获取最新的值,这样可以避免线程间由于缓存导致一致性的问题导致“看到旧数据”的现象。
禁止指令重排序
-
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">volatile</font>还禁止了指令重排序优化,这确保了代码的执行顺序与编写顺序一致。 -
在多线程编程中,指令重排序有时会导致意想不到的结果,因为线程的执行顺序可能与预期不同。通过
<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">volatile</font>关键字,可以避免这种由于指令重排序引起的潜在问题。
8.AQS是什么
全称是 AbstractQueuedSynchronizer,即抽象队列同步器,它是构建锁或者其他同步组件的基础框架(工具)。比如ReentrantLock,Semaphore,CountDownLatch。
工作原理
锁的状态字段 <font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">private volatile int state</font> 0表示无锁,1表示有锁。volatile 保证了多线程下state可见性。
在多线程的获取锁的情况下,通过CAS操作更改锁的字段state 0 -> 1,哪个线程成功修改了锁的state字段,就获得了锁的资源。其他线程在请求获取该锁,判断锁对象的state = 1,就无法获取锁。就在FIFO双向的等待队列里,当获得锁的线程释放锁时,同时唤醒FIFO的队列头元素,如果此时又来了新线程,那么新线程会和刚唤醒的线程争夺,通过cas操作确保获得锁的原子性。.
核心原理
AQS维护了一个 volatile state字段表示是否获被线程获取,和一个FIFO的双向队列来维护等待队列。并且是非公平锁。
9.死锁产生的原因以及排查
死锁产生的原因是:两个线程或多个线程争夺临界资源的情况,而一直获取不到资源,这种情况是死锁。具体原因是一个线程获得了临界资源因为还需要其他资源目前无法执行完成,而一直占有临界资源,导致别的线程请求该临界资源不被允许,一直死锁。
▼java复制代码public class Mytest { static final Object A =new Object(); static final Object B =new Object(); public static void main(String[] args) { new Thread(()-> { synchronized (A) { System.out.println(Thread.currentThread().getName() + "获取A锁"); try { sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (B) { System.out.println(Thread.currentThread().getName() + "获取B锁"); } } },"t1").start(); new Thread(()-> { synchronized (B) { System.out.println(Thread.currentThread().getName() + "获取B锁"); try { sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (A) { System.out.println(Thread.currentThread().getName() + "获取A锁"); } } },"t2").start(); } }
那么怎么排查死锁的线程呢?
如果逻辑简单,就需要理获取锁的线程,是否可能会导致死锁。
还可以使用JDK提供的工具 jps 和jstack
jps: 列出jvm中进程的执行,列出进程的标识和进行id
jstack:jstack ,列出该进程的详细信息
10.阻塞队列都有那些?
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
比较常见的有4个,用的最多是ArrayBlockingQueue和LinkedBlockingQueue
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
ArrayBlockingQueue的LinkedBlockingQueue区别
ArrayBlockingQueue
要指定容量大小。
▼java复制代码ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(10);
LinkedBlockingQueue
可以不指定大小(不推荐),默认Integer.MAX_VALUE
▼java复制代码LinkedBlockingQueue<Runnable> linkedBlockingQueue = new LinkedBlockingQueue<>(10);
▼java复制代码/** * Creates a {@code LinkedBlockingQueue} with a capacity of * {@link Integer#MAX_VALUE}. */ public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
| LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|
| 默认无界,支持有界 | 强制有界 |
| 底层是链表 | 底层是数组 |
| 是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
| 入队会生成新 Node | Node需要是提前创建好的 |
| 两把锁(头尾) | 一把锁 |
左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方式
-
LinkedBlockingQueue读和写各有一把锁,性能相对较好
-
ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些。
11.如何停止一个正在运行的线程?
有三种方式可以停止线程
-
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
-
使用stop方法强行终止(不推荐,方法已作废)
-
使用interrupt方法中断线程
- 若打断阻塞的线程如wait,sleep,join,线程会抛出InterrupterException异常
12.synchronized关键字的底层原理?
synchronized的基本使用
▼java复制代码public class TicketDemo { static Object lock = new Object(); int ticketNum = 10; public synchronized void getTicket() { synchronized (this) { if (ticketNum <= 0) { return; } System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum); // 非原子性操作 ticketNum--; } } public static void main(String[] args) { TicketDemo ticketDemo = new TicketDemo(); for (int i = 0; i < 20; i++) { new Thread(() -> { ticketDemo.getTicket(); }).start(); } } }
synchronized关键字的底层原理是什么?
底层核心就是monitor。
monitor 是jvm中的对象。是实现synchronized的关键。
具体来说,一下例子
▼java复制代码public class Mytest { static final Object lock = new Object(); public static void main(String[] args) { synchronized(lock){ System.out.println("111"); } } }
通过jdk提供的反编译工具 javap -v SyncTest.class
可以看出synchronized关键字的原理就是monitor的作用,monitorenter是加锁,monitorexit是解锁。为什么有两个monitorexit是因为synchronized隐式的使用的finally{},防止异常发生而不能解锁。
那么monitor是什么呢?Monitor 被翻译为监视器,是由jvm提供,c++语言实现。
在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁
monitor主要就是跟这个对象产生关联,如下图
Monitor内部具体的存储结构:
-
Owner:存储当前获取锁的线程的,只能有一个线程可以获取
-
EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
-
WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
具体的流程:
-
线程进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有
-
如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
-
如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
-
如果代码块中调用了wait()方法,则会进去WaitSet中进行等待。
对象是怎么关联上的monitor?
这就要说一下对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。
最重要的就是MarkWord
-
hashcode:25位的对象标识Hash码
-
age:对象分代年龄占4位
-
biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
-
thread:持有偏向锁的线程ID,占23位
-
epoch:偏向时间戳,占2位
-
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
-
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
我们可以通过lock的标识,来判断是哪一种锁的等级
-
后三位是001表示无锁
-
后三位是101表示偏向锁
-
后两位是00表示轻量级锁
-
后两位是10表示重量级锁
如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
-
Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
-
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
▼java复制代码static final Object obj = new Object(); public static void method1() { synchronized (obj) { // 同步块 A method2(); } } public static void method2() { synchronized (obj) { // 同步块 B } }
加锁的流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要升级为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁
就一个线程使用,只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
加锁的流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些
13.JMM是什么?
JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写的行为规范,通过这些规范对内存的读写操作保证指令的正确性。
-
在JMM中,内存被划分为两个主要区域:主内存和工作内存。
-
主内存:是共享内存区域,所有线程都可以访问,用于存储共享变量。在Java中,堆和方法区是主内存的一部分。
-
工作内存:是线程私有的内存区域,每个线程都有一个独立的工作内存,用于存储线程的私有变量以及从主内存中复制的共享变量副本。在Java中,程序计数器、虚拟机栈和本地方法栈是工作内存的一部分。
-
-
特征
-
可见性:指一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。在JMM中,通过read、load、store和write四种原子操作来实现主内存和工作内存之间的数据交互,从而保证可见性。
-
有序性:指程序中的指令按照特定的顺序执行,前一个指令执行完毕,后一个指令才能执行。JMM通过一系列规则(如happens-before规则)来确保指令的有序性。
-
原子性:指一个操作是不可分割的,在执行期间不能被中断。JMM通过lock和unlock两种原子操作来确保原子性。当一个线程对共享变量进行加锁操作时,其他线程无法访问该变量,直到锁被释放。
14.CAS 是什么?
CAS的全称是: Compare And Swap(比较再交换),将比较和交换封装成一个指令确保原子性。它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
工作原理:
-
比较(Compare): CAS会检查内存中的某个值是否与预期值相等。
-
交换(Swap):如果相等,将将内存中的值更新为新值。
-
失败重试:如果不想等,说明由其他线程已经修改了该值,CAS操作失败,一般会用重试(锁的自旋),直到成功。
举个例子:
在 JMM中,操作数据的过程
同时两个线程:
线程1:从主内存中取出数据a = 100,到线程1的工作内存,进行a++操作
线程2:从主内存中取出数据a = 100,到线程2的工作内存,进行a--操作
过程:
线程1
-
线程1拿A的值与主内存V的值进行比较,判断是否相等
-
如果相等,则把B的值101更新到主内存中
-
由于比较和交换是原子操作,确保了比较时候的数据是一样的但是交换时候数据发生改变(同时cas具体的体现,库存超卖,使用乐观锁 sql解决)。
线程2:从主内存中取出数据a = 100,到线程2的工作内存,进行a--操作
-
线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值99)
-
不相等,则线程2更新失败
-
自旋锁操作
-
因为没有加锁,所以线程不会陷入阻塞,效率较高
-
如果竞争激烈,重试频繁发生,效率会受影响
-
▼java复制代码//不断尝试 while(true){ int 工作内存中的A = 共享变量A; int 结果A = 工作内存中的A - 1; if(compareAndSwap(工作内存中的A,共享变量的A){ //相等就 赋值新结果 跳出循环。 //不相等就再次尝试 }) }
CAS的底层实现
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。
都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现
在java中比较常见使用有很多,比如ReentrantLock和Atomic开头的线程安全类,都调用了Unsafe中的方法
ReentrantLock中的一段CAS代码
CAS的优缺点
优点
-
无锁并发:没有使用锁,不会影响性能。
-
原子性:保证了线程安全。
缺点
-
ABA问题:CAS操作中,一个变量的值从A变成B,又变回A,CAS无法检测到这种变化。可能导致错误
-
自旋开销:CAS操作通过自旋实现,可能导致CPU资源浪费
-
单变量限制:CAS操作仅适用于单个变量的更新
ABA问题的解决
引入版本号或者时间戳,本次变化都改变版本号或时间戳,用版本号和时间戳来表示是否变化
14.ReentrantLock实现的原理
ReentrantLock翻译过来是可重入锁。
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
查看ReentrantLock源码中的构造方法:
其中sync 是Sync类 继承了AQS接口。 NofairSync和FairSync 类继承了Sync类。
工作流程
-
线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功。
-
假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部。
-
当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程。
-
公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁。
15.线程池的种类有哪些?
线程的创建和关闭会消耗大量的资源,同时业务如果需要很多线程,我们创建很多线程,但是单核cpu一次只能执行一个线程,创建大量的线程会消耗资源,而且还得不到cpu的控制权,所以业务中都是选择线程池。
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种
newFixedThreadPool 创建使用固定线程数的线程池
使用
▼java复制代码public class Mytest { static class FixedThreadDemo implements Runnable{ @Override public void run() { String name = Thread.currentThread().getName(); for (int i = 0; i < 2; i++) { System.out.println(name + ":" + i); } } } public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { executorService.execute(new FixedThreadDemo()); sleep(10); } executorService.shutdown(); System.out.println("Author:hnsqls"); } }
源码
▼java复制代码public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
解释: 只有核心线程数,没有临时线程,阻塞队列是LinkedBlockingQueue,并且没有指定阻塞队列空间,那么默认是Integer.Value_MAX。 拒绝策略是默认策略,直接抛出异常。
newSingleThreadExecutor 单线程化的线程池
▼plain复制代码ExecutorService executorService = Executors.newSingleThreadExecutor();
源码
▼java复制代码/** * Creates an Executor that uses a single worker thread operating * off an unbounded queue. (Note however that if this single * thread terminates due to a failure during execution prior to * shutdown, a new one will take its place if needed to execute * subsequent tasks.) Tasks are guaranteed to execute * sequentially, and no more than one task will be active at any * given time. Unlike the otherwise equivalent * {@code newFixedThreadPool(1)} the returned executor is * guaranteed not to be reconfigurable to use additional threads. * * @return the newly created single-threaded Executor */ public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
解释: 核心线程数1,最大线程为1,阻塞队列是LinkedBlockingQueue且没指定空间。拒绝策略为默认策略。
newCachedThreadPool 可缓存线程池
源码
▼java复制代码/** * Creates a thread pool that creates new threads as needed, but * will reuse previously constructed threads when they are * available. These pools will typically improve the performance * of programs that execute many short-lived asynchronous tasks. * Calls to {@code execute} will reuse previously constructed * threads if available. If no existing thread is available, a new * thread will be created and added to the pool. Threads that have * not been used for sixty seconds are terminated and removed from * the cache. Thus, a pool that remains idle for long enough will * not consume any resources. Note that pools with similar * properties but different details (for example, timeout parameters) * may be created using {@link ThreadPoolExecutor} constructors. * * @return the newly created thread pool */ public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
解释
核心线程为0,最大线程数为MAX_Value.
阻塞队列是SychronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
ScheduledThreadPoolExecutor 提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。
源码
▼java复制代码/** * Creates a new {@code ScheduledThreadPoolExecutor} with the * given core pool size. * * @param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {@code allowCoreThreadTimeOut} is set * @throws IllegalArgumentException if {@code corePoolSize < 0} */ public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }
解释
核心线程数自定义,最大线程数max_value
阻塞队列 DelayedWorkQueue。
扩展 不建议使用Executors创建线程池
阿里开发手册
16.线程池的核心参数以及执行原理
参考ThreadPoolExecutor
-
corePoolSize 核心线程数目
-
maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
-
keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
-
unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
-
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
-
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
-
handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
工作流程
1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行
2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列
3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务
如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务
4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略
17.进程与线程的区别
进程 是操作系统中 资源分配的基本单位,代表一个正在运行的程序。每个进程都有 独立的内存空间 和 系统资源,不同进程之间相互独立。
-
独立性:进程拥有独立的地址空间,一个进程的崩溃不会影响其他进程。
-
资源分配:每个进程都有自己独立的 内存、文件句柄、全局变量 等资源。
-
进程间通信(IPC):由于进程相互独立,它们需要通过 进程间通信(如管道、消息队列、共享内存、Socket等)进行数据交换。
-
切换开销大:进程切换涉及到资源回收和重新加载,开销较大。
线程 是 CPU 调度的基本单位,是 进程中的执行流。一个进程可以包含多个线程,它们共享进程的资源(如内存、文件句柄),但有自己独立的栈空间和程序计数器(PC)。
进程中的执行流 解释:进程是程序的实体,程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
