自学内容网 自学内容网

【面试】java多线程

1.创建线程是几种方式

方式一:继承Thread类,覆写run方法,创建实例对象,调用该对象的start方法启动线程方式二:创建Runnable接口的实现类,类中覆写run方法,再将实例作为此参数传递给Thread类有参构造创建线程对象,调用start方法启动

方式三:创建Callable接口的实现类,类中覆写call方法,创建实例对象,将其作为参数传递给FutureTask类有参构造创建FutureTask对象,再将FutureTask对象传递给Thread类的有参构造创建线程对象,调用start方法启动

Thread有单继承的局限性,Runnable和Callable三避免了单继承的局限,使用更广泛。Runnable适用于无需返回值的场景,Callable使用于有返回值的场景

2.Thread的start和run的区别

start是开启新线程, 而调用run方法是一个普通方法调用,还是在主线程里执行。没人会直接调用run方法

3.sleep 和 wait的区别

第一,sleep方法是Thread类的静态方法,wait方法是Object类的方法

第二:sleep方法不会释放资源,wait方法会释放资源

第三:wait方法在没有获取到锁时,会抛出异常

4.线程的几种状态

新建状态:线程刚创建,还没有调用start方法之前

就绪状态:也叫临时阻塞状态,当调用了start方法后,具备cpu的执行资格,等待cpu调度器轮询的状态

运行状态:就绪状态的线程,获得了cpu的时间片,真正运行的状态

冻结状态:也叫阻塞状态,指的是该线程因某种原因放弃了cpu的执行资格,暂时停止运行的状态,比如调用了wait,sleep方法

死亡状态:线程执行结束了,比如调用了stop方法

5.Synchronized 和 lock的区别

  1. synchronized是一个关键字,依靠Jvm内置语言实现,底层是依靠指令码来实现;Lock是一个接口,它基于CAS乐观锁来实现的
  2. synchronized在线程发生异常时,会自动释放锁,不会发生异常死锁,Lock在异常时不会自动释放锁,我们需要在finally中释放锁
  3. synchronized是可重入,不可判断,非公平锁,Lock是可重入,可判断的,可手动指定公平锁或者非公平锁

6.你知道AQS吗,是什么意思

AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制,它维护了一个volatile修饰的 int 类型的,state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

工作思想是如果被请求的资源空闲,也就是还没有线程获取锁,将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果请求的资源被占用,就将获取不到锁的线程加入队列。

7.悲观锁和乐观锁

悲观锁和乐观锁,指的是看待并发同步问题的角度

  • 悲观锁认为,对同一个数据的并发操作,一定是会被其他线程同时修改的。所以在每次操作数据的时候,都会上锁,这样别人就拿不到这个数据。如果不加锁,并发操作一定会出问题。
  • 乐观锁认为,对同一个数据的并发操作,是不会有其他线程同时修改的。它不会使用加锁的形式来操作数据,而是在提交更新数据的时候,判断一下在操作期间有没有其他线程修改了这个数据
  • 悲观锁一般用于并发小,对数据安全要求高的场景,乐观锁一般用于高并发,多读少写的场景,通常使用版本号控制,或者时间戳来解决.

8.你知道什么是CAS嘛

CAS,compare and swap的缩写,中文翻译成比较并交换。它是乐观锁的一种体现,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作

9.Synchronized 加非静态和静态方法上的区别

实例方法上的锁,锁住的是这个对象实例,它不会被实例共享,也叫做对象锁

静态方法上的锁,锁住的是这个类的字节码对象,它会被所有实例共享,也叫做类锁

10.Synchronized(this) 和 Synchronized (User.class)的区别

Synchronized(this) 中,this代表的是该对象实例,不会被所有实例共享

Synchronized (User.class),代表的是对类加锁,会被所有实例共享

11.Synchronized 和 volatitle 关键字的区别

这两个关键字都是用来解决并发编程中的线程安全问题的,不同点主要有以下几点

第一:volatile的实现原理,是在每次使用变量时都必须重主存中加载,修改变量后都必须立马同步到主存;synchronized的实现原理,则是锁定当前变量,让其他线程处于阻塞状态

第二:volatile只能修饰变量,synchronized用在修饰方法和同步代码块中

第三:volatile修饰的变量,不会被编译器进行指令重排序,synchronized不会限制指令重排序

第四:volatile不会造成线程阻塞,高并发时性能更高,synchronized会造成线程阻塞,高并发效率低

第五:volatile不能保证操作的原子性,因此它不能保证线程的安全,synchronized能保证操作的原子性,保证线程的安全

12.synchronized 锁的原理

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实 现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖 底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低,涉及到用户态到内核态的切换,会让整个程序性能变得很差。

因此在JDK1.6及以后的版本中,增加了锁升级的过程,依次为无锁,偏向锁,轻量级锁,重量级锁。而且还增加了锁粗化,锁消除等策略,这就节省了锁操作的开销,提高了性能

