写在最后
还有一份JAVA核心知识点整理(PDF):JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…
-
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
-
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
2.如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁
3.当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而otifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
4.所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。
9. 描述一下synchronized和lock区别 ?
=============================
如下表
可以再多说下 synchronized的 加锁流程
由于HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低从而引入偏向锁。偏向锁在获取资源的时候会在锁对象头上记录当前线程ID,偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断锁对象头中线程ID是否为自己,如果是当前线程重入,直接进入同步操作,不需要额外的操作。默认在开启偏向锁和轻量锁的情况下,当线程进来时,首先会加上偏向锁,其实这里只是用一个状态来控制,会记录加锁的线程,如果是线程重入,则不会进行锁升级。获取偏向锁
流程:
1. 判断是否为可偏向状态–MarkWord中锁标志是否为‘01’,是否偏向锁是否为‘1’
2. 如果是可偏向状态,则查看线程ID是否为当前线程,如果是,则进入步骤 ‘V’,否则进入步骤‘III’
3. 通过CAS操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行‘V’;竞争失败,则执行‘IV’
4. CAS获取偏向锁失败表示有竞争。当达到safepoint时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块
5. 执行同步代码
轻量级锁是相对于重量级锁需要阻塞/唤醒涉及上下文切换而言,主要针对多个线程在不同时间请求同一把锁的场景。轻量级锁获取过程:
-
进行加锁操作时,jvm会判断是否已经是重量级锁,如果不是,则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象MarkWord复制到该锁记录中
-
复制成功之后,jvm使用CAS操作将对象头MarkWord更新为指向锁记录的指针,并将锁记录里的owner指针指向对象头的MarkWord。如果成功,则执行‘III’,否则执行‘IV’
-
更新成功,则当前线程持有该对象锁,并且对象MarkWord锁标志设置为‘00’,即表示此对象处于轻量级锁状态
-
更新失败,jvm先检查对象MarkWord是否指向当前线程栈帧中的锁记录,如果是则执行‘V’,否则执行‘VI’
-
表示锁重入;然后当前线程栈帧中增加一个锁记录第一部分(Displaced Mark Word) 为null,并指向Mark Word的锁对象,起到一个重入计数器的作用
-
表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,则升级为重量级锁
当有多个锁竞争轻量级锁则会升级为重量级锁,重量级锁正常会进入一个cxq的队列,在调用wait方法之后,则会进入一个waitSet的队列park等待,而当调用notify方法唤醒之后,则有可能进入EntryList。重量级锁加锁过程:
-
分配一个ObjectMonitor对象,把Mark Word锁标志置为‘10’,然后Mark Word存储指向ObjectMonitor对象的指针。ObjectMonitor对象有两个队列和一个指针,每个需要获取锁的线程都包装成ObjectWaiter对象
-
多个线程同时执行同一段同步代码时,ObjectWaiter先进入EntryList队列,当某个线程获取到对象的monitor以后进入Owner区域,并把monitor中的owner变量设置为当前线程同时monitor中的计数器count+1;
10.简单描述一下ABA问题?
===============
1.有两个线程同时去修改一个变量的值,比如线程1、线程2,都更新变量值,将变量值从A更新成B。
2.首先线程1、获取到CPU的时间片,线程2由于某些原因发生阻塞进行等待,此时线程1进行比较更新(CompareAndSwap),成功将变量的值从A更新成B。
3.更新完毕之后,恰好又有线程3进来想要把变量的值从B更新成A,线程3进行比较更新,成功将变量的值从B更新成A。 4. 线程2获取到CPU的时间片,然后进行比较更新,发现值是预期的A,然后又更新成了B。但是线程1并不知道,该值已经有了A->B->A这个过程,这也就是我们常说的ABA问题。
4.可以通过加版本号或者加时间戳解决,或者保证单向递增或者递减就不会存在此类问题。
11.实现一下DCL?
===========
12.实现一个阻塞队列(用Condition写生产者与消费者就)?
=================================
13.实现多个线程顺序打印abc?
=================
14.服务器CPU数量及线程池线程数量的关系?
=======================
首先确认业务是CPU密集型还是IO密集型的,
如果是CPU密集型的,那么就应该尽量少的线程数量,一般为CPU的核数+1;
如果是IO密集型:所以可多分配一点 cpu核数*2 也可以使用公式:CPU 核数 / (1 - 阻塞系数);其中阻塞系数 在 0.8 ~ 0.9 之间。
15.多线程之间是如何通信的?
===============
1、通过共享变量,变量需要volatile 修饰
2、使用wait()和notifyAll()方法,但是由于需要使用同一把锁,所以必须通知线程释放锁,被通知线程才能获取到锁,这样导致通知不及时。
3、使用CountDownLatch实现,通知线程到指定条件,调用countDownLatch.countDown(),被通知线程进行countDownLatch.await()。
4、使用Condition的await()和signalAll()方法。
16.synchronized关键字加在静态方法和实例方法的区别?
=================================
修饰静态方法,是对类进行加锁,如果该类中有methodA 和methodB都是被synchronized修饰的静态方法,此时有两个线程T1、T2分别调用methodA()和methodB(),则T2会阻塞等待直到T1执行完成之后才能执行。
修饰实例方法时,是对实例进行加锁,锁的是实例对象的对象头,如果调用同一个对象的两个不同的被synchronized修饰的实例方法时,看到的效果和上面的一样,如果调用不同对象的两个不同的被synchronized修饰的实例方法时,则不会阻塞。
17.countdownlatch的用法?
=====================
两种用法:
1、让主线程await,业务线程进行业务处理,处理完成时调用countdownLatch.countDown(),CountDownLatch实例化的时候需要根据业务去选择CountDownLatch的count;
2、让业务线程await,主线程处理完数据之后进行countdownLatch.countDown(),此时业务线程被唤醒,然后去主线程拿数据,或者执行自己的业务逻辑。
18.线程池问题:
=========
(1)Executor提供了几种线程池
1、newCachedThreadPool()(工作队列使用的是 SynchronousQueue)
创建一个线程池,如果线程池中的线程数量过大,它可以有效地回收多余的线程,如果线程数不足,那么它可 以创建新的线程。
不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处 理却是我们无法控制的。
优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并 不会重新创建新的线程,提高了线程的复用率。
作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确 定,是根据实际情况动态调整的。
2、newFixedThreadPool()(工作队列使用的是 LinkedBlockingQueue)
这种方式可以指定线程池中的线程数。如果满了后又来了新任务,此时只能排队等待。
优点:newFixedThreadPool 的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务 器达到最大的使用率,同时又可以保证即使流量突然增大也不会占用服务器过多的资源。
作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也 不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数
3、newScheduledThreadPool()
该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让 任务重复执行。该线程池中有以下两种延迟的方法。
scheduleAtFixedRate 不同的地方是任务的执行时间,如果间隔时间大于任务的执行时间,任务不受执行 时间的影响。如果间隔时间小于任务的执行时间,那么任务执行结束之后,会立马执行,至此间隔时间就会被 打乱。
scheduleWithFixedDelay 的间隔时间不会受任务执行时间长短的影响。
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。
4、newSingleThreadExecutor()
这是一个单线程池,至始至终都由一个线程来执行。
作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务 队列中,等待这一个线程空闲,当这个线程空闲了再按 FIFO 方式顺序执行任务队列中的任务。
5、
newSingleThreadScheduledExecutor()
只有一个线程,用来调度任务在指定时间执行。
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线 程池大小为 1,而上面的可以指定线程池的大小。
(2)线程池的参数
int corePoolSize,//线程池核心线程大小
int maximumPoolSize,//线程池最大线程数量
long keepAliveTime,//空闲线程存活时间
TimeUnit unit,//空闲线程存活时间单位,一共有七种静态属性(TimeUnit.DAYS天,TimeUnit.HOURS 小时,TimeUnit.MINUTES分钟,TimeUnit.SECONDS秒,TimeUnit.MILLISECONDS毫 秒,TimeUnit.MICROSECONDS微妙,TimeUnit.NANOSECONDS纳秒)
BlockingQueue workQueue,//工作队列
ThreadFactory threadFactory,//线程工厂,主要用来创建线程(默认的工厂方法是:
Executors.defaultThreadFactory()对线程进行安全检查并命名)
RejectedExecutionHandler handler//拒绝策略(默认是:
ThreadPoolExecutor.AbortPolicy不 执行并抛出异常)
(3)拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进 来,就会执行拒绝策略。
jdk中提供了4种拒绝策略:
①
ThreadPoolExecutor.CallerRunsPolicy:该策略下,在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任 务。
②
ThreadPoolExecutor.AbortPolicy:该策略下,直接丢弃任务,并抛出 RejectedExecutionException 异常。
③
ThreadPoolExecutor.DiscardPolicy:该策略下,直接丢弃任务,什么都不做。
④
ThreadPoolExecutor.DiscardOldestPolicy:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
除此之外,还可以根据应用场景需要来实现 RejectedExecutionHandler 接口自定义策略。
(4)任务放置的顺序过程
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。 了解这部分就相当于了解了线程池的核心运行机制。
首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行 线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝 该任务。其执行过程如下:
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队 列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
其执行流程如下图所示:
(5)任务结束后会不会回收线程
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
首先线程池内的线程都被包装成了一个个的
java.util.concurrent.ThreadPoolExecutor.Worker,然 后这个worker会马不停蹄的执行任务,执行完任务之后就会在while循环中去取任务,取到任务就继续执行,取 不到任务就跳出while循环(这个时候worker就不能再执行任务了)执行 processWorkerExit方法,这个方 法呢就是做清场处理,将当前woker线程从线程池中移除,并且判断是否是异常进入processWorkerExit方 法,如果是非异常情况,就对当前线程池状态(RUNNING,shutdown)和当前工作线程数和当前任务数做判断,是否要加入一个新的线程去完成最后的任务.
(6)未使用的线程池中的线程放在哪里
private final HashSet workers = new HashSet();
(7)线程池线程存在哪
private final HashSet workers = new HashSet();
19.Java多线程的几种状态及线程各个状态之间是如何切换的?
===============================
20.如何在方法栈中进行数据传递?
=================
通过方法参数传递;通过共享变量;如果在用一个线程中,还可以使用ThreadLocal进行传递.
21.描述一下ThreadLocal的底层实现形式及实现的数据结构?
==================================
Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null:
只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建它们。
除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量,防止出现内存泄漏。
22.Sychornized是否是公平锁?
=====================
不是公平锁
23.描述一下锁的四种状态及升级过程?
===================
以下是32位的对象头描述
synchronized锁的膨胀过程:
当线程访问同步代码块。首先查看当前锁状态是否是偏向锁(可偏向状态)
1、如果是偏向锁:
1.1、检查当前mark word中记录是否是当前线程id,如果是当前线程id,则获得偏向锁执行同步代码 块。
1.2、如果不是当前线程id,cas操作替换线程id,替换成功获得偏向锁(线程复用),替换失败锁撤销升 级轻量锁(同一类对象多次撤销升级达到阈值20,则批量重偏向,这个点可以稍微提一下,详见下面的注意)
2、升级轻量锁
升级轻量锁对于当前线程,分配栈帧锁记录lock_record(包含mark word和object-指向锁记录首地 址),对象头mark word复制到线程栈帧的锁记录 mark word存储的是无锁的hashcode(里面有重入次数 问题)。
3、重量级锁(纯理论可结合源码)
CAS自旋达到一定次数升级为重量级锁(多个线程同时竞争锁时)
存储在ObjectMonitor对象,里面有很多属性ContentionList、EntryList 、WaitSet、 owner。当一个线程尝试获取锁时,如果该锁已经被占用,则该线程封装成ObjectWaiter对象插到 ContentionList队列的对首,然后调用park挂起。该线程锁时方式会从ContentionList或EntryList挑 一个唤醒。线程获得锁后调用Object的wait方法,则会加入到WaitSet集合中(当前锁或膨胀为重量级锁)
注意:
1.偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活
2.偏向锁撤销是需要在safe_point,也就是安全点的时候进行,这个时候是stop the word的,所以说偏向 锁的撤销是开销很大的,如果明确了项目里的竞争情况比较多,那么关闭偏向锁可以减少一些偏向锁撤销的开销
3.以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作时 (这个时候进入轻量级锁),该计数器+1,当这个值达到重偏向阈值(默认20,也就是说前19次进行加锁的时候, 都是假的轻量级锁,当第20次加锁的时候,就会走批量冲偏向的逻辑)时,JVM就认为该class的偏向锁有问 题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch值。每次发生批量重偏向时,就将该值+1, 同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次 获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操 作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID
23. CAS的ABA问题怎么解决的?
====================
通过加版本号控制,只要有变更,就更新版本号
24. 描述一下AQS?
=============
状态变量state:
AQS中定义了一个状态变量state,它有以下两种使用方法:
(1)互斥锁
当AQS只实现为互斥锁的时候,每次只要原子更新state的值从0变为1成功了就获取了锁,可重入是通过不断把state原子更新加1实现的。
(2)互斥锁 + 共享锁
当AQS需要同时实现为互斥锁+共享锁的时候,低16位存储互斥锁的状态,高16位存储共享锁的状态,主要用于实现读写锁。
互斥锁是一种独占锁,每次只允许一个线程独占,且当一个线程独占时,其它线程将无法再获取互斥锁及共享锁,但是它自己可以获取共享锁。
共享锁同时允许多个线程占有,只要有一个线程占有了共享锁,所有线程(包括自己)都将无法再获取互斥锁,但是可以获取共享锁。
AQS队列
AQS中维护了一个队列,获取锁失败(非tryLock())的线程都将进入这个队列中排队,等待锁释放后唤醒下一个排队的线程(互斥锁模式下)。
condition队列
AQS中还有另一个非常重要的内部类ConditionObject,它实现了Condition接口,主要用于实现条件锁。
ConditionObject中也维护了一个队列,这个队列主要用于等待条件的成立,当条件成立时,其它线程将signal这个队列中的元素,将其移动到AQS的队列中,等待占有锁的线程释放锁后被唤醒。
Condition典型的运用场景是在BlockingQueue中的实现,当队列为空时,获取元素的线程阻塞在notEmpty条件上,一旦队列中添加了一个元素,将通知notEmpty条件,将其队列中的元素移动到AQS队列中等待被唤醒。
获取锁、释放锁的这些方法基本上都穿插在ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch的源码解析中
需要子类实现的方法
上面一起学习了AQS中几个重要的模板方法,下面我们再一起学习下几个需要子类实现的方法:
这几个方法为什么不直接定义成抽象方法呢?
因为子类只要实现这几个方法中的一部分就可以实现一个同步器了,所以不需要定义成抽象方法。
25.介绍一下volatile的功能?
===================
保证线程可见性
防止指令重排序
26.volatile的可见性和禁止指令重排序怎么实现的?
=============================
可见性:
volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次是用之前都从主内存刷新。本质也是通过内存屏障来实现可见性
写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。
最后
各位读者,由于本篇幅度过长,为了避免影响阅读体验,下面我就大概概括了整理了
/d08d3c93037da84db71e0bdb64b9e61a.png)
这几个方法为什么不直接定义成抽象方法呢?
因为子类只要实现这几个方法中的一部分就可以实现一个同步器了,所以不需要定义成抽象方法。
25.介绍一下volatile的功能?
===================
保证线程可见性
防止指令重排序
26.volatile的可见性和禁止指令重排序怎么实现的?
=============================
可见性:
volatile的功能就是被修饰的变量在被修改后可以立即同步到主内存,被修饰的变量在每次是用之前都从主内存刷新。本质也是通过内存屏障来实现可见性
写内存屏障(Store Memory Barrier)可以促使处理器将当前store buffer(存储缓存)的值写回主存。
最后
各位读者,由于本篇幅度过长,为了避免影响阅读体验,下面我就大概概括了整理了
[外链图片转存中…(img-aH4gfaTb-1715787853215)]
[外链图片转存中…(img-AKioQAMk-1715787853215)]
[外链图片转存中…(img-sNRaxqCk-1715787853215)]
[外链图片转存中…(img-DuNBaTkg-1715787853216)]