5.Java并发编程高频面试题

本文详细介绍了Java并发编程中的关键概念,包括并发编程的好处和缺点,线程的生命周期,线程安全,同步方法和同步块,以及锁的类型如synchronized和Lock,还包括线程池、线程局部变量(ThreadLocal)、并发容器如ConcurrentHashMap等。文章还深入讨论了线程间的通信、死锁和避免死锁的方法,以及如何创建和管理线程。
摘要由CSDN通过智能技术生成

目录


Java Web面试题目录清单(高频面试题型)(点击进入…)


Java并发编程高频面试题


Java并发编程高频面试题

1.为什么要使用并发编程(并发编程的优点)

(1)充分利用多核CPU的计算能力
通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升

(2)方便进行业务拆分,提升系统并发能力和性能
在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分


2.并发编程有什么缺点

并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题。比如:内存泄漏、上下文切换、线程安全、死锁等问题


3.并发编程三要素是什么?在Java程序中怎么保证多线程的运行安全?

并发编程三要素(线程的安全性问题体现在)
(1)原子性
原子。即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败
解决:synchronized、Lock,可以解决原子性问题

(2)可见性
一个线程对共享变量的修改,另一个线程能够立刻看到(synchronized,volatile)
解决:synchronized、volatile、Lock,可以解决可见性问题

(3)有序性
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
解决:Happens-Before规则可以解决有序性问题

出现线程安全问题的原因:
(1)线程切换带来的原子性问题
(2)缓存导致的可见性问题
(3)编译优化带来的有序性问题


4.并行和并发有什么区别?

运行方式描述
并发多个任务在同一个CPU上,按细分的时间片轮流执行(交替),从逻辑上来看,那些任务是同时执行
并行单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”
串行有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题

做一个形象的比喻:
并发 = 两个队列和一台咖啡机
并行 = 两个队列和两台咖啡机
串行 = 一个队列和一台咖啡机


5.什么是多线程,多线程的优劣?

指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务

好处:可以提高CPU的利用率
在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

劣势
(1)线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
(2)多线程需要协调和管理,需要CPU时间跟踪线程
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题


6.什么是线程和进程?

进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的*.exe就是一个进程

线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据


7.进程与线程的区别

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

进程线程
根本区别操作系统资源分配的基本单位处理器任务调度和执行的基本单位
资源开销进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
包含关系如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的线程是进程的一部分,所以线程也被称为轻权进程或轻量级进程
内存分配进程之间的地址空间和资源是相互独立的同一进程的线程共享本进程的地址空间和资源
影响关系一个进程崩溃后,在保护模式下不会对其他进程产生影响一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮
执行过程每个独立的进程有程序运行的入口、顺序执行序列和程序出口线程不能独立执行,必须依存在应用程序中(进程),由应用程序提供多个线程执行控制,两者均可并发执行

8.什么是上下文切换?