Synchronized 核心组件

1) Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;

2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;

3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;

4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;

5) Owner:当前已经获取到所资源的线程被称为 Owner;

6) !Owner:当前释放锁的线程。

实现原理:

1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,

ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将

一部分线程移动到 EntryList 中作为候选竞争线程。

2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定

EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。

3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,

OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在

JVM 中,也把这种选择行为称之为“竞争切换”。

4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList

中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify

或者 notifyAll 唤醒,会重新进去 EntryList 中。

5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统

来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。

6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先

尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是

不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁

资源。

参考:https://blog.csdn.net/zqz_zqz/article/details/70233767

7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加

上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的

8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线

程加锁消耗的时间比有用操作消耗的时间更多。

9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向

锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做

了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。

10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁

13.synchronized 锁升级原理

每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成,锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。

偏向锁(无锁)

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程 获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销),如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

轻量级锁(CAS):

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁自旋锁);没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)

重量级锁:

如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起,在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

14.乐观锁的使用场景(数据库,ES)

场景一:ES中对version的控制并发写。

场景二:数据库中使用version版本号控制来防止更新覆盖问题。

场景三:原子类中的CompareAndSwap操作

  1. 读多写少:在并发环境下,数据被多个用户频繁读取,但更新操作相对较少。在这种情况下,乐观锁可以提高系统的并发性能,因为它在读取数据时不加锁,只有在更新时才进行冲突检查。
  2. 事务粒度较小:乐观锁适用于事务处理的数据范围相对较小的情况,例如单个记录或少量记录的更新。由于每次更新时才会进行版本校验,因此当数据集较大时,频繁校验版本会增加开销。
  3. 冲突概率较低:乐观锁假设并发冲突的概率较低,在这种前提下避免了不必要的锁定资源,从而提高了系统整体的吞吐量。
  4. 高可用要求:对于对响应时间敏感的服务,如电商、金融等领域的部分业务模块,乐观锁能够减少因为等待锁释放而造成的延迟,提高服务的响应速度和可用性。

具体使用场景举例:

  • 商品库存管理:当一个商品有多个并发请求尝试购买时,每个请求先读取库存然后扣减,提交时检查库存是否已被其他请求修改过。
  • 订单状态变更:多个操作可能同时尝试改变订单状态,乐观锁通过版本号或其他机制确保在更新前状态未被他人更改。
  • 分布式环境下的数据一致性:在分布式系统中,乐观锁可以通过数据库中的version字段或者其他类似机制来保证不同节点间的数据同步时的一致性。

在实现上,乐观锁通常采用如下方式:

  • 数据库层面:在表中添加一个版本字段(version),每次更新时都会比较当前版本与数据库中的版本,如果一致则更新并递增版本;如果不一致,则认为存在并发修改,更新失败。
  • 内存级别:Java中通过AtomicInteger、AtomicLong等原子类,或者利用CAS(Compare And Swap)操作实现线程安全的无锁更新。

15.AtomicInterger怎么保证并发安全性的

通过CAS操作原理来实现的,就可见性和原子性两个方面来说

它的value值使用了volatile关键字修饰,也就保证了多线程操作时内存的可见性

Unsafe这个类是一个很神奇的类,而compareAndSwapInt这个方法可以直接操作内存,依靠的是C++来实现的,它调用的是Atomic类的cmpxchg函数。而这个函数的实现是跟操作系统有关的,比如在X86的实现就利用汇编语言的CPU指令lock cmpxchg,它在执行后面的指令时,会锁定一个北桥信号,最终来保证操作的原子性

16.什么是重入锁,什么是自旋锁,什么是阻塞

可重入锁是指允许同一个线程多次获取同一把锁,比如一个递归函数里有加锁操作

自旋锁不是锁,而是一种状态,当一个线程尝试获取一把锁的时候,如果这个锁已经被占用了,该线程就处于等待状态,并间隔一段时间后再次尝试获取的状态,就叫自旋

阻塞,指的是当一个线程尝试获取锁失败了,线程就就进行阻塞,这是需要操作系统切换CPU状态的

17.你用过JUC中的类吗,说几个

Lock锁体系 ,ConcurrentHashMap ,Atomic原子类,如:AtomicInteger ;ThreadLoal ; ExecutorService%%

显示锁类

ReentrantLook,ReadWriteLock

原子变量类

Atomic原子类

线程池相关

future定时相关,callable,Executor,拒绝策略常用的拒绝策略有

  • 丢失任务抛出异常
  • 丢弃任务不抛出异常
  • 丢弃等待队列第一个,尝试执行任务
  • 由调度线程执行任务

容器类

concurerentHashMap,

队列:双向队列,阻塞队列:常用的阻塞队列

synchronousQueue,不会阻塞,直接创建新的线程执行

ArrayBlockingQueue,基于数组的阻塞队列,需要给一个默认的长度

LinkedBlockingQueue,基于链表的阻塞队列,默认长度是int的最大值

