一、基础知识
进程和线程有什么区别?
- 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
- 线程与进程相似,但线程是一个比进程更小的执行单位。
一个进程在其执行的过程中可以产生多个线程。 与进程不同的是同类的多个线程共 享进程的堆和方法区资源 , 但每个线程有自己的程序计数器、虚拟机栈和本地方法栈 ,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
什么是线程安全
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
多线程向解决什么?
访问共享变量时,保证临界区代码的【原子性】,共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题。
Java中实现多线程有几种方法?
创建线程的常用三种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口( JDK1.5>= )
- 线程池方式创建
实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的 方法返回值 ,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。
Thread 类中的start() 和 run() 方法有什么区别?
1、start()内部调用了run()方法 2、调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
interrupted 和 isInterrupted方法的区别?
1、interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。 2、调用Thread.interrupt()来中断一个线程就会设置中断标识为true
- 当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。
- 而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。两阶段终止模式
3、简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零
如何停止一个正在运行的线程?
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。退出标志要用volatile修饰 2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 3、使用interrupt方法中断线程。interrupt只是给线程设置一个中断标志,线程仍会继续运行。参考两阶段终止模式,并能用犹豫(Balking)模式进行改进
@Slf4j
class TwoPhaseTerminations {
private Thread monitor;
private volatile boolean stop;
private boolean start = false;
public void start() {
synchronized (this) {
if (start) {
return;
}
start = true;
}
monitor = new Thread(() -> {
while(true) {
Thread currentThread = Thread.currentThread();
if (stop) {
log.debug("被打断...");
break;
}
try {
Thread.sleep(1000);
log.debug("保存监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"监控线程");
monitor.start();
}
public void stop() {
stop = true;
}
}
线程之间的状态转换?TODO
操作系统层面 - 五种
Java API 层面 - 六种
sleep() 方法和 wait() 方法区别和共同点?
- 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
- 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- 调用 start() 方法,会启动一个线程并使线程进入了
就绪状态 ,当分配到时间片后就可以开始执行run()方法中的内容了 - 直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
Thread类中的yield方法有什么作用?
- Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。
- 它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU。
- 执行yield()的线程有可能在进入到暂停状态后马上又被执行。
notify()和notifyAll()有什么区别?使用时有什么注意事项?
- notify可能会导致死锁,而notifyAll则不会。
- 使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。
注意事项
1、wait() 应配合while循环使用,防止虚假唤醒 。 2、notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中。
notify()为什么会导致死锁?
简单的说,notify()只唤醒一个正在等待的线程,当该线程执行完以后施放该对象的锁,而没有再次执行notify()方法,则其它正在等待的线程则一直处于等待状态,不会被唤醒而进入该对象的锁的竞争池,就会发生死锁。
为什么wait, notify 和 notifyAll这些方法不在thread类里面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线 程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线 程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所 以把他们定义在Object类中因为锁属于对象。
为什么wait和notify方法要在同步块中调用?
- wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
- 在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
- 调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:“特殊状态已经被设置”。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
有三个线程T1,T2,T3,如何保证顺序执行?
参考:https://blog.csdn.net/qq_36389060/article/details/121743099
Park & Unpark TODO
Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面:
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
二、synchronized
说一说自己对于 synchronized 关键字的了解?
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
在 Java 早期版本中,synchronized属于重量级锁。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
Synchronized的作用有哪些?
- 原子性:确保线程互斥的访问同步代码;
- 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “
对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值 ” 来保证的; - 有序性:有效解决重排序问题,即 “
一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作 ”。
说一下 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 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调
说说synchronized怎么使用?
- 修饰普通方法:作用于
当前对象实例 ,进入同步代码前要获得当前对象实例的锁 - 修饰静态方法:作用于
当前类 ,进入同步代码前要获得当前类对象的锁,synchronized 关键字加到static 静态方法和 synchronized(xxx.class)代码块上 都是给 Class 类上锁 - 修饰代码块:
指定加锁对象 ,对给定对象加锁,进入同步代码库前要获得给定对象的锁
特别注意:
如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁 - 尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
synchronized 锁升级的原理是什么?
synchronized 锁升级原理(锁膨胀 ):偏向锁 ==》 轻量级锁 ==》 重量级锁
- 在锁对象的对象头里面有一个 ThreadID 字段,在第一次访问的时候 ThreadID 为空,
jvm 让其持有偏向锁,并将 ThreadID 设置为其线程 id ,再次进入的时候会先判断ThreadID 是否与其线程 id 一致
- 如果一致则可以直接使用此对象
- 如果不一致,则
升级偏向锁为轻量级锁 ,通过 自旋循环一定次数来获取锁 ,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是 为了减低了锁带来的性能消耗 。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
JVM对synchronized的优化有哪些?
锁膨胀:是锁升级的流程,膨胀方向是:无锁 ==》 偏向锁 ==》 轻量级锁 ==》 重量级锁,并且膨胀方向不可逆。
synchronized原理 之 重量级锁 Monitor
- Monitor(重量级锁)被翻译为监视器或管程
- 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,
该对象头的Mark Word 中就被设置指向 Monitor 对象的指针 - 刚开始 Monitor 中 Owner 为 null。
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner。
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED。
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的。
图中 WaitSet 中的线程是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析。
synchronized优化 之 轻量级锁(synchronized是否是可重入锁)?
-
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。 -
轻量级锁对使用者是透明的,即语法仍然是 synchronized 。 -
每个线程的栈帧都会包含一个锁记录(Lock Record)对象的结构,内部可以 存储锁定对象的 Mark Word -
当一个线程要获取锁时,会让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
-
如果 cas 替换成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下 -
如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入
锁膨胀 过程 - 如果是自己执行了 synchronized
锁重入( 同一个线程给同一个对象加了多次锁 ) ,那么再添加一条 Lock Record 作为重入的计数 ,如下图: -
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一 -
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
synchronized优化 之 偏向锁
- 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
- Java 6 中引入了偏向锁来做进一步优化
- 偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,第一次交换时使用 CAS 将线程 ID 设置到对象的 Mark Word 头,如果交换成功,则偏向锁获取成功,记录锁状态为偏向锁,
以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
自旋锁与自适应自旋锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
锁消除
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
- o对象不会被共享,synchronized没有任何意义,那么JIT即时编译器会把synchronized优化掉,实际执行时是没有对o对象加锁的
锁粗化
在遇到一连串地对同一锁不断进行请求和释放的操作时,把所有的锁操作整合成锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化。
哪些情况会撤销偏向状态?
情况一:调用对象hashCode()
- 调用 hashCode() 会禁用 偏向锁,这是因为 markword 一共64位,使用偏向锁,Thread ID占54位,那么就没地方存放31位hashcode
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
情况二:其它线程加锁对象
- 如果存在其他线程加锁,那么偏向锁将膨胀为轻量级锁。
情况三:调用 wait/notify
- 这个很好理解,因为wait/notify只有重量级锁才有
什么是批量重偏向?
- 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
synchronized 锁能降级吗?
可以的。
具体的触发时机:在全局安全点(safepoint)中 ,执行清理任务的时候会触发尝试降级锁。当锁降级时,主要进行了以下操作:
- 恢复锁对象的 markword 对象头;
- 重置 ObjectMonitor,然后将该 ObjectMonitor 放入全局空闲列表,等待后续使用。
三、volatile
谈谈volatile的使用及其原理?
volatile可以用来修饰成员变量和静态成员变量。它能保证可见性和有序性,但无法保证原子性。
volatile的两层语义:
保证变量对所有线程的可见性 :当volatile变量被修改,新值对所有线程会立即更新。或者理解为多线程环境下使用volatile修饰的变量的值一定是最新的。- jdk1.5以后volatile
完全避免了指令重排优化 ,实现了 有序性 。
volatile原理之有序性(单例模式)
以单例模式说明,在并发环境下的单例实现方式,我们通常可以采用双重检查加锁(DCL)的方式来实现。其源码如下:
public class Singleton {
public static volatile Singleton singleton;
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么要在变量singleton之间加上volatile关键字。要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:
- 分配内存空间。
- 初始化对象。
- 将内存空间的地址赋值给对应的引用。
但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
- 分配内存空间。
- 将内存空间的地址赋值给对应的引用。
- 初始化对象
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障。
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。 - 对 volatile 变量的读指令前会加入读屏障。
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。
volatile原理之可见性
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个 高速缓存区 ——线程工作内存。
修改volatile变量时会强制将修改后的值刷新的主内存中。
四、ReentrantLock - 可重入锁
ReentrantLock 了解吗?
- ReetrantLock是一个可重入的独占锁,主要有两个特性,一个是支持公平锁和非公平锁,一个是可重入。
- ReetrantLock实现依赖于AQS(AbstractQueuedSynchronizer)。
- ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。
说说ReentrantLock 和 synchronized 的区别?
相对于 synchronized 它具备如下特点
可中断 可以设置超时时间 可以设置为公平锁 支持多个条件变量
与 synchronized 一样,都支持可重入(重复加锁)
五、各种对比
synchronized 和 volatile 的区别是什么?
- volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile 仅能使用在变量级别;synchronized 则可以使用在 变量、方法、和类级别的。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
synchronized 和 Lock 有什么区别?
- synchronized 可以给类、方法、代码块加锁;而 lock
只能给代码块加锁 。 - synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Java中synchronized 和 ReentrantLock 有什么不同?
相似点
它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。
区别
1、Synchronized是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。 2、Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。
- 在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。
- 如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
3、ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
-
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过 lock.lockInterruptibly() 来实现这个机制。 try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
-
可以设置为公平锁。 ReentrantLock lock = new ReentrantLock(true);
-
支持多个条件变量,synchronized 中也有条件变量,就是我们讲原理时那个 waitSet ,当条件不满足时进入 waitSet 等待,ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。
六、CAS
什么是 CAS?有什么特点?
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现 比较并交换 的效果。
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
- CAS 体现的是
无锁并发、无阻塞并发
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
为什么无锁效率高?
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,
发生上下文切换,进入阻塞 。 - 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,
但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换 。
CAS有什么缺陷(带来的问题)?
ABA 问题
并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可 能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
可以通过 AtomicStampedReference 解决ABA问题,它,一个带有标记的原子引用类,通过控制变量值 的版本来保证CAS的正确性。
循环时间长开销
CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原 子性的。
可以通过这两个方式解决这个问题:
- 使用互斥锁来保证原子性;
将多个变量封装成对象,通过AtomicReference来保证原子性。
只能保证一个变量的原子操作
说说 Atomic 原子类?TODO
七、ThreadLocal
ThreadLocal是什么?
ThreadLocal,即 线程本地变量 。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量 , 从而起到线程隔离的作用,避免了线程安全问题。
static ThreadLocal<String> localVariable = new ThreadLocal<>();
ThreadLocal的实现原理?
- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的
ThreadLocalMap 。 - ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身。
- 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
- 当设置值是,需要首先获得当前线程对象Thread;
- 然后取出当前线程对象的成员变量ThreadLocalMap;
- 如果ThreadLocalMap存在,那么进行KEY/VALUE设置,KEY就是ThreadLocal;
- 如果ThreadLocalMap没有,那么创建一个;
知道ThreadLocal 内存泄露问题吗?
- 弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。
- 弱引用比较容易被回收。因此,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了。
- 但是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:
ThreadLocalMap的key没了,value还在,这就会「造成了内存泄漏问题」。 - 如何「解决内存泄漏问题」?使用完ThreadLocal后,及时调用remove()方法释放内存间。
八、线程池
为什么要用线程池?
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
- 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的状态?
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
为什么不用两个int来分别存储线程状态和线程个数呢?
这些信息存储在一个 原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
private static int ctlOf(int rs, int wc) { return rs | wc; }
说下线程池核心参数?
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂
- handler 拒绝策略
说明:救急线程 = maximumPoolSize - corePoolSize ,每个线程是用到了才创建,节省资源,懒汉式,当阻塞队列达到容量时,还有任务加入就会创建救急线程,它有生存时间。
线程池有哪些拒绝策略?
- AbortPolicy : 线程任务丢弃报错。让调用者抛出 RejectedExecutionException 异常,这是默认策略。
- DiscardPolicy : 线程任务直接丢弃不报错。
- DiscardOldestPolicy : 将workQueue队首任务丢弃,将最新线程任务重新加入队列执行。
- CallerRunsPolicy :线程池之外的线程直接调用run方法执行。让调用者运行任务。
还知道哪些拒绝策略?TODO
说说线程池的执行流程?
- 线程池执行execute/submit方法向线程池添加任务,当任务小于核心线程数corePoolSize,线程池中可以创建新的线程。
- 当任务大于核心线程数corePoolSize,就向阻塞队列添加任务。
- 如果阻塞队列已满,需要通过比较参数maximumPoolSize,在线程池创建新的线程,当线程数量大于maximumPoolSize,说明当前设置线程池中线程已经处理不了了,就会执行饱和策略。
执行execute()方法和submit()方法的区别是什么呢?
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
常用的线程池有哪些?
newScheduledThreadPool :创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。newFixedThreadPool :创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool :创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
- 全部都是救急线程(60s 后可以回收)
- 救急线程可以无限创建
- 队列采用了 SynchronousQueue 实现特点是,它没有容量,但是当一个线程想放任务,但没有线程来取,放任务的线程是放不进去任务的(一手交钱、一手交货)
newSingleThreadExecutor :创建一个单线程的线程池,此 线程池保证所有任务的执行顺序按照任务的提交顺序执行。 public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
使用场景:希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。 那么我们自己定义一个线程不行吗,是有区别的
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,
而线程池还会新建一个线程,保证池的正常工作 - Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
- FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
- Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
- 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
线程池常用的阻塞队列有哪些?
LinkedBlockingQueue :对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列 。由于 FixedThreadPool 线 程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。SynchronousQueue :CachedThreadPool 是线程数可以无限扩展,所以CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。DelayedWorkQueue :ScheduledThreadPool 和SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。DelayedWorkQueue延迟队列正好可以把任务按时间进行排序,方便任务的执行。
源码中线程池是怎么复用线程的?
源码中ThreadPoolExecutor中有个内置对象Worker,每个worker都是一个线程,worker线程数量和参数有关,每个worker线程池的线程复用就是通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中不停地取任务,并直接调用 Runnable 的 run 方法来执行任务,这样就保证了每个线程都始终在一个循环中,反复获取任务,然后执行任务,从而实现了线程的复用。
Executor、ExecutorService以及Executors之间的区别?
-
Executor, ExecutorService, 和 Executors 最主要的区别是 Executor 是一个抽象层面的核心接口(大致代码如下)。 public interface Executor {
void execute(Runnable command);
}
-
ExecutorService 接口 对 Executor 接口进行了扩展,提供了返回 Future 对象,终止,关闭线程池等方法。当调用 shutDown 方法时,线程池会停止接受新的任务,但会完成正在 pending 中的任务。 -
Executors 是一个工具类,类似于 Collections。提供工厂方法来创建不同类型的线程池,比如 FixedThreadPool 或 CachedThreadPool。
shutdown() VS shutdownNow()
- shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
- shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线 程,所以无法响应中断的任务可能永远无法终。
isTerminated() VS isShutdown()
- isShutDown 当调用 shutdown() 方法后返回为 true。
- isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
知道 Fork/Join 线程池吗?
- Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
- 所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
- Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
- Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
九、AQS TODO
十、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
- 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
- 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁的实现方式
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。 2、java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。(吃点亏,多尝试几次,这就是乐观锁)
补充知识 方便查询
Java对象头
- 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。 - 也就是说
JAVA对象 = 对象头 + 实例数据 + 对齐填充 。 - 其中,对象头由两部分组成,一部分用于
存储自身的运行时数据 ,称之为 Mark Word,另外一部分是类型指针,即 对象指向它的类元数据的指针。
对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)
- 在32位系统中,Mark Word = 4 bytes = 32 bits,对象头 = 8 bytes = 64 bits;
- 在64位系统中,Mark Word = 8 bytes = 64 bits ,对象头 = 16 bytes = 128bits;
bytes 是字节,bits 是位。所以说,在32位JVM虚拟机系统中,Mark Word部分,占了4个字节,Klass Word部分也占了4个字节,所以,对象头大小为8个字节。在64位JVM虚拟机系统中,Mark Word部分,占了8个字节,Klass Word部分也占了8个字节,所以,对象头大小为16个字节。
|-----------------------------------------------------------------------------------------------------------------|
| Object Header(128bits) |
|-----------------------------------------------------------------------------------------------------------------|
| Mark Word(64bits) | Klass Word(64bits) | State |
|-----------------------------------------------------------------------------------------------------------------|
| unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:0| 01 | OOP to metadata object | Nomal |
|-----------------------------------------------------------------------------------------------------------------|
| thread:54| epoch:2 |unused:1|age:4|biase_lock:1| 01 | OOP to metadata object | Biased |
|-----------------------------------------------------------------------------------------------------------------|
| ptr_to_lock_record:62 | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:62 | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
| | 11 | OOP to metadata object | Marked for GC |
|-----------------------------------------------------------------------------------------------------------------|
- lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
biased_lock | lock | 状态 |
---|
0 | 01 | 无锁 | 1 | 01 | 偏向锁 | 0 | 00 | 轻量级锁 | 0 | 10 | 重量级锁 | 0 | 11 | GC标记 |
- age:4位的Java对象年龄。
- identity_hashcode:25位的对象标识Hash码
- thread:持有偏向锁的线程ID。
- epoch:偏向时间戳。
- ptr_to_lock_record:指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:指向管程Monitor的指针。
synchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步。而ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。 所以,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,这样,即使在遍历map时,如果其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。
|