Java面试指导

我们是做就业服务的工作室,没有任何培训机构性质!!
主做Java、python、c++,前端vue,react等,
全国各地,简历包装,投递邀约,视频面试,技术面试包通过,离职背调等,
通过正常上班,不拿offer不收费,
不要浪费投递简历的机会和面试机会,
如果已经在职的话,并且不满意目前薪资也可以联系我们。关注公众号回复“就业”即可。

image-20241203185525611

或者添加微信咨询:
添加微信

Java并发

线程的⽣命周期?线程有⼏种状态

线程通常有五种状态,创建,就绪,运⾏、阻塞和死亡状态:

  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
  5. 死亡状态(Dead):线程执⾏完了或者因异常退出了run⽅法,该线程结束⽣命周期。

阻塞的情况⼜分为三种:

等待阻塞:运⾏的线程执⾏wait⽅法,该线程会释放占⽤的所有资源,JVM会把该线程放⼊“等待 池”中。进⼊这个状态后,是不能⾃动唤醒的,必须依靠其他线程调⽤notify或notifyAll⽅法才能被 唤醒,wait是object类的⽅法

同步阻塞:运⾏的线程在获取对象的同步锁时,若该同步锁被别的线程占⽤,则JVM会把该线程放 ⼊“锁池”中。

其他阻塞:运⾏的线程执⾏sleep或join⽅法,或者发出了I/O请求时,JVM会把该线程置为阻塞状 态。当sleep状态超时、join等待线程终⽌或者超时、或者I/O处理完毕时,线程重新转⼊就绪状 态。sleep是Thread类的⽅法

sleep()、wait()、join()、yield()之间的的区别

锁池:所有需要竞争同步锁的线程都会放在锁池当中,⽐如当前对象的锁已经被其中⼀个线程得到,则 其他线程需要在这个锁池进⾏等待,当前⾯的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线 程得到后会进⼊就绪队列进⾏等待cpu资源分配。

等待池:当我们调⽤wait()⽅法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只 有调⽤了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出⼀ 个线程放到锁池,⽽notifyAll()是将等待池的所有线程放到锁池当中

  1. sleep 是 Thread 类的静态本地⽅法,wait 则是 Object 类的本地⽅法。

  2. sleep⽅法不会释放lock,但是wait会释放,⽽且会加⼊到等待队列中。

    1
    2
    3
    4
    5
    6
    7
    sleep就是把cpu的执⾏资格和执⾏权释放出去,不再运⾏此线程,
    当定时时间结束再取回cpu资源,参与cpu的调度,
    获取到cpu资源后就可以继续运⾏了。⽽如果sleep时该线程有锁,
    那么sleep不会释放这个锁,⽽是把锁带着进⼊了冻结状态,
    也就是说其他需要这个锁的线程根本不可能获取到这个锁。
    也就是说⽆法执⾏程序。如果在睡眠期间其他线程调⽤了这个线程的interrupt⽅法,
    那么这个线程也会抛出interruptexception异常返回,这点和wait是⼀样的。
  3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

  4. sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。

  5. sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信。

  6. sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。

  7. yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进⾏线程调度还会让这个线程获取到执⾏权继续执⾏

  8. join()执⾏后线程进⼊阻塞状态,例如在线程B中调⽤线程A的join(),那线程B会进⼊到阻塞队 列,直到线程A结束或中断线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("22222222");
}
});
t1.start();
t1.join();// 这⾏代码必须要等t1全部执⾏完毕,才会执⾏
System.out.println("1111");
}

22222222
1111

对线程安全的理解

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问,当多个线程访问⼀个对象时, 如果不⽤进⾏额外的同步控制或其他的协调操作,调⽤这个对象的⾏为都可以获得正确的结果,我们就 说这个对象是线程安全的。

是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是⽤户分 配的空间。堆在操作系统对进程初始化的时候分配,运⾏过程中也可以向系统要额外的堆,但是⽤完了 要还给操作系统,要不然就是内存泄漏。在Java中,堆是Java虚拟机所管理的内存中最⼤的⼀块,是所 有线程共享的⼀块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯⼀⽬的就是存放对象实 例,⼏乎所有的对象实例以及数组都在这⾥分配内存。

是每个线程独有的,保存其运⾏状态和局部⾃动变量的。栈在线程开始的时候初始化,每个线程的栈 互相独⽴,因此,栈是线程安全的。操作系统在切换线程的时候会⾃动切换栈。栈空间不需要在⾼级语 ⾔⾥⾯显式的分配和释放。