工具类

信号量,计数器,栅栏,创建线程池的工具

  1. java.util.concurrent.locks.ReentrantLock:一个可重入的互斥锁,比传统的synchronized关键字提供了更高的灵活性,如尝试获取锁、定时获取锁、公平锁等特性。
  2. java.util.concurrent.Semaphore:信号量,用于控制同时访问特定资源的线程数量。
  3. java.util.concurrent.CountDownLatch:计数器门闩,用于多线程同步,允许一个或多个线程等待其他线程完成一系列操作后再执行。
  4. java.util.concurrent.CyclicBarrier:循环栅栏,让一组线程在到达某个屏障点时被阻塞,直到所有线程都到达后才继续执行。
  5. java.util.concurrent.ExecutorService 和 ThreadPoolExecutor:提供线程池服务,用于管理和调度任务执行。
  6. java.util.concurrent.Future<T> 和 FutureTask<T>:表示异步计算的结果,可以通过它来取消执行的任务或者检查计算是否完成并获取结果。
  7. java.util.concurrent.ConcurrentHashMap<K, V>:线程安全的哈希表,支持高并发环境下的读写操作。
  8. java.util.concurrent.atomic 包中的原子类:例如 AtomicInteger、AtomicLong、AtomicReference 等,它们提供了线程安全的原子操作。
  9. java.util.concurrent.BlockingQueue<E> 接口及其实现类:如 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等,用于线程间的数据交换。
  10. java.util.concurrent.Callable<V>:类似于 Runnable 接口,但它允许返回值,并且可以抛出异常。

这些JUC组件都是Java进行并发编程时的重要工具。

18.ThreadLocal的作用和原理

ThreadLocal,叫做线程本地变量,它是为了解决线程安全问题的,它通过为每个线程提供一个独立的变量副本,来解决并发访问冲突问题 - 简单理解它可以把一个变量绑定到当前线程中,达到线程间数据隔离目的。

原理:ThredLocal是和当前线程有关系的,每个线程内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,它用来存储每个线程中的变量副本,key就是ThreadLocal变量,value就是变量副本。

当我们调用get方法是,就会在当前线程里的threadLocals中查找,它会以当前ThreadLocal变量为key获取当前线程的变量副本

它的使用场景比如在spring security中,我们使用SecurityContextHolder来获取SecurityContext,比如在springMVC中,我们通过RequestContextHolder来获取当前请求,比如在 zuul中,我们通过ContextHolder来获取当前请求

ThreadLocal内存泄漏的问题

threadLocal对象被回收了,但是threadlocalMap存在与当前线程中,如果当前线程还在工作没有被回收,那么这个map就是强引用,导致map中的entry无法被回收。可以手动remove移除,来消除掉entry的可达性,从而被GC回收。

19.线程池的作用

请求并发高的时候,如果没有线程池会出现线程频繁创建和销毁而浪费性能的情况,同时没办法控制请求数量,所以使用了线程池后有如下好处

  • 主要作用是控制并发数量,线程池的队列可以缓冲请求
  • 线程池可以实现线程的复用效果
  • 使用线程池能管理线程的生命周期

20.Executors创建四种线程池

  • CachedThreadPool:可缓存的线程池,它在创建的时候,没有核心线程,线程最大数量是Integer最大值,最大空闲时间是60S
  • FixedThreadPool:固定长度的线程池,它的最大线程数等于核心线程数,此时没有最大空闲时长为0
  • SingleThreadPool:单个线程的线程池,它的核心线程和最大线程数都是1,也就是说所有任务都串行的执行
  • ScheduledThreadPool:可调度的线程池,它的最大线程数是Integer的最大值,默认最长等待时间是10S,它是一个由延迟执行和周期执行的线程池

21.线程池的核心参数

核心线程数:创建后不会被销毁‘

最大线程数:核心线程数+非核心线程数

非核心线程最大等待时间:非核心线程超过这个时间没有被调用就会被回收

时间单位:

等待队列:常见的等待队列有synchronsQueue,ArrayBlockingQueue,LinkedBlockingQueue

拒绝策略:丢弃任务抛出异常,丢弃任务不抛出异常,丢弃队列第一个尝试执行当前任务,使用调度线程去执行任务

线程工厂:4个

22.线程池的执行流程

corePoolSize,maximumPoolSize,workQueue之间关系。

1. 当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程(使用核心)执行任务,即使此时线程池中存在空闲线程。

2. 当线程池中线程数达到corePoolSize时(核心用完),新提交任务将被放入workQueue中,等待线程池中任务调度执行 。

3. 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程(非核心)执行任务。

4. 当workQueue已满,且提交任务数超过maximumPoolSize(线程用完,队列已满),任务由RejectedExecutionHandler处理。

5. 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。

6. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。

线程池执行流程: 核心线程=> 等待队列 => 非核心线程 => 拒绝策略


原文地址:https://blog.csdn.net/LG_971124/article/details/144377413

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!