多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的CPU时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux相比与其他操作系统(包括其他类Unix系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少


9.守护线程和用户线程有什么区别呢?

(1)用户线程(User)
运行在前台,执行具体的任务。如程序的主线程、连接网络的子线程等都是用户线程

(2)守护线程(Daemon)
运行在后台,为其他前台线程服务。也可以说守护线程是JVM中非守护线程的“佣人”。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作

main函数所在的线程就是一个用户线程,main函数启动的同时在JVM内部同时还启动了很多守护线程,比如垃圾回收线程。

当非守护线程(用户线程)都执行完毕后,无论有没有守护线程在运行,虚拟机都会自动退出。守护线程不会影响JVM的退出


10.如何创建守护线程?

setDaemon(true)必须在start()前执行,否则会抛出IllegalThreadStateException异常
在守护线程中产生的新线程也是守护线程

注意:守护线程(Daemon)不能持有需要关闭的资源(读写操作或者计算逻辑),因为它会在任何时候甚至在一个操作的中间发生中断

守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程(Daemon)中的finally语句块可能无法被执行


11.什么是线程死锁?

死锁:指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。


12.形成死锁的四个必要条件是什么?

四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁

(1)互斥条件
线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。进程在某一时间内独占资源

(2)请求与保持条件
一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放

(3)不剥夺条件
线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源

(4)循环等待条件
当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞。若干进程之间形成一种头尾相接的循环等待资源关系


13.如何避免线程死锁

只要破坏产生死锁的四个条件中的其中一个就可以
(1)破坏互斥条件
这个条件没有办法破坏,因为用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

(2)破坏请求与保持条件
一次性申请所有的资源

(3)破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

(4)破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件

防止死锁可以采用以下的方法:
(1)尽量使用tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
(2)尽量使用 Java. util. concurrent 并发类代替自己手写锁。
(3)尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
(4)尽量减少同步的代码块
.


14.创建线程有哪几种方式?

(1)继承Thread类(无返回值)
(2)实现Runnable接口(无返回值)
(3)实现Callable接口(有返回值)
(4)使用Executors工具类创建线程池(有返回值)


15.Runnable和Callable有什么区别?

相同点
(1)都是接口
(2)都可以编写多线程程序
(3)都采用Thread.start()启动线程

主要区别

RunnableCallable
run()无返回值call()有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
run()只能抛出运行时异常,且无法捕获处理Call()允许抛出异常,可以获取异常信息

注意:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞


16.线程的run()和start()区别?

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()称为线程体。通过调用Thread类的start()来启动一个线程。

startrun
start()用于启动线程,只能调用一次run()用于执行线程的运行时代码,可以重复调用
用来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run()体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行状态,run()方法运行结束,此线程终止。然后CPU再调度其它线程run()是在本线程里的,只是线程里的一个函数方法,而不是多线程的。如果直接调用run(),其实就相当于是调用了一个普通方法而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法

启动一个线程需要调用Thread对象的start()
(1)调用线程的start()后,线程处于可运行状态;此时它可以由JVM调度并执行,这并不意味着线程就会立即运行
(2)run()是线程运行时由JVM回调的方法,无需手动写代码调用。直接调用线程的run()方法,相当于在调用线程里继续调用方法,并未启动一个新的线程


17.为什么调用start()方法时会执行run()方法,为什么不能直接调用run()方法?

new Thread();,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当系统分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作

直接执行run():会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作

总结:调用start()可启动线程并使线程进入就绪状态,而run()只是thread的一个普通方法调用,还是在主线程里执行


18.什么是Callable和Future?

Callable接口类似于Runnable,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值

Future接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future用于获取结果


19.什么是FutureTask?

FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get方法将会阻塞。一个FutureTask 对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中


20.线程的生命周期及五种基本状态?

(1)新建(new)
新创建了一个线程对象

(2)可运行(runnable)
线程对象创建后,当调用线程对象的start(),该线程处于就绪状态,等待被线程调度选中,获取CPU的使用权

(3)运行(running)
可运行状态(Runnable)的线程获得了CPU时间片(timeslice),执行程序代码

注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中

(4)阻塞(block)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,等待再次被CPU调用以进入到运行状态

阻塞的情况分三种:

阻塞情况描述
等待阻塞运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态
同步阻塞线程在获取synchronized同步锁失败(锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态
其他阻塞通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态

(5)死亡(dead)
线程run()、main()执行结束,或者因异常退出了run(),则该线程结束生命周期。死亡的线程不可再次复生


21.Java用到的线程调度算法是什么?

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。

多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权

线程调度模型:分时调度模型、抢占式调度模型(两种):
(1)分时调度模型
指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解

(2)抢占式调度模型
指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU


22.线程的调度策略?

线程调度器选择优先级最高的线程运行。但是,如果发生以下情况,就会终止线程的运行:
(1)线程体中调用了yield()让出了对CPU的占用权利
(2)线程体中调用了sleep()使线程进入睡眠状态
(3)线程由于IO操作受到阻塞
(4)另外一个更高优先级线程出现
(5)在支持时间片的系统中,该线程的时间片用完


23.什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?

(1)线程调度器
一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启动它,它的执行便依赖于线程调度器的实现

(2)时间分片
指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间

线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让程序依赖于线程的优先级)


24.与线程同步以及线程调度相关的方法

方法描述
wait()使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁
sleep()使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常
notify()唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关
notityAll()唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态

25.sleep()和wait()有什么区别?

两者都可以暂停线程的执行

sleep()wait()
类的不同Thread线程类的静态方法Object类的方法
是否释放锁不释放锁释放锁
用途不同用于线程间交互/通信用于暂停执行
用法不同sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法

26.如何调用wait()方法的?使用if块还是循环?为什么?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

wait()方法应该在循环调用,因为当线程获取到CPU开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好

一段标准的使用wait()和notify()的代码:

synchronized (Outer.class) {
	// 判断条件谓词是否得到满足
	while (!locked) {
		// 等待唤醒
		monitor.wait();
	}
	// 处理其他的业务逻辑
}

27.为什么线程通信的方法 wait()、notify()和notifyAll()被定义在Object类里?

Java中,任何对象都可以作为锁,并且wait()、notify()等方法用于等待对象的锁或者唤醒线程,在Java的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中

wait()、notify()和notifyAll()这些方法在同步代码块中调用
有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程
个线程完全可以持有很多锁,一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。

wait()、notify()和notifyAll()方法要定义在Object类中


28.为什么wait()、notify()和notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以只能在同步方法或者同步块中被调用


29.Thread类中的yield()作用?

使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了


30.为什么Thread类的sleep()和yield ()方法是静态的?

Thread类的sleep()和yield()将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法


31.线程的sleep()和yield()区别?

sleep()yield()
运行给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会只会给相同优先级或更高优先级的线程以运行的机会
线程执行sleep()方法后转入阻塞(blocked)状态执行yield()方法后转入就绪(ready)状态
异常sleep()方法声明抛出 InterruptedExceptionyield()方法没有声明任何异常

sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行


32.如何停止一个正在运行的线程?

在Java中有以下3种方法可以终止正在运行的线程:
(1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
(2)使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法
(3)使用interrupt()中断线程


33.Java中interrupted和isInterrupted方法的区别?

Interrupt:用于中断线程。调用该方法的线程的状态为将被置为“中断”状态。

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true还是false


34.什么是阻塞式方法?

阻塞式方法:指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回


35.Java怎样唤醒一个阻塞的线程?

wait()、notify()是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁。相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行。其次,wait、notify()必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait()、notify()的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放


36.notify()和notifyAll()区别?

如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁

方法描述
notify()只会唤醒一个线程
notifyAll()调用后会唤醒所有的线程,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制

37.如何在两个线程间共享数据?

在两个线程间共享变量即可实现共享
一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性


38.同步方法和同步块,哪个是更好的选择?

同步块是更好的选择,因为它不会锁住整个对象(当然也可以让它锁住整个对象)
同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁

同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁
请知道一条原则:同步的范围越小越好


39.什么叫线程安全?Servlet是线程安全吗?

指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成
Servlet不是线程安全的,Servlet是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。

Struts2的Action是多实例多线程的,是线程安全的,每个请求过来都会new一个新的action分配给这个请求,请求完成后销毁。
Spring MVC的Controller是线程安全的吗?不是的,和Servlet类似的处理流程。

Struts2好处是不用考虑线程安全问题;Servlet和SpringMVC需要考虑线程安全问题,但是性能可以提升不用处理太多的gc,可以使用ThreadLocal来处理多线程的问题


40.在Java程序中怎么保证多线程的运行安全?

方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized
方法三:使用手动锁 Lock

手动锁Java示例代码如下:

Lock lock = new ReentrantLock();
lock.lock();
try {
    System.out.println("获得锁");
} catch (Exception e) {
    // TODO: handle exception
} finally {
    System.out.println("释放锁");
    lock.unlock();
}

41.你对线程优先级的理解是什么?

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关
可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10)
1:代表最低优先级,10:代表最高优先级。

Java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级


42.多线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题
请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run()里面的代码才是被线程自身所调用的

例子:假设 Thread2 中new了Thread1,main 函数中new了Thread2,那么:
(1)Thread2的构造方法、静态块是 main 线程调用的,Thread2的run()方法是Thread2自己调用的
(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的


43.一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,
JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler 并将线程和异常作为参数传递给handler的 uncaughtException()方法进行处理


44.Java线程数过多会造成什么异常?

(1)线程的生命周期开销非常高
(2)消耗过多的CPU
(3)资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销
(4)降低稳定性JVM

在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常


45.Java中垃圾回收有什么目的?什么时候进行垃圾回收?

垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的
垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源


46.如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

不会。在下一个垃圾回调周期中,这个对象将是被可回收的
也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存


47.finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?

(1)垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;
finalize是Object类的一个方法,该方法在Object类中的声明

protected void finalize() throws Throwable { }

在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。.

注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间

(2)GC本来就是内存回收了,应用还需要在finalization做什么呢?
大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数


48.为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是想怎么排序就怎么排序,它需要满足以下两个条件:
(1)在单线程环境下不能改变程序运行的结果
(2)存在数据依赖关系的不允许重排序
注意:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义


49.synchronized作用?

在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。synchronized可以修饰类、方法、变量

在Java早期版本中,synchronized属于重量级锁、效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,Java线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因

在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了。JDK 1.6对锁的实现引入了大量的优化;如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销


50.说说自己是怎么使用synchronized关键字,在项目中用到了吗

synchronized关键字最主要的三种使用方式:

(1)修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

(2)修饰静态方法
也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象锁

(3)修饰代码块
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
总结:synchronized关键字加到static静态方法和synchronized(class)代码块上都是是给Class类上锁。synchronized关键字加到实例方法上是给对象实例上锁。尽量不要使用synchronized(String a)因为JVM中,字符串常量池具有缓存功能

双重校验锁实现对象单例(线程安全)

public class Singleton {
    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

需要注意uniqueInstance采用volatile关键字修饰也是很有必要
uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
(1)为uniqueInstance分配内存空间
(2)初始化uniqueInstance
(3)将uniqueInstance指向分配的内存地址

但是由于JVM具有指令重排的特性,执行顺序有可能变成1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。

例如:线程T1执行了1和3,此时T2调用getUniqueInstance()后发现uniqueInstance不为空,因此返回 uniqueInstance,但此时uniqueInstance还未被初始化。
使用volatile可以禁止JVM的指令重排,保证在多线程环境下也能正常运行


51.synchronized可重入的原理?

重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器。当线程获取该锁时,计数器加一,再次获得该锁时继续加一;释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁


52.什么是自旋?

很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略


53.多线程中synchronized锁升级的原理是什么?

synchronized锁升级原理:在锁对象的对象头里面有一个threadid字段,在第一次访问的时候threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断 threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized锁的升级

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在Java 6之后优化synchronized的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗


54.线程B怎么知道线程A修改了变量

(1)volatile修饰变量
(2)synchronized修饰修改变量的方法
(3)wait/notify
(4)while轮询


55.当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的 synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁


56.synchronized、volatile、CAS 比较

synchronized、volatile、CAS 比较描述
synchronized悲观锁,属于抢占式,会引起其他线程阻塞
volatile提供多线程共享变量可见性和禁止指令重排序优化
CAS基于冲突检测的乐观锁(非阻塞)

57.synchronized和Lock有什么区别?

synchronizedLock
synchronized是Java内置关键字 在JVM层面Lock是个Java类(接口)
可以给类、方法、代码块加锁Lock只能给代码块加锁
synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁Lock需要自己加锁和释放锁,如果使用不当没有unLock()去释放锁就会造成死锁
synchronized却无法办到Lock可以知道有没有成功获取锁

58.synchronized和ReentrantLock区别?

相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”

概念:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁
本质区别:ReentrantLock是类,比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量

synchronized早期的实现比较低效,对比ReentrantLock,大多数场景性能都相差较大,但是在Java 6中对synchronized进行了非常多的改进。

synchronizedReentrantLock
本质区别是和if、else一样的关键字ReentrantLock是类。比较灵活,但是必须有释放锁的配合动作
不需要手动释放和开启锁手动获取与释放锁
使用范围可以修饰类、方法、变量等只适用于代码块锁
底层机制操作的是对象头中mark word底层调用的是Unsafe的park方法加锁

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础
(1)普通同步方法。锁是当前实例对象
(2)静态同步方法。锁是当前类的class对象
(3)同步方法块。锁是括号里面的对象


59.volatile关键字作用?

对于可见性,Java提供了volatile关键字来保证可见性和禁止指令重排。

volatile提供happens-before的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到内存,当有其他线程需要读取时,它会去内存中读取新值

从实践角度而言,volatile重要作用就是和CAS结合,保证了原子性。volatile常用于多线程环境下的单次操作(单次读或者单次写)


60.Java中能创建volatile数组?

。Java中可以创建volatile类型数组,不过只是一个指向数组的引用,而不是整个数组

意思:如果改变引用指向的数组,将会受到volatile的保护,但是如果多个线程同时改变数组的元素,volatile标示符就不能起到之前的保护作用了


61.volatile变量和atomic变量有什么不同?

volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用volatile修饰count变量,那么count++操作就不是原子性的

而AtomicInteger类提供的atomic()可以让这种操作具有原子性如getAndIncrement()会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作


62.volatile能使得一个非原子操作变成原子操作?

关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。

所以从Oracle Java Spec里面可以看到:
(1)对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作
(2)如果使用volatile修饰long和double,那么其读写都是原子操作
(3)对于64位的引用地址的读写,都是原子操作
(4)在实现JVM时,可以自由选择是否把读写long和double作为原子操作
(5)推荐JVM实现为原子操作


63.synchronized和volatile区别?

synchronized表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程

volatile表示变量在CPU的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序

区别

volatilesynchronized
变量修饰符可以修饰类、方法、变量
仅能实现变量的修改可见性,不能保证原子性可以保证变量的修改可见性和原子性
不会造成线程的阻塞可能会造成线程的阻塞
标记的变量不会被编译器优化标记的变量可以被编译器优化

volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。

synchronized关键字在Java SE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用synchronized关键字的场景还是更多一些


64.什么是不可变对象,对写并发应用有什么帮助?

不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)

不可变对象的类即为不可变类(Immutable Class)。Java平台类库中包含许多不可变类:String、基本类型的包装类、BigInteger和BigDecimal等

只有满足如下状态,一个对象才是不可变的;
(1)它的状态不能在创建后再被修改
(2)所有域都是final类型;并且,它被正确创建(创建期间没有发生this引用的溢出)

不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率


65.Lock接口(Lock interface)是什么?对比同步它有什么优势?

Lock接口比同步方法和同步块提供了更具扩展性的锁操作。允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁

整体上来说Lock是synchronized的扩展版,Lock提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition方法)锁操作。另外Lock的实现类基本都支持非公平锁(默认)和公平锁,synchronized只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择


66.乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

(1)悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
悲观锁:将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问

(2)乐观锁

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
乐观锁:采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高

乐观锁的实现方式:
1.使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略
2.Java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS 操作中包含三个操作数:需要读写的内存位置(V)、进行比较的预期原值(A)、拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作


67.什么是CAS?

CAS(compare and swap),即比较交换
CAS是一种基于锁的操作,而且是乐观锁。在Java中锁分为乐观锁和悲观锁

CAS操作包含三个操作数:内存位置(V)、预期原值(A)、新值(B)

如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

java.util.concurrent.atomic 包下的类大多是使用CAS操作来实现的
(AtomicInteger,AtomicBoolean,AtomicLong)


68.CAS会产生什么问题?

(1)ABA问题
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类 AtomicStampedReference 来解决ABA问题

(2)循环时间长开销大
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

(3)只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁


69.死锁与活锁的区别,死锁与饥饿的区别?

(1)死锁
指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去

(2)活锁
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败…

(3)饥饿
一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态

Java中导致饥饿的原因
(1)高优先级线程吞噬所有的低优先级线程的CPU时间
(2)线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问
(3)线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是被持续地获得唤醒


70.多线程锁的升级原理是什么?

在Java中,锁共有4种状态,级别从低到高依次为
①无状态锁
②偏向锁
③轻量级锁
④重量级锁状态
这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级


71.什么是可重入锁(ReentrantLock)?

ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞

重入性的实现原理
(1)在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功
(2)由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功
ReentrantLock支持两种锁:公平锁和非公平锁
何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO


72.ReadWriteLock是什么?(读写锁)

首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁ReadWriteLock

ReadWriteLock是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能

而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
(2)重进入:读锁和写锁都支持线程重进入
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁


73.什么是ConcurrentHashMap?

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于hashmap来说,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度

JDK1.8后,ConcurrentHashMap抛弃了原有的Segment分段锁,而采用了CAS + synchronized 来保证并发安全性


74.Java中ConcurrentHashMap的并发度是什么?

ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程情况下就能避免争用。

在JDK 8后,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS 算法。同时加入了更多的辅助变量来提高并发度


75.什么是并发容器的实现?

何为同步容器:可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行
比如:Vector、Hashtable,以及Collections.synchronizedSet、synchronizedList等方法返回的容器。可以通过查看Vector,Hashtable等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性
例如在ConcurrentHashMap中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改map,所以它可以在并发环境下实现更高的吞吐量


76.Java中的同步集合与并发集合有什么区别?

同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java 1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java 5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性


77.SynchronizedMap和ConcurrentHashMap区别?

SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map。ConcurrentHashMap使用分段锁来保证在多线程下的性能。

ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁当前需要用到的桶。

这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的

另外ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变


78.CopyOnWriteArrayList是什么,可以用于什么应用场景?有哪些优缺点?

CopyOnWriteArrayList是一个并发容器。有很多人称它是线程安全的,这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException。在CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行

CopyOnWriteArrayList使用场景
通过源码分析,可以看出它的优缺点比较明显,所以使用场景也就比较明显。就是合适读多写少的场景

CopyOnWriteArrayList缺点
(1)由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
(2)不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求
(3)由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障

CopyOnWriteArrayList设计思想
(1)读写分离,读和写分开
(2)最终一致性
(3)使用另外开辟空间的思路,来解决并发冲突


79.ThreadLocal是什么?有哪些使用场景?

ThreadLocal是一个本地线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap 对象,简单说ThreadLocal就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap对象内的value。通过这种方式,避免资源在多线程间共享

原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如web服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java应用就存在内存泄露的风险

经典的使用场景是为每个线程分配一个JDBC连接Connection。这样就可以保证每个线程的都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的 Connection;还有Session管理等问题


80.什么是线程局部变量?

局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如web服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险


81.什么是线程池?有哪几种创建方式?

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors提供了一些静态工厂方法,生成一些常用的线程池

(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行

(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能

(3)newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小

(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求


82.线程池有什么优点?

(1)降低资源消耗
重用存在的线程,减少对象创建销毁的开销

(2)提高响应速度
可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行

(3)提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

(4)附加功能
提供定时执行、定期执行、单线程、并发数控制等功能。

综上所述使用线程池框架Executor能更好的管理线程、提供系统资源使用率


83.线程池都有哪些状态?

状态描述
RUNNING最正常的状态,接受新的任务,处理等待队列中的任务
SHUTDOWN不接受新的任务提交,但是会继续处理等待队列中的任务
STOP不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程
TIDYING所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()
TERMINATEDterminated()方法结束后,线程池的状态就会变成这个

84.什么是Executor框架?为什么使用Executor框架?

Executor框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架

每次执行任务创建线程new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出

所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池


85.在Java中Executor和Executors区别?

Executors工具类的不同方法按照需求创建了不同的线程池,来满足业务的需求。

Executor接口对象能执行线程任务
ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法能获得任务执行的状态并且可以获取任务的返回值

使用ThreadPoolExecutor可以创建自定义线程池。
Future表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果


86.线程池中submit()和execute()区别?

接收参数:execute()只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务
返回值:submit()方法可以返回持有计算结果的Future对象,而execute()没有
异常处理:submit()方便Exception处理


87.Executors和ThreaPoolExecutor创建线程池的区别

《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险

Executors各个方法的弊端:
(1)newFixedThreadPool和newSingleThreadExecutor
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM

(2)newCachedThreadPool和newScheduledThreadPool
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM

ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定


88.怎么创建线程池吗?

创建线程池的方式有多种,这里只需要答ThreadPoolExecutor即可。
ThreadPoolExecutor()是最原始的线程池创建

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未禾

您的支持是我最宝贵的财富!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值