⽬前主流操作系统都是多任务的,即多个进程同时运⾏。为了保证安全,每个进程只能访问分配给⾃⼰ 的内存空间,⽽不能访问别的进程的,这是由操作系统保障的。

在每个进程的内存空间中都会有⼀块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以 访问到该区域,这就是造成问题的潜在原因。

Thread和Runable的区别

Thread和Runnable的实质是继承关系,没有可⽐性。⽆论使⽤Runnable还是Thread,都会new Thread,然后执⾏run⽅法。⽤法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简 单的执⾏⼀个任务,那就实现runnable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 //会卖出多⼀倍的票
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub

new MyThread().start();
new MyThread().start();
}
static class MyThread extends Thread{
private int ticket = 5;
public void run(){
while(true){
System.out.println("Thread ticket = " + ticket--);
if(ticket < 0){
break;
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//正常卖出
public class Test2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread2 mt=new MyThread2();
new Thread(mt).start();
new Thread(mt).start();
}
static class MyThread2 implements Runnable{
private int ticket = 5;
public void run(){
while(true){
System.out.println("Runnable ticket = " + ticket--);
if(ticket < 0){
break;
}
}
}
}
}

原因是:MyThread创建了两个实例,⾃然会卖出两倍,属于⽤法错误

对守护线程的理解

守护线程:为所有⾮守护线程提供服务的线程;任何⼀个守护线程都是整个JVM中所有⾮守护线程的保姆;

守护线程类似于整个进程的⼀个默默⽆闻的⼩喽喽;它的⽣死⽆关重要,它却依赖整个进程⽽运⾏;哪 天其他线程结束了,没有要执⾏的了,程序就结束了,理都没理守护线程,就把它中断了;

注意: 由于守护线程的终⽌是⾃身⽆法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因 为它不靠谱;

守护线程的作⽤是什么? 举例, GC垃圾回收线程:就是⼀个经典的守护线程,当我们的程序中不再有任何运⾏的Thread,程序就 不会再产⽣垃圾,垃圾回收器也就⽆事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线 程会⾃动离开。它始终在低级别的状态中运⾏,⽤于实时监控和管理系统中的可回收资源。

应⽤场景:(1)来为其它线程提供服务⽀持的情况;(2) 或者在任何情况下,程序结束时,这个线程 必须正常且⽴刻关闭,就可以作为守护线程来使⽤;反之,如果⼀个正在执⾏某个操作的线程必须要正 确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,⽽是⽤户线程。通常都是 些关键的事务,⽐⽅说,数据库录⼊或者更新,这些操作都是不能中断的。

thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出⼀个IllegalThreadStateException 异常。你不能把正在运⾏的常规线程设置为守护线程。

在Daemon线程中产⽣的新线程也是Daemon的。

守护线程不能⽤于去访问固有资源,⽐如读写操作或者计算逻辑。因为它会在任何时候甚⾄在⼀个操作 的中间发⽣中断。

Java⾃带的多线程框架,⽐如ExecutorService,会将守护线程转换为⽤户线程,所以如果要使⽤后台 线程就不能⽤Java的线程池。

ThreadLocal的底层原理

  1. ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部, 该线程可以在任意时刻、任意⽅法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
  3. 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要 把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过 强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿ 动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象
  4. ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅ 法之间进⾏传递,线程之间不共享同⼀个连接)

并发、并⾏、串⾏之间的区别

  1. 串⾏在时间上不可能发⽣重叠,前⼀个任务没搞定,下⼀个任务就只能等着
  2. 并⾏在时间上是重叠的,两个任务在同⼀时刻互不⼲扰的同时执⾏。
  3. 并发允许两个任务彼此⼲扰。统⼀时间点、只有⼀个任务运⾏,交替执⾏

并发的三⼤特性

原子性

原⼦性是指在⼀个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执⾏完成,要 不都不执⾏。就好⽐转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元, 往账户B加上1000元。2个操作必须全部完成。

1
2
3
4
private long count = 0;
public void calc() {
count++;
}
  • 1:将 count 从主存读到⼯作内存中的副本中
  • 2:+1的运算
  • 3:将结果写⼊⼯作内存
  • 4:将⼯作内存的值刷回主存(什么时候刷⼊由操作系统决定,不确定的)

那程序中原⼦性指的是最⼩的操作单元,⽐如⾃增操作,它本身其实并不是原⼦性操作,分了3步的, 包括读取变量的原始值、进⾏加1操作、写⼊⼯作内存。所以在多线程中,有可能⼀个线程还没⾃增完, 可能才执⾏到第⼆部,另⼀个线程就已经读取了值,导致结果错误。那如果我们能保证⾃增操作是⼀个 原⼦性的操作,那么就能保证其他线程读取到的⼀定是⾃增后的数据。

关键字:synchronized

可见性

当多个线程访问同⼀个变量时,⼀个线程修改了这个变量的值,其他线程能够⽴即看得到修改的值。

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2⼜使⽤了i,那么这个i值肯定还 是之前的,线程1对变量的修改线程没看到这就是可⻅性问题。

1
2
3
4
5
6
7
 //线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;

如果线程2改变了stop的值,线程1⼀定会停⽌吗?不⼀定。当线程2更改了stop变量的值之后,但是还 没来得及写⼊主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因 此还会⼀直循环下去。

关键字:volatile、synchronized、final

有序性

虚拟机在进⾏代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不⼀定会按 照我们写的代码的顺序来执⾏,有可能将他们重排序。实际上,对于有些代码进⾏重排序之后,虽然对 变量的值没有造成影响,但有可能会出现线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
 int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}

