Q&A:JUC
线程
线程和进程的区别?
进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
说说协程和线程区别?
守护线程和用户线程的区别?
用户线程:平时使用到的线程均为用户线程。
守护线程:用来服务用户线程的线程,例如垃圾回收线程。
任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(true/false)
设置
- true则是将该线程设置为守护线程,false则是将该线程设置为用户线程。
- 必须在
Thread.start()
之前调用,否则运行时会抛出异常。
守护线程和用户线程的区别主要在于Java虚拟机是否存活。
- 用户线程:当任何一个用户线程未结束,Java虚拟机则不会结束。
- 守护线程:如果只剩守护线程未结束,Java虚拟机结束。
线程的上下文切换
CPU会通过时间片分配算法来循环执行任务,当前任务执行完一个时间片后会切换到下一个任务,但切换前会保存上一个任务的状态,因为下次切换回这个任务时还要加载这个任务的状态继续执行,从任务保存到再加载的过程就是一次上下文切换。
线程状态及转换?
线程的生命周期和状态
在 Java API 中 java.lang.Thread.State
这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW | 初始状态,线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 |
RUNNABLE | 运行状态,包含就绪和运行中两种状态 |
BLOCKED | 阻塞状态 |
WAITING | 等待状态 |
TIME_WAITING | 超时等待状态,和等待状态不同的是,它可以在制定的时间自行返回 |
TERMINATED | 终止状态,线程运行结束 |
创建线程的几种方式?
- 继承 Thread 类创建线程;
- 实现 Runnable 接口创建线程;
- 通过 Callable 和 Future 创建线程;
- 通过线程池创建线程。
runnable 和 callable 有什么区别?
- Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹地去执行 run() 方法中的代码而已;
- Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。
线程死锁是如何产生的,如何避免
死锁:由于两个或两个以上的线程相互竞争对方的资源,而同时不释放自己的资源,导致所有线程同时被阻塞。
死锁产生的条件:
- 互斥条件:一个资源在同一时刻只由一个线程占用。
- 请求与保持条件:一个线程在请求被占资源时发生阻塞,并对已获得的资源保持不放。
- 循环等待条件:发生死锁时,所有的线程会形成一个死循环,一直阻塞。
- 不剥夺条件:线程已获得的资源在未使用完不能被其他线程剥夺,只能由自己使用完释放资源。
避免死锁的方法主要是破坏死锁产生的条件。
- 破坏互斥条件:这个条件无法进行破坏,锁的作用就是使他们互斥。
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏循环等待条件:按顺序来申请资源。
- 破坏不剥夺条件:线程在申请不到所需资源时,主动放弃所持有的资源。
死锁、活锁、饥饿有什么区别?
活锁:任务或者执行者没有被阻塞,由于某些条件没有被满足,导致线程一直重复尝试、失败、尝试、失败。
活锁和死锁的区别:
- 活锁是在不断地尝试、死锁是在一直等待。
- 活锁有可能自行解开、死锁无法自行解开。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源, 导致一直无法执行的状态。
死锁、饥饿的区别:饥饿可自行解开,死锁不行。
run() 和 start() 有什么区别?
- 线程是通过
Thread
对象所对应的方法run()
来完成其操作的,而线程的启动是通过start()
方法执行的。 -
run()
方法可以重复调用,start()
方法只能调用一次
可以直接调用 Thread 类的 run 方法吗?
为什么调用start()方法时会执行run()方法,而不直接执行run()方法?
结论:调用 start()
方法可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行,而在主线程中执行的。
调用 start()
方法,会启动一个线程,这时线程处于就绪状态,当分配到CPU时间片后,就开始执行run()
方法,它包含了要执行的这个线程的内容,这是真正的多线程工作。
直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,这样就没有达到多线程的目的。
sleep() 和 wait() 的区别?
共同点 :两者都可以暂停线程的执行。
区别 :
sleep()
方法没有释放锁,而wait()
方法释放了锁 。wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒,或者也可以使用wait(long timeout)
超时后线程会自动苏醒。sleep()
是Thread
类的静态本地方法,wait()
则是Object
类的本地方法。为什么这样设计呢?
sleep() 和 yield() 有什么不同
-
sleep()
方法会使得当前线程暂停指定的时间,没有消耗CPU时间片。 -
sleep()
一定会完成给定的休眠时间,yield()
不一定能完成。 -
sleep()
使得线程进入到阻塞状态,yield()
只是对CPU进行提示,如果CPU没有忽略这个提示,会使得线程上下文的切换,进入到就绪状态。 -
sleep()
需要抛出InterruptedException,而yield()
方法无需抛出异常。
为什么wait(),notify()必须在同步方法或者同步块中被调用?
因为wait()暂停的是持有锁的对象,notify()或notifyAll()唤醒的是等待锁的对象。
所以wait()、notify()、notifyAll()都需要线程持有锁的对象,进而需要在同步方法或者同步块中被调用。
为什么wait()、notify()、notifyAll()被定义在Object类中而不是在Thread类中?
等待和唤醒必须是同一锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在Object类中。
Java如何实现两个线程之间的通信和协作?
-
syncrhoized
加锁的线程的Object
类的wait()
/notify()
/notifyAll()
- 使用
volatile
关键字。基于volatile关键字实现线程间相互通信,其底层使用了共享内存。简单来说,就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。 -
ReentrantLock
类加锁的线程的Condition
类的await()
/signal()
/signalAll()
- 基于
LockSupport
实现线程间的阻塞和唤醒。LockSupport
是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。 - 使用JUC工具类
CountDownLatch
。jdk1.5 之后在java.util.concurrent
包下提供了很多并发编程相关的工具类,简化了并发编程开发,CountDownLatch
基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。 - 通过管道进行线程间通信:1)字节流;2)字符流 ,就是一个线程发送数据到输出管道,另一个线程从输入管道读数据。
什么是线程同步?什么是线程互斥?他们是如何实现的?
线程互斥是指某一个资源只能被一个访问者访问,具有唯一性和排他性。但访问者对资源访问的顺序是乱序的。
线程同步是指在互斥的基础上使得访问者对资源进行有序访问。 需要等待前面结果返回,才能继续运行
线程同步的实现方法:
- 同步方法
- 同步代码块
-
wait()
和notify()
- 使用volatile实现线程同步
- 使用重入锁实现线程同步
- 使用局部变量实现线程同步
- 使用阻塞队列实现线程同步
如何保证线程的运行安全?
线程安全问题主要体现在原子性、可见性和有序性。
- 原子性:一个或者多个操作在 CPU 执行的过程中不被中断的特性。线程切换带来的原子性问题。
- 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。缓存导致的可见性问题。
- 有序性:程序执行的顺序按照代码的先后顺序执行。编译优化带来的有序性问题。
解决方法:
- 原子性问题:可用JDK
Atomic
开头的原子类、synchronized
、LOCK
来解决 - 可见性问题:可用
synchronized
、volatile
、LOCK
来解决 - 有序性问题:可用
Happens-Before
规则来解决
线程安全的实现方法
- 互斥同步:synchronized , ReentrantLock
- 非阻塞同步:CAS,Atomic类
- 无同步方案:栈封闭,本地存储(ThreadLocal),可重入代码
如何停止一个正在运行的线程?
- 中断:
Interrupt
方法中断线程 - 使用
volatile boolean
标志位停止线程:在线程中设置一个boolean
标志位,同时用volatile
修饰保证可见性,在线程里不断地读取这个值,其他地方可以修改这个boolean
值。 - 使用
stop()
方法停止线程,但该方法已经被废弃。因为这样线程不能在停止前保存数据,会出现数据完整性问题。
synchronized
synchronized解决的是多个线程之间访问资源的同步性,synchronized可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized的用法有哪些?
如何在项目中使用 synchronized 的? synchronized关键字的使用方法?
- 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
synchronized三大特性是什么?
synchronized的作用有哪些 可见性和原子性有什么区别
原子性:一个或多个操作要么全部执行成功,要么全部执行失败。确保线程互斥的访问同步代码;
可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。保证共享变量的修改能够及时可见;
有序性:程序的执行顺序会按照代码的先后顺序执行。有效解决重排序问题。
synchronized可实现什么类型的锁?
悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。
非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。
可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。
独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。
synchronized 底层实现原理?
synchronized 同步代码块的实现是通过 monitorenter
和 monitorexit
指令,
-
monitorenter
指令指向同步代码块的开始位置, monitorexit
指令则指明同步代码块的结束位置。
当执行 monitorenter
指令时,线程试图获取锁也就是获取 monitor
的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit
指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
JDK1.6 后 synchronized 底层做了哪些优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
36.jdk1.6为什么要对synchronized进行优化?做了哪些优化? - 路人张的面试笔记 (mianshi.online)
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?-帅地玩编程 (iamshuaidi.com)
偏向锁、轻量级锁、重量级锁的对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了响应速度 | 如线程成始终得不到锁竞争的线程,使用自旋会消耗CPU性能 | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步块执行速度较长 |
了解锁消除吗?了解锁粗化吗?
锁消除是指Java虚拟机在即时编译时,通过对运行上下的扫描,消除那些不可能存在共享资源竞争的锁。锁消除可以节约无意义的请求锁时间。
锁粗化是指当虚拟机探测到一串的操作对相同对象多次加锁,,导致线程发生多次重入,将会把加锁的范围扩展(粗化)到整个操作序列的外部。因为频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化
synchronized和volatile的区别
-
volatile
主要是告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 -
volatile
作用于变量,synchronized
作用于代码块或者方法。 -
volatile
仅可以保证数据的可见性和有序性,不能保证数据的原子性。synchronized
可以保证数据的可见性、有序性、原子性。 -
volatile
不会造成线程的阻塞,synchronized
会造成线程的阻塞。 -
volatile
标记的变量不会被编译器优化;synchronized
标记的变量可以被编译器优化。
synchronized和ReentrantLock区别
相同点:两者都是可重入锁,即自己可以再次获取自己的内部锁
区别:
1、synchronized 是关键字,ReentrantLock 是类,这是二者的本质区别。
既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性:等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)、性能已不是选择标准。
等待可中断 : ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
可实现公平锁 : ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过 ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。
可实现选择性通知(锁可以绑定多个条件): synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
2、 synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API。
synchronized 是依赖于 JVM 实现的,JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
3、ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁
synchronized和Lock的区别?
- Lock是显示锁,需要手动开启和关闭。synchronized是隐士锁,可以自动释放锁。
- Lock是一个接口,是JDK实现的。synchronized是一个关键字,是依赖JVM实现的。
- Lock是可中断锁,synchronized是不可中断锁,需要线程执行完才能释放锁。
- 发生异常时,Lock不会主动释放占有的锁,必须通过unlock进行手动释放,因此可能引发死锁。synchronized在发生异常时会自动释放占有的锁,不会出现死锁的情况。
- Lock可以判断锁的状态,synchronized不可以判断锁的状态。
- Lock实现锁的类型是可重入锁、公平锁。synchronized实现锁的类型是可重入锁,非公平锁。
- Lock适用于大量同步代码块的场景,synchronized适用于少量同步代码块的场景。
ReentrantLock 是如何实现可重入性的?
ReentrantLock
内部自定义了同步器sync,在加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,检查当前维护的那个线程ID和当前请求的线程ID是否 一致,如果一致,同步状态加1,表示锁被当前线程获取了多次。
volatile
volatile的特性有哪些?
并发编程的三大特性为可见性、有序性和原子性。通常来讲
volatile
可以保证可见性和有序性。
- 可见性:
volatile
可以保证不同线程对共享变量进行操作时的可见性。即当一个线程修改了共享变量时,另一个线程可以读取到共享变量被修改后的值。 - 有序性:
volatile
会通过禁止指令重排序进而保证有序性。 - 原子性:对于单个的
volatile
修饰的变量的读写是可以保证原子性的,但对于i++
这种复合操作并不能保证原子性。这句话的意思基本上就是说volatile
不具备原子性了。
volatile的作用是什么?
- 防重排序
- 如果我们将变量声明为
volatile
,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。 - 实现可见性
- 保证原子性:单次读/写
voliatile的实现原理?
volatile 可见性实现
volatile 有序性实现
volatile实现内存可见性原理?
导致内存不可见的主要原因就是Java内存模型中的本地内存和主内存之间的值不一致所导致,例如上面所说线程A访问自己本地内存A的X值时,但此时主内存的X值已经被线程B所修改,所以线程A所访问到的值是一个脏数据。那如何解决这种问题呢?
volatile
可以保证内存可见性的关键是volatile
的读/写实现了缓存一致性,缓存一致性的主要内容为:
- 每个处理器会通过嗅探总线上的数据来查看自己的数据是否过期,一旦处理器发现自己缓存对应的内存地址被修改,就会将当前处理器的缓存设为无效状态。此时,如果处理器需要获取这个数据需重新从主内存将其读取到本地内存。
- 当处理器写数据时,如果发现操作的是共享变量,会通知其他处理器将该变量的缓存设为无效状态。
那缓存一致性是如何实现的呢?可以发现通过volatile
修饰的变量,生成汇编指令时会比普通的变量多出一个Lock
指令,这个Lock
指令就是volatile
关键字可以保证内存可见性的关键,它主要有两个作用:
- 将当前处理器缓存的数据刷新到主内存。
- 刷新到主内存时会使得其他处理器缓存的该内存地址的数据无效。
Java内存的可见性问题
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。
如何保证变量的可见性?
在 Java 中,volatile
关键字可以保证变量的可见性,如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。
volatile如何实现有序性
为了实现volatile的内存语义,编译器在生成字节码时会通过插入内存屏障来禁止指令重排序。
volatile是如何实现可见性的? 内存屏障。
volatile是如何实现有序性的? happens-before等
volatile能保证原子性吗?
volatile
关键字能保证变量的可见性、有序性,但不能保证对变量的操作是原子性的。
i++为什么不能保证原子性?
volatile能使一个非原子操作变成一个原子操作吗?
volatile
只能保证可见性和有序性,但可以保证64位的long
型和double
型变量的原子性。
对于32位的虚拟机来说,每次原子读写都是32位的,会将long
和double
型变量拆分成两个32位的操作来执行,这样long
和double
型变量的读写就不能保证原子性了,而通过volatile
修饰的long和double型变量则可以保证其原子性。
之前32位机器上共享的long和double变量的为什么要用volatile? 现在64位机器上是否也要设置呢?
说下volatile的应用场景?
如何禁止指令重排序?
如果我们将变量声明为 volatile
,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
双重检验锁(DCL)实现单例模式的原理
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (singleton == null) {
//类对象加锁
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么要在变量 singleton
之间加上 volatile
关键字?
因为对象的构造过程分为三个步骤:(singleton = new Singleton();
)
- 分配内存空间【为
singleton
分配内存空间】 - 初始化对象【初始化
singleton
】 - 将内存空间的地址赋给对象的引用【将
singleton
指向分配的内存地址】
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。
例如,线程 T1 执行了 1 和 3,此时 T2 调用
getUInstance
() 后发现singleton
不为空,因此返回singleton
,但此时singleton
还未被初始化。
为什么要进行指令重排?
计算机在执行程序的过程中,编译器和处理器通常会对指令进行重排序,这样做的目的是为了提高性能。
指令重排序一般分为编译器优化重排、指令并行重拍和内存系统重排三种。
- 编译器优化重排:编译器在不改变单线程程序语义的情况下,可以对语句的执行顺序进行重新排序。
- 指令并行重排:现代处理器多采用指令级并行技术来将多条指令重叠执行。对于不存在数据依赖的程序,处理器可以对机器指令的执行顺序进行重新排列。
- 内存系统重排:因为处理器使用缓存和读/写缓冲区,使得加载(load)和存储(store)看上去像是在乱序执行。
注:简单解释下数据依赖性:如果两个操作访问了同一个变量,并且这两个操作有一个是写操作,这两个操作之间就会存在数据依赖性,例如:
a = 1;
b = a;
这三种指令重排说明了一个问题,就是指令重排在单线程下可以提高代码的性能,但在多线程下可以会出现一些问题。
指令重排会带来什么问题?
指令重排在单线程下可以提高代码的性能,但在多线程下会破坏程序的语义
as-if-serial规则和happens-before规则的区别?
内存屏障的原理
编译器对内存屏障插入策略的优化
ThreadLocal
什么是ThreadLocal?
ThreadLocal
是 JDK java.lang
包下的一个类,ThreadLocal
为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,并且不会和其他线程的局部变量冲突,实现了线程间的数据隔离。分配在堆内的 TLAB 中
ThreadLocal 解决了哪些问题
ThreadLocal 有什么用?用来解决什么问题的? 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
ThreadLocal
类正是为了解决这样的问题。 ThreadLocal
类主要解决的就是让每个线程绑定自己的值,实现了线程间的数据隔离
如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
ThreadLocal 作用:
- 线程并发:应用在多线程并发的场景下
- 传递数据:通过 ThreadLocal 实现在同一线程不同函数或组件中传递公共变量,减少传递复杂度
- 线程隔离:每个线程的变量都是独立的,不会互相影响
ThreadLocal原理?ThreadLocal是如何实现线程隔离的?
JDK8 以后:每个 Thread 内部维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 是真正要存储的值
- Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成副本的隔离,互不干扰
每个线程都有一个ThreadLocalMap
(ThreadLocal
内部类),Map中元素的键为ThreadLocal
,而值对应线程的变量副本。
调用threadLocal.set()
–>调用getMap(Thread)
–>返回当前线程的ThreadLocalMap<ThreadLocal, value>
–>map.set(this, value)
,this是threadLocal
本身。源码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
调用get()
–>调用getMap(Thread)
–>返回当前线程的ThreadLocalMap<ThreadLocal, value>
–>map.getEntry(this)
,返回value
。源码如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal有哪些应用场景?
ThreadLocal
的应用场景主要有以下几个方面:
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
- 保存线程上下文信息,在需要的地方可以获取
- 线程间数据隔离
- 数据库连接
- session管理
- 用于保存线程不安全的工具类,SimpleDateFormat
ThreadLocal为什么会造成内存泄露? 如何解决
ThreadLocal 内存泄露问题是怎么导致的? ThreadLocal 内存泄漏如何解决 ThreadLocal 内部如何防止内存泄漏,在哪些方法中存在
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。
如何解决
ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。
每次使⽤完ThreadLocal
就调⽤它的remove()
⽅法,最好手动将对应的键值对删除,从⽽避免内存泄漏。
ThreadLocal 底层数据结构
ThreadLocal 底层是通过 ThreadLocalMap 这个静态内部类来存储数据的,ThreadLocalMap 就是一个键值对的 Map,它的底层是 Entry 对象数组,Entry 对象中存放的键是 ThreadLocal 对象,值是 Object 类型的具体存储内容。
ThreadLocalMap 的散列方式
// 获取当前 ThreadLocal 对象的散列值
int i = key.threadLocalHashCode & (table.length - 1);
这行代码得到的值其实是一个 ThreadLocal 对象的散列值,这就是 ThreadLocal 的散列方式,我们称之为 斐波那契散列 。
ThreadLocal 的散列方式称之为斐波那契散列,每次获取哈希值都会加上 HASH_INCREMENT
,这样做可以尽量避免 hash 冲突,让哈希值能均匀的分布在 2 的 n 次方的数组中
计算 ThreadLocal 对象的哈希值:
private final int threadLocalHashCode = nextHashCode()
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
每创建一个 ThreadLocal 对象就会使用 nextHashCode 分配一个 hash 值给这个对象:
private static AtomicInteger nextHashCode = new AtomicInteger()
斐波那契数也叫黄金分割数,hash 的增量就是这个数字,带来的好处是 hash 分布非常均匀:
private static final int HASH_INCREMENT = 0x61c88647
ThreadLocalMap 如何处理哈希冲突
ThreadLocalMap 使用线性探测法来解决哈希冲突,该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍,在探测过程中 ThreadLocal 会复用 key 为 null 的脏 Entry 对象,并进行垃圾清理,防止出现内存泄漏
假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个环形数组
ThreadLocalMap 扩容机制
当 ThreadLocalMap 内元素超过最大大小的 3/4 时会进行扩容,新 map 大小为 2 倍。扩容时从第一个entry 开始,依次往新 map 里迁移,当中会丢弃掉 key 为 null 的。
ThreadLocal原理总结
ThreadLocal 将自身作为key,和需要保存的value一起存入到当前线程的 threadlocalmap 中。
ThreadLocalMap 的 key 是一个 threadLocal 变量,ThreadLocalMap 对它的引用是一个弱引用,也就是说,如果除了 ThreadLocalMap 以外没有指向 threadLocal 的引用,那么这个 threadlocal 就会被回收。
ThreadLocalMap 本质是张哈希表。为了增加计算效率,ThreadLocal 类维护了一个静态变量 nextHashCode,每一个 threadlocal 实例都会获得属于自己的 hashcode,这是通过对 nextHashCode 不断加 0x61c88647 实现的(为啥是0x61c88647 呢我也不知道,可能会比较均匀吧)。ThreadLocalMap 使用线性探测法解决哈希冲突:每个 threadlocal 在插入时会检查数组的当前位置的情况,如果为 null 直接插入(为 null 表示弱引用被 gc 掉了),不为 null 则顺序查找下一个,直到找到空位置为止。
当 ThreadLocalMap 内元素超过最大大小的 3/4 时会进行扩容,新 map 大小为 2 倍。扩容时从第一个entry 开始,依次往新 map 里迁移,当中会丢弃掉 key 为 null 的。
ThreadLocal 如何实现父子线程间局部变量共享
一文让你彻底明白ThreadLocal_木子雷的博客-CSDN博客
final
final基础使用
final
修饰的类不能被继承final
修饰的方法不能被重写final
修饰的变量是基本数据类型则值不能改变final
修饰的变量是引用类型则不能再指向其他对象
final方法可以被重载吗? 可以
父类的final方法能不能够被子类重写? 不可以
如果字段由static和final修饰,仅能在声明时赋值或声明后在静态代码块中赋值,因为该字段属于这个类。
所有的final修饰的字段都是编译期常量吗?
public class Test {
//编译期常量
final int i = 1;
final static int J = 1;
final int[] a = {1,2,3,4};
//非编译期常量
Random r = new Random();
final int k = r.nextInt();
public static void main(String[] args) {
}
}
不是所有的final修饰的字段都是编译期常量,k的值由随机数对象决定,只是k的值在被初始化后无法被更改。
说说final类型的类如何拓展?
比如String是final类型,我们想写个MyString复用所有String中方法,同时增加一个新的toMyString()的方法,应该如何做?
设计模式中最重要的两种关系,一种是继承/实现;另外一种是组合关系。所以当遇到不能用继承的(final修饰的类),应该考虑用组合, 如下代码大概写个组合实现的意思:
class MyString{
private String innerString;
// ...init & other methods
// 支持老的方法
public int length(){
return innerString.length(); // 通过innerString调用老的方法
}
// 添加新方法
public String toMyString(){
//...
}
}
说说final域重排序规则?
说说final的原理?
使用 final 的限制条件和局限性?
String为什么不可变
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[];
//...
}
String
内部用private final
修饰char
数组,并且String
类没有提供暴露修改这个数组的方法。- 用
final
修饰char
数组,这个数组无法被修改。(仅仅是指引用地址不可被修改,并非是value[]这个数组的内容不可修改) - 用
private
修饰char
数组,且String
类不提供修改这个数组的方法,所以初始化之后我们没法改变数组的内容。
- 用
String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
Java 作者在 String 的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String 对象。比如 substring 方法
如何让String可变
value是私有属性,你只需要一种方法访问类的私有属性即可。使用反射可以直接修改value数组中的内容,当然建议不要这样做。
乐观锁
什么是CAS?
CAS全称Compare And Swap
,比较与交换,Java中可以通过CAS操作来保证原子性,是乐观锁的主要实现方式。CAS在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock
内部的AQS和原子类内部都使用了CAS。
CAS算法涉及到三个操作数:
- 需要读写的内存值V(内存值)。
- 进行比较的值A(旧值)。
- 要写入的新值B(新值)。
只有当V的值等于A时,才会使用原子方式用新值B来更新V的值,否则会继续重试直到成功更新值。
当多个线程同时操作一个共享变量时,只有一个线程可以对变量进行成功更新,其他线程均会失败,但是失败并不会被挂起,进行再次尝试,也就是自旋。Java中的自旋锁就是利用CAS来实现的。
原子操作类是CAS在Java中的应用
CAS存在的问题及优点
其中ABA问题是面试中比较常见的问题
- ABA问题
在CAS的算法流程中,首先要先比较V的值和A的值,如果相等则进行更新。
ABA 问题:当进行获取主内存值时,该内存值在写入主内存时已经被修改了 N 次,但是最终又改成原来的值。即其他线程先把 A 改成 B 又改回 A,主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,这时 CAS 虽然成功,但是过程存在问题。
ABA 问题的解决方式:ABA 的解决方法也很简单,就是利用版本号。给变量加上一个版本号,每次变量更新的时候就把版本号加1,这样即使 旧值A 的从 A—>B—>A,版本号也发生了变化,这样就解决了CAS出现的ABA问题。基于CAS的乐观锁也是这个实现原理。
- 循环时间过长导致开销太大
CAS自旋时间过长会给CPU带来非常大的开销
- 只能保证一个共享变量的原子操作
在操作一个共享变量时,可以通过CAS的方式保证操作的原子性,但如果对多个共享变量进行操作时,CAS则无法保证操作的原子性,这时候就需要用锁了。在看《Java并发编程的艺术》时,里面提到了一个办法可以参考一下,就是将多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a
,合并成ij=2a
,然后用CAS来操作ij
那么CAS有什么优点呢?在并发量不是很大时提高效率。
并发队列的无锁化可以怎么实现?
…
说下对悲观锁和乐观锁的理解?
- 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如:行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
- 乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
- 两种锁的使用场景
两种锁各有优缺点,不可认为一种好于另一种,
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁和悲观锁的区别可以说一下吗?
乐观锁常见的两种实现方式是什么?
乐观锁一般会使用版本号机制或者 CAS 算法实现。
- 版本号机制 一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
- CAS 算法 即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数:需要读写的内存值 V、进行比较的值 A、拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
CAS 和 synchronized 的使用场景?
悲观锁和乐观锁的使用场景
简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)。
1、对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
Jvm 中的 cas 是怎么实现的?操作系统中的呢?
请阐述你对Unsafe类的理解?
Unsafe 是 CAS 的核心类,由于 Java 无法直接访问底层系统,需要通过本地(Native)方法来访问
Unsafe 类存在 sun.misc 包,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,其中所有方法都是 native 修饰的,都是直接调用操作系统底层资源执行相应的任务,基于该类可以直接操作特定的内存数据,其内部方法操作类似 C 的指针,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
Java魔法类:Unsafe应用解析 - 美团技术团队 (meituan.com)
简单说下对 Java 中的原子类的理解?
包含13个,4组分类,说说作用和使用场景。 Atomic 指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
原子操作类是CAS在Java中的应用,从JDK1.5开始提供了java.util.concurrent.atomic
包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。Atomic包里的类基本都是使用Unsafe
实现的包装类。
JUC包中的4种原子类
- 基本类型:使用原子的方式更新基本类型
-
AtomicInteger
:整形原子类 -
AtomicLong
:长整型原子类 -
AtomicBoolean
:布尔型原子类
-
- 数组类型:使用原子的方式更新数组里的某个元素
-
AtomicIntegerArray
:整形数组原子类 -
AtomicLongArray
:长整形数组原子类 -
AtomicReferenceArray
:引用类型数组原子类
-
- 引用类型:
-
AtomicReference
:引用类型原子类,存在ABA问题 -
AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。 -
AtomicMarkableReference
:原子更新带有标记位的引用类型
-
- 原子更新字段类
-
AtomicIntegerFieldUpdater
:原子更新整型的字段的更新器。 -
AtomicLongFieldUpdater
:原子更新长整型字段的更新器。 -
AtomicReferenceFieldUpdater
:引用类型更新器原子类
-
AtomicInteger底层实现? CAS+volatile
atomic 的原理是什么?
Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。我们需要先知道一个东西就是 Unsafe 类,全名为:sun.misc.Unsafe,这个类包含了大量的对 C 代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过 unsafe 分配内存的时候,如果自己指定某些区域可能会导致一些类似 C++ 一样的指针越界到其他进程的问题。
线程池
什么是线程池?
线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作
线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务
池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销
为什么要用线程池?
为什么要使用Executor线程池框架呢?说下对线程池的理解?
线程池能够对线程进行统一分配,调优和监控:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池原理是什么?
假如我把 corepoolsize 和 maxpoolsize 分别设置为 5 和 10,假设任务计算时间很长,我往里面连续放入 20 个任务,按时间顺序会发生什么
- 创建线程池,等待提交过来的任务请求(懒惰),调用 execute 方法才会创建线程【①】
- 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务【②】
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列【③】
- 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程立刻运行这个任务,对于阻塞队列中的任务不公平。这是因为创建每个 Worker(线程)对象会绑定一个初始任务,启动 Worker 时会优先执行【④】
- 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行【⑤】
- 当一个线程完成任务时,会从队列中取下一个任务来执行【take/poll】
- 当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小
线程池创建的方法
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
线程池的常用创建方式主要有两种,通过通过ThreadPoolExecutor 的构造方法创建和Executors工厂方法创建
方式一:通过 ThreadPoolExecutor 的构造方法实现:
方式二:通过 Executor 框架的工具类 Executors 来实现:
可以创建三种类型的 ThreadPoolExecutor:
- FixedThreadPool :
newFixedThreadPool
方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 - SingleThreadExecutor:
newSingleThreadExecutor
方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 - CachedThreadPool:
newCachedThreadPool
方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
为什么不允许使用Executors创建线程池? 那么推荐怎么使用呢?
《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
推荐方式 1
首先引入:commons-lang3包
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
推荐方式 2
首先引入:com.google.guava包
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
// excute
pool.execute(()-> System.out.println(Thread.currentThread().getName()));
//gracefully shutdown
pool.shutdown();
推荐方式 3
spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可
<bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="10" />
<property name="maxPoolSize" value="100" />
<property name="queueCapacity" value="2000" />
<property name="threadFactory" value= threadFactory />
<property name="rejectedExecutionHandler">
<ref local="rejectedExecutionHandler" />
</property>
</bean>
//in code
userThreadPool.execute(thread);
ThreadPoolExecutor构造函数的参数分析
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
-
corePoolSize(线程池的基本大小)
:核心线程数,定义了最小可以同时运行的线程数量。 -
maximumPoolSize(线程池最大数量)
:线程中允许存在的最大工作线程数量 -
keepAliveTime(线程活动保持时间)
:当线程池中的数量大于核心线程数时,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到时间超过keepAliveTime
时才会被销毁。 -
unit(线程活动保持时间的单位)
:keepAliveTime
参数的时间单位。可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。 - **
workQueue(任务队列)
**:存放任务的阻塞队列。新来的任务会先判断当前运行的线程数是否到达核心线程数,如果到达的话,任务就会先放到阻塞队列。 -
threadFactory(线程工厂)
:为线程池提供创建新线程的线程工厂。 -
handler(饱和策略)
:线程池任务队列超过maxinumPoolSize
之后的拒绝策略。
ThreadPoolExecutor的任务队列
可以选择以下几个阻塞队列:(都是BlockingQueue 的实现类)
- 1)ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- 2)LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列。
- 3)SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
- 4)PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
ThreadPoolExecutor的饱和策略
说说ThreadPoolExecutor有哪些RejectedExecutionHandler策略? 默认是什么策略? 当队列满了并且worker的数量达到maxSize的时候,会怎么样? 执行具体的拒绝策略
当同时运行的线程数量达到最大线程数量并且阻塞队列也已经放满了任务时,ThreadPoolExecutor
会指定一些饱和策略。主要有以下四种类型:
-
AbortPolicy
策略:该策略会直接抛出异常拒绝新任务,默认策略 -
CallerRunsPolicy
策略:只用调用者所在线程来运行任务。 -
DiscardPolicy
策略:直接丢弃新任务。 -
DiscardOleddestPolicy
策略:丢弃最早的未处理的任务请求。
当然,也可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。如记录日志或持久化存储不能处理的任务。
补充:其他框架拒绝策略
- Dubbo:在抛出 RejectedExecutionException 异常前记录日志,并 dump 线程栈信息,方便定位问题
- Netty:创建一个新线程来执行任务
- ActiveMQ:带超时等待(60s)尝试放入队列
- PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
线程池的执行流程
创建线程池创建后提交任务的流程如下图所示:
简要说下线程池的任务执行机制?
execute –> addWorker –>runworker (getTask)
线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。 从Woker类的构造方法实现可以发现: 线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。 firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
线程池中任务是如何提交的?
线程池中任务是如何关闭的?
线程池中的的线程数一般怎么设置?需要考虑哪些问题?
配置线程池需要考虑哪些因素? 线程池里的具体的数量,如何去定
从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且尽可能地使用有界的工作队列。
创建多大容量的线程池合适?
- 一般来说池中总线程数是核心池线程数量两倍,确保当核心池有线程停止时,核心池外有线程进入核心池
- 过小会导致程序不能充分地利用系统资源、容易导致饥饿
- 过大会导致更多的线程上下文切换,占用更多内存 上下文切换:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换
性质不同的任务可用使用不同规模的线程池分开处理:(核心线程数常用公式)
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务。比如在内存中对大量数据进行排序。
IO 密集型是涉及到网络读取,文件读取的这类任务。这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
如何监控线程池的状态?
可以使用ThreadPoolExecutor以下方法:
getTaskCount()
Returns the approximate total number of tasks that have ever been scheduled for execution.getCompletedTaskCount()
Returns the approximate total number of tasks that have completed execution. 返回结果少于getTaskCount()。getLargestPoolSize()
Returns the largest number of threads that have ever simultaneously been in the pool. 返回结果小于等于maximumPoolSizegetPoolSize()
Returns the current number of threads in the pool.getActiveCount()
Returns the approximate number of threads that are actively executing tasks.
execute()方法和submit()方法的区别
这个地方首先要知道Runnable接口和Callable接口的区别,之前有写到过
execute()
和submit()
的区别主要有两点:
-
execute()
方法只能执行Runnable
类型的任务,没有返回值。submit()
方法可以执行Runnable
和Callable
类型的任务,当任务类型为Callable
时,返回值类型为Future
,但当任务类型为Runnable
时,返回值为null
。 -
execute()
会直接抛出任务执行时的异常,submit()
会吞掉异常,可通过 Future 的get()
方法将任务执行时的异常重新抛出
execute()方法 | submit()方法 |
---|---|
在 Executor 接口中声明 | 在 ExecutorService 接口中声明 |
执行Runnable 类型的任务 | 执行Runnable和 Callable类型的任务 |
返回类型为 void | 返回类型为 Future |
会直接抛出任务执行时的异常 | 会吞掉异常,可通过 Future 的 get() 方法将任务执行时的异常重新抛出 |
Java 自带的线程池有哪些?
为什么不推荐用 Java 预设的线程池
1、CachedThreadPool:这种线程池没有等待队列(队列长度为 0)内部没有核心线程,线程的数量是有没限制的。 2、FixedThreadPool:该线程池的最大线程数等于核心线程数。等待队列长度无限。 3、SingleThreadPool:有且仅有一个工作线程执行任务。等待队列长度不限。 4、ScheduledThreadPool:设置了核心线程数。等待队列长度无限,会按任务启动时间排序,若不到时间则会阻塞。
不要使用 Java 自带的线程池。因为 CachedThreadPool 会无限创建线程,FixedThreadPool、SingleThreadPool、ScheduledThreadPool 会无限增加队列长度,会导致线程数溢出或内存溢出。
并发容器
AQS
?参考资料
相关文章
- Jgit的使用笔记
- 利用Github Action实现Tornadofx/JavaFx打包
- 叹息!GitHub Trending 即将成为历史!
- 微软软了?开源社区讨论炸锅,GitHub CEO 亲自来答
- GitHub Trending 列表频现重复项,前后端都没去重?
- Photoshop Elements 2021版本软件安装教程(mac+windows全版本都有)
- (ps全版本)Photoshop 2020的安装与破解教程(mac+windows全版本都有)
- (ps全版本)Photoshop cc2018的安装与破解教程(mac+windows全版本,包括2023
- 环境搭建:Oracle GoldenGate 大数据迁移到 Redshift/Flat file/Flume/Kafka测试流程
- 每个开发人员都要掌握的:最小 Linux 基础课
- 来撸羊毛了!Windows 环境下 Hexo 博客搭建,并部署到 GitHub Pages
- 超实用!手把手入门 MongoDB:这些坑点请一定远离
- 【GitHub日报】22-10-09 zustand、neovim、webtorrent、express 等4款App今日上新
- 【GitHub日报】22-10-10 brew、minio、vite、seaweedfs、dbeaver 等8款App今日上新
- 【GitHub日报】22-10-11 cobra、grafana、vue、ToolJet、redwood 等13款App今日上新
- Photoshop 2018 下载及安装教程(mac+windows全版本都有,包括最新的2023)
- Photoshop 2017 下载及安装教程(mac+windows全版本都有,包括最新的2023)
- Photoshop 2020 下载及安装教程(mac+windows全版本都有,包括最新的2023)
- Photoshop 2023 资源免费下载(mac+windows全版本都有,包括最新的2023)
- 最新版本Photoshop CC2018软件安装教程(mac+windows全版本都有,包括2023