JUC和线程池的详细讲解

理解: 正式工已经招满, 此时所有的正式工都在忙着工作, 且队列中的新任务已达到队列上线, 需要招聘临时工, 来完成任务. 正式工和临时工的总数为公司的最大员工数, 如果所有的临时工也都有任务, 再来新任务公司予以拒绝

4. keepAliveTime 线程最大空闲时间

线程池中没有被使用的线程, 就会处于空闲(alive)状态, 只要超过keepAliveTime, 空闲的线程就会被销毁,直到线程池中的线程数等于corePoolSize

如果设置了allowCoreThreadTimeOut=true(默认false),核心线程也可以被销毁

理解: 到了淡季, 公司不忙了, 很多的正式工和临时工都空闲了, 临时工就直接解雇了, 而正式工没有重大违纪的不会解雇

5. unit keepAliveTime 时间单位

TimeUnit是枚举类型

包含如下时间单位

在这里插入图片描述

注:

1秒 = 1000毫秒 1毫秒 = 1000微秒 1微妙 = 1000纳秒

6. threadFactory 线程工厂

主要用来创建线程

7. handler 线程池拒绝策略(面试题:线程池拒绝策略有哪些)

只有核心线程都在使用中,任务队列已满,且线程数量已经达到maximunPoolSize才会触发拒绝策略。或在调用shutdown()和真正关闭线程池之间提交的任务都会被决绝。因为线程池被shutdown()时,会等待线程池内线程对象执行结束,才关闭线程池

在这里插入图片描述

DiscardoldestPolicy: 从队列中去掉一个最先进入队列的任务。然后重新提交被拒绝的任务(重复此过程)

AbortPolicy:丢弃任务,抛出运行时异常(RejectedExecutionException)(默认值)

CallerRunsPolicy:由调用该任务的线程处理, 线程池不参与

DiscardPolicy:丢弃任务,不抛出异常

| | |

| — | — |

| | |

8. 线程池总结 (常见面试题)

================================================================================

corePoolSize: 核心线程数大小

maximunPoolSize:最大线程数大小(包含核心线程数,剩下的就是普通线程数)

queueCapacity:任务队列最大长度

keepAliveSize:线程最大空闲时间

allowCoreThreadTimeOut:核心线程超时是否被销毁(默认 false)

handler:拒绝策略

workQueue:任务队列类型

1.当线程数小于核心线程数时,创建线程, 直到到达核心指定的核心线程数

2.当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列

3.当线程数大于等于核心线程数,且任务队列已满

若线程数小于最大线程数,创建线程, 直到达到最大线程数

若线程数等于最大线程数,抛出异常,拒绝任务

在这里插入图片描述

Executors

========================================================================

Executors可以帮助快速实例化特定类型的线程池对象, Executors属于一个工具类

返回值都是ExecutorService接口的实现类, 底层都是调用ThreadPoolExecutor()在这里插入图片描述

newCachedThreadPool()


不需要要参数,基本上都是默认值

构造方法参数:

corePoolSize: 0

maximumPoolSize: Integer的最大值

keepAliveTime: 60秒

workQueue:SynchronousQueue

同步队列, 一个线程向队列put(放入)数据后,阻塞等待被take(取出)

效果总结:线程需要时就新建,空闲60秒后被销毁。默认都是放在同步队列中。

在这里插入图片描述

newFixedThreadPool(int)


构造方法参数:

corePoolSize: 参数值

maximunPoolSize: 参数值

keepAliveTime: 0秒

workQueue: LinkedBlockingQueue

生产者端和消费者端分别采用了独立的锁来控制数据同步, 这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能

效果总结:

最大线程数和核心线程数一致。 即:从0开始新建,当达到固定值后不新建,也不销毁线程对象(销毁数量:最大线程数 - 核心线程数)

在这里插入图片描述

newScheduledThreadPool(int)


构造方法参数:

corePoolSize: 参数值

maximunPoolSize: int的最大值

keepAliveTime: 0秒

workQueue: DelayedWorkQueue

DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中放入数据的操作永远不会被阻塞,而只有获取数据的操作才会被阻塞

效果总结:

指定核心线程数,超出核心线程数的任务放入到延迟队列中,线程空闲立即被回收

newWorkStealingPool()


该方法是Java 8 新增的一个方法。底层使用的是ForkJoinPool类

在这里插入图片描述

ForkJoinPool介绍


ForkJoinPool是Java7新增的线程池类型。是ThreadPoolExecutor的兄弟类

工作原理(工作窃取算法):把一个Thread 分叉(fork)成多个子线程。让多个子线程执行本来一个线程应该执行的任务。最后把多个线程执行结果合并

在这里插入图片描述

如果在分叉后一个线程执行完成,另外的线程还没有结束,会重双端队列中尾部处理任务,另一个线程从头部取任务,防止出现线程竞争

在这里插入图片描述

所有任务都是异步的。不会阻塞主线程,主线程不用等到线程池任务结束在关闭

