![](https://static.nowcoder.com/fe/file/hr-image/1705641251470_1185.gif)
前言
p.s.戳这里查看《Java面试必知必会》系列内容~
01
说说什么是线程安全?如何实现线程安全?
回答:
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。【摘自深入理解Jvm虚拟机】
实现线程安全的方式有三大种方法,分别是互斥同步、非阻塞同步和无同步方案。
互斥同步:
同步是指多个线程并发访问共享数据时,保证共享数据在同一各时刻只被一条(或一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界去、互斥量和信号量都是常见的互斥实现方式。Java中实现互斥同步的手段主要有synchronized关键字或ReentrantLock等。
非阻塞同步类似是一种乐观并发的策略,比如CAS。
无同步方案,比如使用ThreadLocal。
追问1:synsynchronized和ReentLock的区别是什么?
相同点:
(1)都是可重入锁;
(2)都保证了可见性和互斥性;
(3)都可以用于控制多线程对共享对象的访问。
不同点:
(1)ReentrantLock等待可中断。
(2)synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,但是可以通过修改参数来实现公平锁。
(3)ReentrantLock绑定多个条件。
(4)synchronized是Java中的关键字是JVM级别的锁,而ReentrantLock是一个Lock接口下的实现类,是API层面的锁。
(5)synchronized隐式获取锁和释放锁,ReentrantLock显示获取和释放锁,在使用时避免程序异常无法释放锁,需要在finally控制块中进行解锁操作。
追问2:synsynchronized和volatile的区别
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
🔷volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而 synchronized` 关键字可以修饰方法以及代码块。
🔷volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
🔷volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
【摘自网络】
追问3:synchronized锁升级的过程说一下?
回答:
在jdk1.6后Java对synchronize锁进行了升级过程,主要包含偏向锁、轻量级锁和重量级锁,主要是针对对象头MarkWord的变化。
(1)偏向锁:
为什么要引入偏向锁?
因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
偏向锁的升级:
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;
如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
(2)轻量级锁
为什么要引入轻量级锁?
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
轻量级锁什么时候升级为重量级锁?
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
追问4:synchronize锁的作用范围
回答:
(1)synchronize作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
(2)synchronize作用于静态方法时,锁住的是Class实例。
(3)synchronize作用于一个代码块时,锁住的是所有代码块中配置的对象。
02
Java中线程的状态有哪些?线程间的通信方式有哪些?
回答:
Java中线程生命周期分为新建(New)、运行(Runnable)、阻塞(Blocked)、无限期等待(Waiting)、限期等待(Time Waiting)和结束(Terminated)这6种状态。
![](https://static.nowcoder.com/fe/file/hr-image/1705641251863_1186.png)
Java中线程间通信方式有:
互斥量(Mutex):
采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
信号量(Semphares) :
它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
事件(Event) :Wait/Notify:
通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操。
追问1:sleep后进入什么状态,wait后进入什么状态?
回答:
sleep后进入Time waiting超时等待状态,wait后进入等待waiting状态。
追问2:sleep和wait的区别?
回答:
(1)sleep方法属于Thread类,wait方法属于Object类。
(2)sleep方法暂停执行指定的时间,让出CPU给其他线程,但其监控状态依然保持在指定的时间过后又会自动恢复运行状态。
(3)在调用sleep方法的过程中,线程不会释放对象锁,而wait会释放对象锁。
追问3:wait为什么是数Object类下面的方法?
这个问题我被问到过两次,第一次不会(美团),就去百度搜了搜,第二次遇到就会了(贝壳),下面是网上搜到的。
所谓的释放锁资源实际是通知对象内置的monitor对象进行释放,而只有所有对象都有内置的monitor对象才能实现任何对象的锁资源都可以释放。又因为所有类都继承自Object,所以wait()就成了Object方法,也就是通过wait()来通知对象内置的monitor对象释放,而且事实上因为这涉及对硬件底层的操作,所以wait()方法是native方法,底层是用C写的。【来自网络】
追问4:start方法和run方法有什么区别?
(1)start方法用于启动线程,真正实现了多线程运行。在调用了线程的start方法后,线程会在后台执行,无须等待run方法体的代码执行完毕。
(2)通过调用start方法启动一个线程时,此线程处于就绪状态,并没有运行。
(3)run方法也叫做线程体,包含了要执行的线程的逻辑代码,在调用run 方法后,线程就进入运行状态,开始运行run方法中的代码,在run方法运行结束后,该线程终止,CPU在调度其他的线程。
03
AQS了解吗?
回答:
AQS是一个抽象队列同步器,通过维护一个状态标志位state和一个先进先出的(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。
AQS的原理大概是这样的,给每个共享资源都设置一个共享锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果没有获取到共享锁,该线程被放入到等待队列中,等待下一次资源调度。
![](https://static.nowcoder.com/fe/file/hr-image/1705641252043_1187.png)
AQS定义了两种资源共享方式:独占式和共享式
独占式:
只有一个线程能执行,具体的Java实现有ReentrantLock。
共享式:
多个线程可同时执行,具体的Java实现有Semaphore和CountDownLatch。
AQS只是一个框架(模板模式),只定义了一个接口,具体资源的获取、释放都交由自定义同步器去实现。不同的自定义同步器争取用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器在做处理。
追问1:Java中的并发关键字
Java中常见的并发关键字有CountDownLatch、CylicBarrier、Semaphore和volatile。
追问2:你使用过哪个AQS组件,有将其用于多线程编程吗?(给一个例题说一下思路或者直接写)
回答:
这个可以自己去网上找一些,或者自己总结一些。我一般举的例子是我在笔试中遇到的一个例子。就有四个子线程分别统计四个盘的容量,然后最终通过一个主线程将四个盘的进行求和,输出总的容量。
用到了CountDownLatch,他是基于线程计数器来实现并发控制,主要用于主线程等待其他子线程都执行完毕后执行相关操作。
p.s.详细代码请戳这里查看
04
CAS说一下
CAS指Compare and swap比较和替换是设计并发算法时用到的一种技术,CAS指令有三个操作数,分别是内存位置(在Java中可以简单的理解为变量的内存地址,用V表示),旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令在执行的时候,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不会执行更新。
追问1:CAS带来的问题是什么?如何解决的?
回答:
ABA问题、循环时间长开销很大、只能保证一个共享变量的原子操作。
一般加版本号进行解决(具体操作:乐观锁每次在执行数据的修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时就可以执行修改操作,并对版本号执行加1操作,否则执行失败。)
追问2:什么是乐观锁,什么是悲观锁?
回答:
悲观锁和乐观锁并不是某个具体的“锁”而是一种并发编程的基本概念。乐观锁和悲观锁最早出现在数据库的设计当中,后来逐渐被 Java 的并发包所引入。
悲观锁:
认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观地认为,不加锁的并发操作一定会出问题。
乐观锁:
正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。
05
Java中创建线程的方式有哪些?
回答:
Java中创建线程的方式有4种,分别是:
1.写一个类继承子Thread类,重写run方法;
2.写一个类重写Runable接口,重写run方法;
3.写一个类重写Callable接口,重写call方法;
4.使用线程池。
追问1:线程池的好处?说几个Java中常见的线程池?说一下其中的参数和运行流程?
回答:
使用线程池可以降低资源消耗(反复创建线程是一件很消耗资源的事,利用已创建的线程降低线程创建和销毁造成的消耗)、提供处理速度(当任务到达时,可以直接使用已有线程,不比等到线程创建完成才去执行。)、线程资源可管理性和通过控制系统的最大并发数,以保证系统高效且安全的运行。
Executors 实现了以下四种类型的 ThreadPoolExecutor:
![](https://static.nowcoder.com/fe/file/hr-image/1705641252293_1188.png)
线程池有7大核心参数,分别是:
🔷corePoolSize:核心线程数;
🔷maximumPoolSize:线程池中最大线程数;
🔷keepAliveTime:多余空闲线程数的存活时间,当前线程数大于corePoolSize,并且等待时间大于keepAliveTime,多于线程或被销毁直到剩下corePoolSize为止;
🔷TimeUnit unit: keepAliveTime的单位;
🔷workQueue:阻塞队列,被提交但未必执行的任务;
🔷threadFactory:用于创建线程池中工作线程的线程工厂,一般用默认的;
🔷handler:拒绝策略,当堵塞队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)。
![](https://static.nowcoder.com/fe/file/hr-image/1705641252533_1189.png)
线程池中的执行流程:
(1)当线程数小于核心线程数的时候,使用核心线程数。
(2)如果核心线程数小于线程数,就将多余的线程放入任务队列(阻塞队列)中。
(3)当任务队列(阻塞队列)满的时候,就启动最大线程数。
(4)当最大线程数也达到后,就将启动拒绝策略。
![](https://static.nowcoder.com/fe/file/hr-image/1705641252838_1190.png)
追问2:拒绝策略有哪些?
回答:
有四种拒绝策略
1.ThreadPoolExecutor.AbortPolicy
线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常(即后面提交的请求不会放入队列也不会直接消费并抛出异常);
2.ThreadPoolExecutor.DiscardPolicy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃(也不会抛出任何异常,任务直接就丢弃了)。
3.ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务(丢弃掉了队列最前的任务,并不抛出异常,直接丢弃了)。
4.ThreadPoolExecutor.CallerRunsPolicy
由调用线程处理该任务(不会丢弃任务,最后所有的任务都执行了,并不会抛出异常)
追问3:线程池的参数如何确定呢?
回答:
一般需要确定核心线程数、最大线程数、任务队列和拒绝策略,这些需要根据实际的业务场景去设置,可以大致分为CPU密集型和IO密集型。
CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务。
IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数。
详细可以看一下《阿里调优手册》
追问4:Java中常见的阻塞队列有哪些?
ArrayBlockingQueue:
是一个我们常用的典型的有界队列,其内部的实现是基于数组来实现的。
LinkedBlockingQueue 从它的名字我们可以知道,它是一个由链表实现的队列,这个队列的长度Integer.MAX_VALUE ,这个值是非常大的,几乎无法达到,对此我们可以认为这个队列基本属于一个无界队列(也又认为是有界队列)。此队列按照先进先出的顺序进行排序。
SynchronousQueue 是一个不存储任何元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。同时它也支持公平锁和非公平锁。
PriorityBlockingQueue是一个支持优先级排序的无界阻塞队列,可以通过自定义实现 compareTo() 方法来指定元素的排序规则,或者通过构造器参数 Comparator 来指定排序规则。但是需要注意插入队列的对象必须是可比较大小的,也就是 Comparable 的,否则会抛出 ClassCastException 异常。
DelayQueue是一个实现PriorityBlockingQueue的延迟获取的无界队列。具有“延迟”的功能。
06
ThreaLocal知道吗?
回答:
Java中每一个线程都有自己的专属本地变量, JDK 中提供的ThreadLocal类,ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
1.ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。
2.ThreadLocal底层是通过ThreadLocalmap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值。
3.ThreadLocal经典的应用场景就是连接管理(一个线程持有一个链接,该连接对象可以在不同给的方法之间进行线程传递,线程之间不共享同一个连接)。
追问1:用它可能会带来什么问题?
回答:
如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalmap,ThreadLocalmap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是:在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象。
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。【摘自JavaGuide】
追问2:什么是强软弱虚引用?
回答:
(1)强引用是使用最普遍的引用。只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
(2)软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。只有在内存不足的时候JVM才会回收该对象。
(3)只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
(4)虚引用也称为幻影引用,一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。
虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
看完后记得点赞加分享哦,戳阅读原文查看《Java面试必知必会》系列内容~
【高频知识点汇总】内容征集中,参与即有奖,另外最高可领取500元京东卡和500元实物奖品!>>>点击查看
★
作者:代码界的小白
链接:
https://www.nowcoder.com/link/gzhts-zsdhz
来源:牛客网
![](https://static.nowcoder.com/fe/file/hr-image/1705641253042_1191.jpeg)
点击阅读原文查看更多内容
▼▼▼
▼▼▼