4)volatile 能使得一个非原子操作变成原子操作吗?
答:是的。
一个典型的例子是类中long 类型的成员变量。如果您知道成员变量将被多个线程访问,例如计数器或价格,您可能需要将其设置为volatile。为什么?在Java 中读取long 变量不是原子的,必须分为两个步骤,因此如果一个线程正在更改long 变量的值,则另一个线程正在更改long 变量的值(前32 位)。但是,读取和写入volatile long 或double 变量是原子的。
采访者:您对volatile 限定符使用了哪些实践?
回答:
一种做法是使用volatile 来修改long 和double 变量,以便它们可以作为原子类型进行读写。双精度数和长整型数都是64 位宽,因此读取这两种类型分为两部分,首先读取前32 位,然后读取剩余的32 位。这个过程不是原子的,但它确实读取和写入易失性长整型。 Java 中的Double 变量是原子的。
易失性定子的另一个功能是提供内存屏障,例如在分布式框架中。简单来说,当你写入一个易失性变量时,Java内存模型会插入一个写屏障(Write Barrier),并且在读取易失性变量之前,它会插入一个读屏障(Read Barrier)。这意味着当你写入一个易失性字段时,你可以保证你写入的值对所有线程都可见。同时,内存屏障还允许您确保值更新在写入之前对所有线程都可见。所有其他写入的值都会更新到缓存中。
5)ThreadLocal(线程局部变量)关键字:
Ans:使用ThreadLocal来管理变量,为每个使用它的线程提供了一个独立的变量副本,因此每个线程都可以创建自己的副本,而不会影响其他线程中相应的副本可以独立更改。
ThreadLocal内部实现机制:
每个线程内部维护一个类似HashMap 的对象,称为ThreadLocalMap。这个对象包含了许多条目(K-V键值对),对应的线程称为这些条目的所有者线程。
该条目的键是一个ThreadLocal 实例,其值是一个线程特定的对象。 Entry的作用是建立一个ThreadLocal实例与其主线程的线程专用对象之间的对应关系。
对条目键的引用是弱引用,对条目值的引用是强引用。
6)线程池有了解吗?(必考)
Ans:java.util.concurrent.ThreadPoolExecutor类是一个线程池。客户端通过调用ThreadPoolExecutor.submit(Runnable task)来提交任务。线程池中维护的工作线程数量就是线程池的线程池大小,有三种形式:
当前线程池大小:显示线程池中实际的工作线程数。
最大线程池大小(maxinumPoolSize):表示线程池中可以存在的工作线程数量的上限。
核心线程大小(corePoolSize):表示小于或等于最大线程池大小的工作线程数量上限。
如果运行的线程数少于corePoolSize,则执行器总是倾向于添加新线程而不是对它们进行排队。
如果超过corePoolSize 的线程正在运行,Executor 总是倾向于将请求排队,而不是添加新线程。
如果请求无法排队(即队列已满),则会创建一个新线程,除非超出了MaximumPoolSize。如果超过了maximumPoolSize,任务将被拒绝。
面试官:为什么要用线程池?
回答:
通过减少创建和销毁的线程数量,每个工作线程都可以重复使用来执行多个任务。
您可以根据系统的容量调整线程池中工作线程的数量,以防止由于内存消耗过多而造成服务器饥饿(每个线程大约需要1MB内存;打开的线程越多)(内存越多,服务器就会被饿死)。消耗),最终崩溃的可能性越大)
面试官:你了解核心线程池的内部实现吗?
答:对于核心线程池来说,无论是newFixedThreadPool()方法、newSingleThreadExecutor()还是newCachedThreadPool()方法,创建的线程在功能特征上可能看起来完全不同,但实际上都是使用ThreadPoolExecutor的内部实现。这些只是ThreadPoolExecutor类的封装。
为什么ThreadPoolExecutor这么强大呢?我们来看看ThreadPoolExecutor最重要的构造函数。
公共ThreadPoolExecutor(int corePoolSize,
int 的最大池大小,
保活时间长,
TimeUnit 单位,
BlockingQueue 工作队列,
线厂
RejectedExecutionHandler 处理程序)
函数参数的含义如下:
corePoolSize:指定线程池中的线程数。
MaximumPoolSize:指定线程池中的最大线程数。
keepAliveTime:如果线程池中的线程数量超过corePoolSize,则超出空闲线程的生存时间。也就是说,超过corePoolSize的空闲线程需要多长时间才会被销毁?
unit: KeepAlive时间单位。
workQueue:任务队列,已提交但尚未执行的任务。
threadFactory:线程工厂,用于创建线程。通常使用默认值。
处理程序:拒绝政策。当任务太多无法处理时如何拒绝。
7)Atomic关键字:
Ans:您可以启用基本数据类型以原子方式实现增量和减量等操作。参考博客:在concurrent.atomic包中使用类AtomicInteger
8)创建线程有哪几种方式?
答:创建线程有两种方法。可以通过实现Runnable接口并将其传递给Thread构造函数来创建Thread对象,也可以直接继承Thread类。
采访者:这两种方法有什么区别?
继承方法:
(1) Java类是单继承的。当从Thread继承时,该类不能有任何其他直接父类。
(2)从行为分析的角度来看,继承方式更容易,获取线程名也更容易(behavior-wise,更容易)。
(3)从多个线程共享同一资源的角度来看,继承是不可能的。
实施方法:
(1)一个Java类可以实现多个接口。这时,类还可以继承其他类,实现其他接口(设计更精巧)。
(2)从行为分析的角度来看,实现稍微复杂一些,获取线程名还需要使用Thread.currentThread()来获取当前线程的引用。
(3)从多个线程共享同一资源的角度来看,这是一种可行的实现方法(无论是否共享同一资源)。
9)run() 方法和 start() 方法有什么区别?
Ans:start()方法创建一个新线程并在该线程上执行run()方法。直接调用run() 方法只是常规方法调用,只是在当前线程上串行执行run() 。代码。
10)你怎么理解线程优先级?
Ans:Java中的线程可以有自己的优先级。优先级非常高的线程在获取资源方面有优势,更有可能获取资源。这只是一个概率问题。在不良行为中,高优先级线程也可能无法抢占。
线程优先级调度与底层操作系统紧密相关,因此它在不同平台上的行为有所不同。此外,此优先级的后果很难预测;例如,较低优先级的线程可能并不总是受到精确控制。由于它不能夺走资源,所以它总是跑不动而饿死(它的优先级低,但它不能饿死)。因此,即使在要求较高的情况下,线程调度问题也必须在应用层解决。
在Java 中,线程优先级用1 到10 表示,通常可以用三个内置静态标量表示。
公共最终静态int MIN_PRIORITY=1;
公共最终静态int NORM_PRIORITY=5;
公共最终静态int MAX_PRIORITY=10;
数字越大,优先级越高,但有效范围为1 到10,默认优先级为5。
11)在 Java 中如何停止一个线程?
Ans:Java提供了丰富的API,但没有提供停止线程的API。
JDK 1.0 最初有几个控制方法,例如stop()、suspend() 和resume(),但由于潜在的死锁威胁,它们在更高版本的JDK 中已被弃用。然后Java API设计者没有提供兼容性和线程。停止线程的安全方法。
当执行run() 或call() 方法时,线程自动终止。如果要手动终止线程,请在run()方法中使用易失性布尔变量终止循环,或者取消任务以挂起线程。
12)多线程中的忙循环是什么?
答:繁忙循环是指程序员使用循环使线程等待。 wait()、sleep() 和yield() 等传统方法都会放弃对CPU 的控制,但繁忙循环不会放弃线程。 CPU,它只是在运行一个空循环。这样做的目的是保留CPU 缓存。
在多核系统上,等待线程在启动时可能会在不同的核心上运行,这会重建缓存,避免缓存重建,并减少重建延迟。
13)10 个线程和 2 个线程的同步代码,哪个更容易写?
答:从编写代码的角度来看,同步代码的复杂度和线程数是相同的,因为它们是相互独立的。然而,同步策略的选择取决于线程的数量。更多线程意味着更多争用,需要使用锁隔离等同步技术,并且需要更复杂的代码和专业知识。
14)你是如何调用 wait()方法的?使用 if 块还是循环?为什么?
Ans:wait()方法必须在循环内调用。当线程获得CPU 并开始执行时,其他条件可能尚未满足,因此最好在继续之前循环检查条件是否满足。下面是使用等待和通知方法的标准代码。
//使用wait 方法的标准习惯用法
同步(对象){
while(条件不满足)
obj.wait(); //(释放锁并在唤醒时重新获取它)
… //根据条件执行动作
}
有关为什么应在循环内调用wait 方法的更多信息,请参阅《Effective Java》第69 项。
15)什么是多线程环境下的伪共享(false sharing)?
答:虚假共享是多线程系统中众所周知的性能问题(每个处理器都有自己的本地缓存)。当不同处理器上的线程修改依赖于同一缓存行的变量时,就会发生错误共享,如下图所示。
错误共享问题很难检测,因为线程可能访问内存中位置非常接近的完全不同的全局变量。与许多其他并发问题一样,避免错误共享的最基本方法是仔细检查代码并根据缓存行调整数据结构。
16)用 wait-notify 写一段代码来解决生产者-消费者问题?
分析:这是一道经常考的基本题型。如果被阻塞,不要忘记通过循环测试等待状态。
回答:
导入java.util.Vector。
导入java.util.logging.Level;
导入java.util.logging.Logger。
/**
使用等待和通知解决生产者-消费者问题的Java程序
Java的Producer Consumer方法是另一种常见的并发设计模式。
@作者贾文·保罗
*/
公共类ProducerConsumerSolution {
公共静态无效主(字符串参数[]){
向量共享队列=new Vector();
整数大小=4;
线程prodThread=新线程(新生产者(sharedQueue, 大小), “生产者”);
线程consThread=new Thread(new Consumer(sharedQueue, size), “Consumer”);
prodThread.start();
consThread.start();
}
}
类生产者实现Runnable {
私有最终向量sharedQueue;
私有最终int SIZE;
publicProducer(VectorsharedQueue,int大小){
this.sharedQueue=共享队列;
this.SIZE=大小;
}
@覆盖
公共无效运行(){
for (int i=0; i 7; i++) {
System.out.println(\’Produced:\’ + i);
尝试{
生成(一);
} catch (InterruptedException ex) {
Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
私有无效生成(int i)抛出InterruptedException {
//如果队列已满则等待
while (sharedQueue.size()==SIZE) {
已同步(共享队列){
System.out.println(\’队列已满\’ + Thread.currentThread().getName()
\’ 等待,size: \’ +sharedQueue.size());
共享队列.wait();
}
}
//生成元素并通知消费者
已同步(共享队列){
共享队列.add(i);
共享队列.notifyAll();
}
}
}
类Consumer 实现Runnable {
私有最终向量sharedQueue;
私有最终int SIZE;
公共消费者(VectorsharedQueue,int大小){
this.sharedQueue=共享队列;
this.SIZE=大小;
}
@覆盖
公共无效运行(){
而(真){
尝试{
System.out.println(\’Consumed:\’ + Consumer());
线程睡眠(50);
} catch (InterruptedException ex) {
Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
私有int Consumer() 抛出InterruptedException {
//如果队列为空则等待
while (sharedQueue.isEmpty()) {
已同步(共享队列){
System.out.println(\’队列为空\’ + Thread.currentThread().getName()
\’ 等待,size: \’ +sharedQueue.size());
共享队列.wait();
}
}
//否则消耗该元素并通知等待的生产者
已同步(共享队列){
共享队列.notifyAll();
返回(整数)sharedQueue.remove(0);
}
}
}
输出:
生产: 0
队列为空消费者正在等待,size: 0
生产: 1
消费: 0
生产: 2
生产: 3
生产: 4
生产: 5
队列已满生产者等待,大小: 4
消费: 1
生产: 6
队列已满生产者等待,大小: 4
消费: 2
消费: 3
消费: 4
消费: 5
消费: 6
队列为空消费者正在等待,size: 0
17)用 Java 写一个线程安全的单例模式(Singleton)?
分析:实现这一点的方法有很多,但关键是掌握双重认证锁。
回答:
1. 饥饿的中国单身人士
饥饿式单例意味着在调用方法之前创建实例。实现代码如下。
公共类单例{
私有静态单例实例=new Singleton();
私有单例(){}
公共静态单例getInstance() {
返回实例。
}
}
2.添加同步惰性单例
所谓延迟单例模式,是在调用时创建这个实例,并在外部实例创建方法中添加synchronized关键字,使其在多线程中能够正常运行。
公共类单例{
私有静态单例实例。
私有单例(){}
公共静态同步单例getInstance() {
如果(实例==空){
实例=新单例();
}
返回实例。
}
}
3.使用静态内部类创建单例
该方法利用类加载器机制来保证初始化实例时只有一个线程。该方法与饿汉式的区别在于,只要加载了饿汉式的Singleton类,就会实例化一个实例。没有达到延迟加载的效果)。这样,Singleton 类就被加载了,而实例不一定被初始化。仅当通过调用getInstance() 方法显式加载SingletonHoder 类时,才会实例化单例。
公共类单例{
私有单例(){
}
private static class SingletonHolder {//静态内部类
私有静态单例单例=new Singleton();
}
公共静态单例getInstance() {
返回SingletonHolder.singleton。
}
}
4.双重认证锁
您可以使用DCL的双重检查锁定机制来实现这一点,以实现线程安全并提高代码执行效率。代码实现如下:
公共类单例{
私有静态单例单例。
私有单例(){
}
公共静态单例getInstance(){
if (单例==null) {
同步(Singleton.class){
if (单例==null) {
Singleton=new Singleton();
}
}
}
返回一个单例。
}
}
这个方法使用双重测试来创建单例,但是为什么要使用两个if来判断对象当前是否为空呢?如果多个线程想要同时创建对象怎么办,这不是单例模式,因为多个线程可能会停止在第一个if 决策点并在多个线程创建对象之前等待锁被释放。为了判断这个对象是否存在,我们需要使用两个if。
5.使用静态代码块实现单例
由于静态代码块中的代码在使用类时已经被执行,因此可以应用静态代码块的这一特性来实现单例设计模式。
公共类单例{
私有静态单例实例=null;
私有单例(){}
静止的{
实例=新单例();
}
公共静态单例getInstance() {
返回实例。
}
}
6.使用枚举数据类型实现单例模式
枚举类型enum 具有与静态代码块类似的特征。使用枚举时,还可以使用此功能来实现单例。
总结
面试一般都有套路:一是基础面试,二是结构化面试,三是个人面试。
最后,我们的编辑在这里收集并总结了一些信息,包括采访问题(包括答案)。
、书籍、视频等。希望也能帮助想进大厂的朋友
}
return singleton;
}
}
这种是用双重判断来创建一个单例的方法,那么我们为什么要使用两个if判断这个对象当前是不是空的呢 ?因为当有多个线程同时要创建对象的时候,多个线程有可能都停止在第一个if判断的地方,等待锁的释放,然后多个线程就都创建了对象,这样就不是单例模式了,所以我们要用两个if来进行这个对象是否存在的判断。
5.使用 static 代码块实现单例
静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性的实现单例设计模式。
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
static{
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
}
6.使用枚举数据类型实现单例模式
枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例:
总结
总的来说,面试是有套路的,一面基础,二面架构,三面个人。
最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友
[外链图片转存中…(img-rU5cTAEN-1719483090418)]
[外链图片转存中…(img-4L8oVZvH-1719483090419)]
#以上关于2024最新一线互联网大厂常见高并发面试题解析的相关内容来源网络仅供参考,相关信息请以官方公告为准!
原创文章,作者:CSDN,如若转载,请注明出处:https://www.sudun.com/ask/92607.html