public void multiply() {
if (flag) { //3
int ret = a * a; //4
}

write⽅法⾥的1和2做了重排序,线程1先对flag赋值为true,随后执⾏到线程2,ret直接计算出结果,再 到线程1,这时候a才赋值为2,很明显迟了⼀步

关键字:volatile、synchronized

volatile本身就包含了禁⽌指令重排序的语义,⽽synchronized关键字是由“⼀个变量在同⼀时刻只允许 ⼀条线程对其进⾏lock操作”这条规则明确的。

synchronized关键字同时满⾜以上三种特性,但是volatile关键字不满⾜原⼦性。

在某些情况下,volatile的同步机制的性能确实要优于锁(使⽤synchronized关键字或 java.util.concurrent包⾥⾯的锁),因为volatile的总开销要⽐锁低。

我们判断使⽤volatile还是加锁的唯⼀依据就是volatile的语义能否满⾜使⽤的场景(原⼦性)

Java死锁如何避免?

造成死锁的⼏个原因:

  1. 一个资源每次只能被一个线程使用
  2. 一个线程在阻塞等待某个资源时,不释放已占有资源
  3. 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
  4. 若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
  2. 要注意加锁时限,可以针对所设置一个超时时间
  3. 要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

如何理解volatile关键字

保证被volatile修饰的共享变量对所有线程总是可⻅的,也就是当⼀个线程修改了⼀个被volatile修饰共 享变量的值,新值总是可以被其他线程⽴即得知。 如果线程2改变了stop的值,线程1⼀定会停⽌吗?不⼀定。当线程2更改了stop变量的值之后,但是还 没来得及写⼊主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会⼀直循环下去。

1
2
3
4
5
6
7
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop=true;

禁⽌指令重排序优化

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 0;
bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}

public void multiply() {
if (flag) { //3
int ret = a * a; //4
}
}

write⽅法⾥的1和2做了重排序,线程1先对flag赋值为true,随后执⾏到线程2,ret直接计算出结果,再 到线程1,这时候a才赋值为2,很明显迟了⼀步。但是⽤volatile修饰之后就变得不⼀样了:

  1. 使⽤volatile关键字会强制将修改的值⽴即写⼊主存;
  2. 使⽤volatile关键字的话,当线程2进⾏修改时,会导致线程1的⼯作内存中缓存变量stop的缓存⾏⽆ 效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存⾏⽆效);
  3. 由于线程1的⼯作内存中缓存变量stop的缓存⾏⽆效,所以线程1再次读取变量stop的值时会去主存 读取。

inc++; 其实是两个步骤,先加加,然后再赋值。不是原⼦性操作,所以volatile不能保证线程安全。

为什么用线程池?解释下线程池参数?

  1. 降低资源消耗;提⾼线程利⽤率,降低创建和销毁线程的消耗。

  2. 提⾼响应速度;任务来了,直接有线程可⽤可执⾏,⽽不是先创建线程,再执⾏。

  3. 提⾼线程的可管理性;线程是稀缺资源,使⽤线程池可以统⼀分配调优监控。

    • corePoolSize 代表核⼼线程数,也就是正常情况下创建⼯作的线程数,这些线程创建后并不会 消除,⽽是⼀种常驻线程

    • maxinumPoolSize 代表的是最⼤线程数,它与核⼼线程数相对应,表示最⼤允许被创建的线程数,⽐如当前任务较多,将核⼼线程数都⽤完了,还⽆法满⾜需求时,此时就会创建新的线程,但 是线程池内线程总数不会超过最⼤线程数

    • keepAliveTime 、 unit 表示超出核⼼线程数之外的线程的空闲存活时间,也就是核⼼线程不 会消除,但是超出核⼼线程数的部分线程如果空闲⼀定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间

    • workQueue ⽤来存放待执⾏的任务,假设我们现在核⼼线程都已被使⽤,还有任务进来则全部 放⼊队列,直到整个队列被放满但任务还再持续进⼊则会开始创建新的线程

    • ThreadFactory 实际上是⼀个线程⼯⼚,⽤来⽣产线程执⾏任务。我们可以选择使⽤默认的创 建⼯⼚,产⽣的线程都在同⼀个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选 择⾃定义线程⼯⼚,⼀般我们会根据业务来制定不同的线程⼯⼚

    • Handler 任务拒绝策略,有两种情况,第⼀种是当我们调⽤ shutdown 等⽅法关闭线程池后, 这时候即使线程池内部还有没执⾏完的任务正在执⾏,但是由于线程池已经关闭,我们再继续想线 程池提交任务就会遭到拒绝。另⼀种情况就是当达到最⼤线程数,线程池已经没有能⼒继续处理新 提交的任务时,这是也就拒绝

