leftso 6037 0 2021-10-09
版权申明:本文为博主原创文章,未经博主允许不得转载。 https://www.leftso.com/blog/910.html

1. 线程的安全性问题:

线程安全和非线程安全: 一个类在单线程环境下能够正常运行,并且在多线程环境下,使用方不做特别处理也能运行正常,我们就称其实线程安全的。反之,一个类在单线程环境下运行正常,而在多线程环境下无法正常运行,这个类就是非线程安全的。

线程安全问题体现在:
  • 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
出现线程安全问题的原因:
  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

2. 并行和并发有什么区别?

  • 并发:多个任务在同一个 CPU 核上,按时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
  • 并行:单位时间内,多个处理器同时处理多个任务,是真正意义上的“同时进行”。
  • 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻
  • 并发 = 两个队列和一台咖啡机。
  • 并行 = 两个队列和两台咖啡机。
  • 串行 = 一个队列和一台咖啡机。

3. 多线程

多线程:宏观上看,一个程序中可以同时运行多个不同的线程来执行不同的任务。

多线程的好处
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

坏处
并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

4. 进程和线程的区别

以java为例讲进程与线程的区别:

首先,进程是一个程序运行的实例,是一个资源分配的基本单位,而线程是程序独立运行的最小单位
从资源方面看,进程有自己独立的寻址空间,而且一个进程可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8后的元空间),但是每个线程也有自己的程序计数器、虚拟机栈、本地方法栈。
从安全性和健壮性方面比较,进程间是相互独立的,一个进程死掉不会影响其他的进程,而在多线程环境下,线程间极有可能相互影响,可能会存在死锁,线程不安全的情况。
从系统开销方面看,由于线程资源共享,使用相同的地址空间,因此线程切换的系统开销小,但不利于资源的管理和保护,进程则相反。
在通信方面,线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
5. Linux 上查找哪个线程cpu利用率最高?
windows上面用任务管理器看,linux下可以用 top 这个工具看。

找出cpu耗用厉害的进程pid, 终端执行top命令,然后按下shift+p 查找出cpu利用最厉害的pid号
根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查找出cpu利用率最厉害的线程号,比如top -H -p 1328
将获取到的线程号转换成16进制,去百度转换一下就行
使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat,比如jstack 31365 > /tmp/t.dat
编辑/tmp/t.dat文件,查找线程号对应的信息

6. 线程死锁

死锁:指两个或两个以上的进程(线程)由于竞争资源而造成的一种阻塞的现象,若无外力作用,将永远在互相等待,互相僵持下去。
比如说,两个线程互相持有对方资源,同时他们都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

形成死锁的四个必要条件是什么
  • 互斥条件:线程对于所分配到的资源具有排它性,即一个资源只能被一个线程占用,直到被该线程释放
  • 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:当发生死锁时,所等待的线程必定会形成一个环路(类似于死循环),造成永久阻塞
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的

破坏请求与保持条件
一次性申请所有的资源。

破坏不可剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。

7. 创建线程有哪几种方式?

创建线程有四种方式:
  • 继承 Thread 类;
  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 使用 Executors 工具类创建线程池
1) 继承 Thread 类

定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()是线程要执行的业务逻辑方法
创建自定义的线程子类对象
调用子类实例的star()方法来启动线程
2 )实现 Runnable 接口

定义Runnable接口实现类MyRunnable,并重写run()方法
创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
调用线程对象的start()方法
3)实现 Callable 接口
4. 创建实现Callable接口的类myCallable
5. 以myCallable为参数创建FutureTask对象
6. 将FutureTask作为参数创建Thread对象
7. 调用线程对象的start()方法

run()和 start()有什么区别?

run()方法
是在主线程中执行方法,和调用普通方法一样(按顺序执行,同步执行)。
start()方法
是创建了新的线程,在新的线程中执行(异步执行),只有通过调用线程类的start()方法可能真正达到多线程的目的。单独调用run()方法,是同步执行;通过start()调用run(),是异步执行。
同步
发送一个请求,等待返回,然后再发送下一个请求。
异步
发送一个请求,不等待返回,随时可以再发送下一个请求。

Callable 和 Future?

Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。

什么是 FutureTask

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

8. 说说线程的生命周期及五种基本状态?

新建(new):新创建了一个线程对象。

可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。

运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。