线程池代码演示


创建了固定长度为10的线程池

每个线程任务是输出随机数后休眠500毫秒

每次10个核心线程执行任务

public class Threadpool {

public static void main(String[] args) {

//创建线程池, 核心线程数和最大线程数都为10, 一次最多有10个线程工作

ExecutorService executorService = Executors.newFixedThreadPool(10);

//分配100个任务

for (int i = 0; i < 50; i++) {

//将任务交给线程池, 每次10个核心线程并行执行

executorService.submit(new Runnable() {

@Override

public void run() {

try {

int i1 = new Random().nextInt(11);

System.out.println(Thread.currentThread().getName()+": "+i1);

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

});

}

//关闭线程池, 即使执行了关闭线程池, 也会等待线程池中的任务执行完才会关闭

executorService.shutdown();

}

}

JUC中的AtomicInteger原子类

====================================================================================

原子性介绍


原子性:数据操作是整体,整体只有成功或失败,不允许出现部分成功部分失败

例如:

num++在多线程下执行是不安全的, num++可以分解为

  1. 读取num的值(从主内存读取)

  2. 对num+1(副本内存操作)

  3. 把结果赋值给num(更新主内存)

所以说num++是不具备原子性的

如果希望num++具备原子性,可以把num++的三个步骤看做一个不可分的整体。只要具备了原子性,就一定是线程安全的

JUC中原子类


在这里插入图片描述

使用AtomicInteger保证线程安全


原始方式: 使用synchronized同步锁解决

public class Demo07 {

static int a = 0;

public static void main(String[] args) {

//创建线程池

ExecutorService executorService = Executors.newFixedThreadPool(5);

//创建五个任务

for (int i = 0; i < 5; i++) {

//五个核心线程处理任务

executorService.submit(new Runnable() {

@Override

public void run() {

//保证数据的安全性, 使用同步锁

synchronized (“锁”){

for (int i1 = 0; i1 < 10000; i1++) {

a++;

}

}

}

});

}

try {

Thread.sleep(2000);

System.out.println(a);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

原子类方式: 使用AtomicInteger原子类

public class Demo08 {

public static void main(String[] args) {

//使用int原子类, 初始值设置为0

AtomicInteger atomicInteger = new AtomicInteger(0);

//创建线程池

ExecutorService executorService = Executors.newFixedThreadPool(5);

//创建五个任务

for (int i = 0; i < 5; i++) {

//五个核心线程处理任务

executorService.submit(new Runnable() {

@Override

public void run() {

for (int i1 = 0; i1 < 10000; i1++) {

//先获取再+1

int i2 = atomicInteger.incrementAndGet();

System.out.println(Thread.currentThread().getName()+": "+i2);

}

}

});

}

try {

Thread.sleep(2000);

System.out.println(“总和:”+atomicInteger.get());

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

原子类AtomicInteger方法介绍

public class Demo09 {

public static void main(String[] args) {

//创建原子类AtomicInterger, 设置初始值为1

AtomicInteger atomicInteger = new AtomicInteger(1);

ExecutorService executorService = Executors.newFixedThreadPool(5);

for (int i = 0; i < 1; i++) {

//五个核心线程处理任务

executorService.submit(new Runnable() {

@Override

public void run() {

for (int i1 = 0; i1 < 1; i1++) {

System.out.println(“------”);

//先+1, 再获取

int i2 = atomicInteger.incrementAndGet();

System.out.println(i2);

//先获取, 再+1

int andIncrement = atomicInteger.getAndIncrement();

System.out.println(andIncrement);

//先-1, 再获取

int i3 = atomicInteger.decrementAndGet();

System.out.println(i3);

//先获取, 再获取

int andDecrement = atomicInteger.getAndDecrement();

System.out.println(andDecrement);

//先+指定的值, 再获取

int i4 = atomicInteger.addAndGet(100);

System.out.println(i4);

//先获取, 再+指定的值

int andAdd = atomicInteger.getAndAdd(200);

System.out.println(andAdd);

//获取值

int i5 = atomicInteger.get();

System.out.println(i5);

System.out.println(“======”);

}

}

});

}

}

}

原子类AtomicInteger底层实现

原子类AtomicInteger底层使用的是volatile和cas

Volatitle(面试题)


在多线程环境下,volatile 关键字可以保证共享数据的可见性,有序性, 但是并不能保证对数据操作的原子性(数据操作是整体,整体只有成功或失败,不允许出现部分成功部分失败)。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的

可见性

内存可见性是指当一个线程修改了某个变量的值,其它线程总是能立即知道这个变量的变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程再B 在使用 V 的值时,能立即读到 V 的最新值(排除B正在用使用变量V)

可见性原理

现在CPU很少有单核CPU,都是双核、双核四线程、四核、四核八线程甚至更好的CPU。 CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,一定没有直接操作自己的高速缓存效率高,所以,为了提高处理速度,CPU 不直接和主内存进行通信,而是在 CPU 与内存之间加入高速缓存,它们比直接操作主内存的速度高的多

由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而总线嗅探机制是实现缓存一致性的常见机制

在这里插入图片描述

嗅探机制工作原理

每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中

有序性

什么是指令重排?

为了提高性能,编译器和处理器常常会对指令做重排序。

一般重排序可以分为如下三种类型:

  1. 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑

从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:

在这里插入图片描述

有序性原理

使用 volatile 修饰变量时,Java 编译器在生成字节码时,会在指令序列中插入内存屏障(CPU处理器的指令)来禁止CPU处理器重排序

CAS算法(面试题)


CAS算法介绍

CAS( Compare And Swap)算法,比较和交换算法。

CAS不需要和synchronized一样让对象具有独立性、互斥性保持线程安全。而是一种无锁也可以保证线程安全的算法。

理解:

给定代码:

//num++

num=num+1;

多个线程同时执行这段代码

synchronized: 一次只能有一个线程操作

CAS算法: 实现思路

线程开启时候,每个线程在主内存中拷贝一个变量副本, 每个线程操作自己的副本

1.首先获取到num在主内存中值, 存储为一个副本

2.然后对num的值+1

3.修改主内存中num值时,会比较主内存中的num值是否和副本中值相同

a. 如果主内存中num值和副本值相同,把主内存num值更新为新的值

b. 如果内存中num值和副本值不同,会从第1步重新开始执行

在这里插入图片描述

优点

保证数据操作的原子性,保证了线程是安全的

这个算法相对synchronized是比较“乐观的”,它不会像synchronized一样,当一个线程访问共享数据的时候,别的线程都在阻塞。synchronized不管是否有线程冲突都会进行加锁。由于CAS是非阻塞的,它死锁问题天生免疫,并且线程间的相互影响也非常小,更重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以它要比锁的方式拥有更优越的性能

缺点

多线程操作时每次修改值时,可能多次出现内存值和副本值不同的情况,需要循环执行多次

可能出现ABA问题

ABA问题

所谓ABA问题,其实用最通俗易懂的话语来总结就是狸猫换太子

比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题

比如有两个线程A、B:

  1. 主内存中变量为 V=3

1.两个线程A, B, 都拷贝了主内存中的变量 E=3

2.A线程执行到N=V+0, 即N=3 此时A线程挂起

3.B线程修改原值为N=V+1,即N=4,

4.然后B觉得修改错了,然后再重新把值修改为3;

5.A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存V的值等于线程A中E的值, 都为3,(但是却不知道B曾经修改过), A线程修改成功。

6.尽管线程A的CAS操作成功,结果也没有问题, 但不代表就没有问题, V的值是发生过改变的

a)有的需求,只注重结果,结果一直就可以, 可以不用考虑ABA的问题

b)但是有的需求,需要注重过程,必须考虑ABA的问题

解决ABA问题

为了解决ABA问题,一般都会在操作时设置一个int类型版本号(version),每次对内存中变量操作后都让版本号加1。当需要修改变量时,除了比较副本中值和内存值以外,还需要比较版本号是否相同。JDK中AtomicStampedReference就是这种方式,提供了全局int属性

源码分析AtomicInteger原子类


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

最后,强调几点:

  • 1. 一定要谨慎对待写在简历上的东西,一定要对简历上的东西非常熟悉。因为一般情况下,面试官都是会根据你的简历来问的; 能有一个上得了台面的项目也非常重要,这很可能是面试官会大量发问的地方,所以在面试之前好好回顾一下自己所做的项目;
  • 2. 和面试官聊基础知识比如设计模式的使用、多线程的使用等等,可以结合具体的项目场景或者是自己在平时是如何使用的;
  • 3. 注意自己开源的Github项目,面试官可能会挖你的Github项目提问;

我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目。

面试答案

三面头条+四面阿里+五面腾讯拿offer分享面经总结,最终入职阿里

三面头条+四面阿里+五面腾讯拿offer分享面经总结,最终入职阿里

三面头条+四面阿里+五面腾讯拿offer分享面经总结,最终入职阿里

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)**

img

最后

最后,强调几点:

  • 1. 一定要谨慎对待写在简历上的东西,一定要对简历上的东西非常熟悉。因为一般情况下,面试官都是会根据你的简历来问的; 能有一个上得了台面的项目也非常重要,这很可能是面试官会大量发问的地方,所以在面试之前好好回顾一下自己所做的项目;
  • 2. 和面试官聊基础知识比如设计模式的使用、多线程的使用等等,可以结合具体的项目场景或者是自己在平时是如何使用的;
  • 3. 注意自己开源的Github项目,面试官可能会挖你的Github项目提问;

我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!

以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目。

面试答案

[外链图片转存中…(img-KCB2efiX-1712165406074)]

[外链图片转存中…(img-e6jH0nIG-1712165406074)]

[外链图片转存中…(img-hNssUxCq-1712165406074)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值