线程池的底层⼯作原理

  1. 线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
  2. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建 新的线程来处理被添加的任务。
  3. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊ 缓冲队列。
  4. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数 量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
  5. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等 于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  6. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被 终⽌。这样,线程池可以动态的调整池中的线程数

线程池中阻塞队列的作⽤?为什么是先添加列队⽽不是先创建最 ⼤线程?

  1. ⼀般的队列只能保证作为⼀个有限⻓度的缓冲区,如果超出了缓冲⻓度,就⽆法保留当前的任务了, 阻塞队列通过阻塞可以保留住当前想要继续⼊队的任务。

    阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进⼊wait状态,释放cpu资 源。

    阻塞队列⾃带阻塞和唤醒的功能,不需要额外处理,⽆任务执⾏时,线程池利⽤阻塞队列的take⽅法挂 起,从⽽维持核⼼线程的存活、不⾄于⼀直占⽤cpu资源

  2. 在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

​ 就好⽐⼀个企业⾥⾯有10个(core)正式⼯的名额,最多招10个正式⼯,要是任务超过正式⼯⼈数 (task > core)的情况下,⼯⼚领导(线程池)不是⾸ 先扩招⼯⼈,还是这10⼈,但是任务可以稍微积 压⼀下,即先放到队列去(代价低)。10个正式⼯慢慢⼲,迟早会⼲完的,要是任务还在继续增加,超 过正 式⼯的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时⼯)要是正式⼯加上外包还 是不能完成任务,那新来的任务就会被领导拒绝了(线程 池的拒绝策略)。

线程池中线程复⽤原理

线程池将线程和任务进⾏解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的⼀个 线程必须对应⼀个任务的限制。

在线程池中,同⼀个线程可以从阻塞队列中不断获取新任务来执⾏,其核⼼原理在于线程池对 Thread 进⾏了封装,并不是每次执⾏任务都会调⽤ Thread.start() 来创建新线程,⽽是让每个线程去执⾏⼀ 个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执⾏,如果有则直接执⾏,也就是调⽤ 任务中的 run ⽅法,将 run ⽅法当成⼀个普通的⽅法执⾏,通过这种⽅式只使⽤固定的线程就将所有任 务的 run ⽅法串联起来。

ReentrantLock中的公平锁和⾮公平锁的底层实现

⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使 ⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队, 则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。

不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线 程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。

ReentrantLock中tryLock()和lock()⽅法的区别

  1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回 true,没有加到则返回false
  2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤ CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对 CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。

对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中 排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通 过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release() ⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤 醒,直到没有空闲许可。

Sychronized的偏向锁、轻量级锁、重量级锁

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就 可以直接获取到了
  2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个 线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻 量级锁底层是通过⾃旋来实现的,并不会阻塞线程
  3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒 这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标 记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运 ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

Sychronized和ReentrantLock的区别

  1. sychronized是⼀个关键字,ReentrantLock是⼀个类
  2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
  3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
  4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态
  6. sychronized底层有⼀个锁升级的过程

谈谈你对AQS的理解,AQS如何实现可重⼊锁?

  1. AQS是⼀个JAVA线程同步的框架。是JDK中很多锁⼯具的核⼼实现框架。
  2. 在AQS中,维护了⼀个信号量state和⼀个线程组成的双向链表队列。其中,这个线程队列,就是⽤ 来给线程排队的,⽽state就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下, 有不⽤的意义。
  3. 在可重⼊锁这个场景下,state就⽤来表示加锁的次数。0标识⽆锁,每加⼀次锁,state就加1。释 放锁state就减1。
公告
面试指导,100%帮您拿Offer,不拿Offer不收费!!!