阻塞的情况分三种:
  • (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
  • (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
  • (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
线程锁流程

9. 线程同步以及线程调度相关的方法。

  • (1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • (2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
  • (3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
  • (4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

sleep() 和 wait() 有什么区别?

两者都可以暂停线程的执行
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

sleep()方法和 yield()方法有什么区别?

  • (1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  • (2)线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
  • (3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
  • (4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

10. 锁机制(悲观锁和乐观锁)

乐观锁:
每次访问数据的时候都认为其他线程不会修改数据,所以直接访问数据,更新的时候再判断在此期间其他线程是否修改数据。CAS和版本号机制是乐观锁的实现。
作用:
乐观锁适合多读场景,悲观锁适合多写情况。
版本号机制:数据有个version字段,表示被修改的次数。
CAS:无琐算法,非阻塞同步,需要读写的内存值V和旧的期望值A相同时,更新为B.一般都是自旋CAS,不断的重试。
乐观锁缺点:
  • 1、ABA问题(加入版本号机制)
  • 2、自旋CAS如果一直不成功,开销大。
  • 3、只对单变量有效,当涉及多个共享变量时,无效。
乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。CAS 操作包含三个操作数 —— 内存值(V)、预期原值(A)和新值(B)。 如果内存值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。CAS是通过硬件命令保证了原子性。

悲观锁:
-每次访问数据的时候都会认为其他线程会修改数据,所以先获取锁,再访问数据。synchronized和ReentrantLock都是悲观锁思想的实现。
Synchronized关键字三种实现方式:
  • 修饰实例方法,对当前实例对象加锁,进入同步代码前要获取对象实例的锁。
  • 修饰静态方法,对当前类对象加锁,
  • 修饰代码块,指定加锁对象,给对象加锁。
具体实例,双重校验锁实现对单例模式;
Synchronized同步的实现,是基于进入退出监视器Monitor对象实现的,无论是同步代码块还是同步方法,都是如此;同步代码块,是根据monitorenter 和 monitorexit 指令实现的,同步方法,是通过设置方法的 ACC_SYNCHRONIZED 访问标志;监视器Monitor对象存在于每个对象的对象头中。

10.CAS 的会产生什么问题?

1、ABA 问题:


比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

2、循环时间长开销大:


对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

3、只能保证一个共享变量的原子操作:


当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

11. synchronized和ReenTrantLock的区别

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

相同点:两者都是可重入锁
  1. 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。
  2. 比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,
  3. 当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
  4. 同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

主要区别如下
1)synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。 synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API,synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
2)ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
3)ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
4)ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
5)相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。

synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。它具有很好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象

互斥锁:同一时间只能被一个线程持有。
可重入锁:可以被单个线程多次获取。
公平锁:线程依次排队获取锁。
非公平锁:不管是不是队头都能获取。
  1. 公平锁和非公平锁,它们尝试获取锁的方式不同:
  2. 公平锁在尝试获取锁时,即使“锁”没有被任何线程锁持有,它也会判断自己是不是CLH等待队列的表头;
  3. 是的话,才获取锁。
  4. 而非公平锁在尝试获取锁时,如果“锁”没有被任何线程持有,则不管它在CLH队列何处,都直接获取锁。
  5. 公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,
  6. 如果有自己要挂起,加到队列后面,然后唤醒队列最前面的线程。
  7. 这种情况下相比较非公平锁多了一次挂起和唤醒。

线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。
  1. JDK1.6引入了大量的锁优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术减少开销。
  2. 锁主要存在4种状态:无琐状态,偏向锁状态,轻量级锁状态,重量级锁状态 。
  3. 锁可升级不可降级,提供获取锁和释放锁的效率。
  4. 自旋锁:进程进入阻塞的开销很大,为防止进入阻塞状态,在线程请求共享数据锁的时候循环自旋一段时间,如果在这段时间内获取到锁,就避免进入阻塞状态了。
  5. 1.6引入自适应自旋锁,自旋次数不再固定:由锁拥有者状态和上次获取锁的自旋次数决定。
  6. 锁消除:对于被检测出不可能存在竞争的共享数据的锁进行消除。(逃逸分析)
  7. 锁粗化:虚拟机探测到一系列连续操作都对同一个对象加锁解锁,就将加锁的范围粗化到整个操作系列的外部。
  8. 偏向锁:当锁对象第一次被线程获取的时候,进入偏向状态,标记为101,
  9. 同时CAS将线程ID进入到对象头的Mark Word中,如果成功,这个线程以后每次获取锁就不再需要进行同步操作,
  10. 甚至CAS不都需要。当另一个线程尝试获取这个锁,偏向状态结束,恢复到未锁定状态或者轻量级状态。
  11. 轻量级锁:对象头的内存布局Mark Word,有个tag bits,记录了锁的四种状态:无琐状态,偏向锁状态,轻量级锁状态,重量级锁状态.轻量级锁相对重量级锁而言,使用CAS去避免重量级锁使用互斥量的开销。线程尝试获取锁时,如果锁处于无琐状态,先采用CAS去尝试获取锁,如果成功,锁状态更新为轻量级锁状态。如果有两条以上的线程争用一个锁,状态重为重量级锁。

12. synchronized 和 volatile 的区别是什么?

synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

区别
  • volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。
  • 但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。

13. AQS

AQS(AbstactQueuedSynchronizer)同步队列器,是一个构建锁和同步器的框架,使用AQS能够简单有效的构造出应用广泛的大量同步器。如ReentrantLock, Semphore,其他的ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的
AQS原理:如果被请求的共享资源空闲,则将当前请求线程设为有效的工作线程,并且将共享资源设置为锁定状态。如果请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的。即将暂时获取不到的线程放入队列中。
 
  1. CLH,是虚拟的双向队列,即不存在队列实例,仅存在节点与节点之间的pre和next关系。
  2. AQS将每条请求共享资源的线程封装成一个CLH锁队列的一个节点来实现锁的分配。
  3. AQS属性(Node head, Node tail, int state(这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁), Thread 持有独占锁的线程);
  4. 等待队列中每个线程被封装为一个Node实例
  5. (thread + waitStatus(-1: 当前node的后继节点对应的线程需要被唤醒,) + pre + next);
  6. State:表示当前锁的状态,等于0时,表示没有被线程占用。当大于0时,表示被线程占用。
  7. Node节点的属性 watiStatus:默认为0,
  8. 当大于0时,表示放弃等待,ReentrantLock是可以指定timeouot的。
  9. 等于-1,表示当前node的后继节点对应的线程需要被唤醒。
  10. 当等于-2时,标志着线程在Condition条件上等待的线程唤醒。
  11. 等于-3时,用于共享锁,标志着下一个acquireShared方法线程应该被允许。

公平锁,只有处于队头的线程才被允许去获取锁。非公平性锁模式下线程上下文切换的次数少,因此其性能开销更小。公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

14. AQS 对资源的共享方式

Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

15. 线程池

线程池则是为了减少线程建立和销毁带来的性能消耗。线程池的使用可以优化资源的使用,提高响应的速度,方便管理。
Executors是个静态工厂类。

在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
  • (1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。
  • (2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
  • (3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
  • (4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

Executors 各个方法的弊端:

newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。

newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。

ThreadPoolExecutor,真正线程池实现类。

ThreadPoolExecutor线程池的7大参数:

corePoolSize:核心池的大小:创建线程池之后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

keepAliveTime:非核心线程的最大空闲时间。

TimeUnit:空闲时间的单位。

BlockingQueue workQueue :等待执行的任务阻塞队列,队列分为有界队列和无界队列。有界队列:队列的长度有上限,当核心线程满载的时候,新任务进来进入队列,当达到上限,有没有核心线程去即时取走处理,这个时候,就会创建临时线程。(警惕临时线程无限增加的风险)
无界队列:队列没有上限的,当没有核心线程空闲的时候,新来的任务可以无止境的向队列中添加,而永远也不会创建临时线程。(警惕任务队列无限堆积的风险)

ThreadFactory threadFactory:线程工厂,用来创建线程

RejectedExecution handler:队列已满,而且任务量大于最大线程的异常处理策略
线程池执行逻辑
 

16. JUC原子类

基本类型:AtomicInteger,AtomicLong,AtomicBoolean;
数组类型AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray;
引用类型,对象属性修改类型。
在32位操作系统中,64位的long 和 double 变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性;
原子类基本通过自旋CAS来实现,期望的值和现在的值是否一致,如果一致就更新。
 
    public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

主要利用CAS+volatile + native方法来保证操作的原子性,从而避免同步方法的高开销。CAS原理是那期望的值和现在的值进行比较,如果相同则更新成新的值。
提示:本文最后更新于【 2021-10-09 11:43:13 】,某些文章具有时效性,若有错误或已失效,请在下方留言

评论区域