乐趣区

关于java:2021最新一线互联网大厂常见高并发面试题解析

(一)高并发编程基础知识

这里波及到一些根底的概念,我从新捧起了一下《实战 Java 高并发程序设计》这一本书,感觉到心潮澎湃,这或者就是笔者叙述功底扎实的魅力吧,喜爱。

作为浏览福利,我把大厂常见高并发面试题以及知识点整顿成了 pdf 文档,当初收费分享给浏览到本篇文章的 Java 程序员敌人们,须要的点击下方链接支付!!

最全学习笔记大厂真题 + 微服务 +MySQL+ 分布式 +SSM 框架 +Java+Redis+ 数据结构与算法 + 网络 +Linux+Spring 全家桶 +JVM+ 高并发 + 各大学习思维脑图 + 面试汇合

1)多线程和单线程的区别和分割?

答:

  1. 在单核 CPU 中,将 CPU 分为很小的工夫片,在每一时刻只能有一个线程在执行,是一种宏观上轮流占用 CPU 的机制。
  2. 多线程会存在线程上下文切换,会导致程序执行速度变慢,即采纳一个领有两个线程的过程执行所须要的工夫比一个线程的过程执行两次所须要的工夫要多一些。

论断:即采纳多线程不会进步程序的执行速度,反而会升高速度,然而对于用户来说,能够缩小用户的响应工夫。

面试官:那应用多线程有什么劣势?

解析:只管面临很多挑战,多线程有一些长处依然使得它始终被应用,而这些长处咱们应该理解。

答:

(1)资源利用率更好

设想一下,一个应用程序须要从本地文件系统中读取和解决文件的情景。比方说,从磁盘读取一个文件须要 5 秒,解决一个文件须要 2 秒。解决两个文件则须要:

1| 5 秒读取文件 A
2| 2 秒解决文件 A
3| 5 秒读取文件 B
4| 2 秒解决文件 B
5| ---------------------
6| 总共须要 14 秒

从磁盘中读取文件的时候,大部分的 CPU 工夫用于期待磁盘去读取数据。在这段时间里,CPU 十分的闲暇。它能够做一些别的事件。通过扭转操作的程序,就可能更好的应用 CPU 资源。看上面的程序:

1| 5 秒读取文件 A
2| 5 秒读取文件 B + 2 秒解决文件 A
3| 2 秒解决文件 B
4| ---------------------
5| 总共须要 12 秒

CPU 期待第一个文件被读取完。而后开始读取第二个文件。当第二文件在被读取的时候,CPU 会去解决第一个文件。记住,在期待磁盘读取文件的时候,CPU 大部分工夫是闲暇的。

总的说来,CPU 可能在期待 IO 的时候做一些其余的事件。这个不肯定就是磁盘 IO。它也能够是网络的 IO,或者用户输出。通常状况下,网络和磁盘的 IO 比 CPU 和内存的 IO 慢的多。

(2)程序设计在某些状况下更简略

在单线程应用程序中,如果你想编写程序手动解决下面所提到的读取和解决的程序,你必须记录每个文件读取和解决的状态。相同,你能够启动两个线程,每个线程解决一个文件的读取和操作。线程会在期待磁盘读取文件的过程中被阻塞。在期待的时候,其余的线程可能应用 CPU 去解决曾经读取完的文件。其后果就是,磁盘总是在忙碌地读取不同的文件到内存中。这会带来磁盘和 CPU 利用率的晋升。而且每个线程只须要记录一个文件,因而这种形式也很容易编程实现。

(3)程序响应更快

有时咱们会编写一些较为简单的代码(这里的简单不是说简单的算法,而是简单的业务逻辑),例如,一笔订单的创立,它包含插入订单数据、生成订单赶快找、发送邮件告诉卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要期待这些操作全副实现能力看到订购胜利的后果。然而这么多业务操作,如何可能让其更快地实现呢?

在下面的场景中,能够应用多线程技术,行将数据一致性不强的操作派发给其余线程解决(也能够应用音讯队列),如生成订单快照、发送邮件等。这样做的益处是响应用户申请的线程可能尽可能快地解决实现,缩短了响应工夫,晋升了用户体验。

多线程还有一些劣势也不言而喻:
① 过程之前不能共享内存,而线程之间共享内存 (堆内存) 则很简略。
② 零碎创立过程时须要为该过程重新分配系统资源, 创立线程则代价小很多, 因而实现多任务并发时, 多线程效率更高.
③ Java 语言自身内置多线程性能的反对, 而不是单纯第作为底层零碎的调度形式, 从而简化了多线程编程.

2)多线程肯定快吗?

答:不肯定。

比方,咱们尝试应用并行和串行来别离执行累加的操作察看是否并行执行肯定比串行执行更快:

以下是我测试的后果,能够看出,当不超过 1 百万的时候,并行是显著比串行要慢的,为什么并发执行的速度会比串行慢呢?这是因为线程有创立和上下文切换的开销。

3)什么是同步?什么又是异步?

解析:这是对多线程基础知识的考查

答:同步和异步通常用来形容一次办法调用。

同步办法调用一旦开始,调用者必须等到办法返回后,能力持续后续的行为。这就如同是咱们去商城买一台空调,你看中了一台空调,于是就跟售货员下了单,而后售货员就去仓库帮你调配物品,这天你热的切实不行,就催着商家连忙发货,于是你就在商店里等着,晓得商家把你和空调都送回家,一次欢快的购物才完结,这就是同步调用。

而异步办法更像是一个消息传递,一旦开始,办法调用就会立刻返回,调用者就能够持续后续的操作。回到方才买空调的例子,咱们能够坐在里关上电脑,在网上订购一台空调。当你实现网上支付的时候,对你来说购物过程曾经完结了。尽管空调还没有送到家,然而你的工作都曾经实现了。商家接到你的订单后,就会加紧安顿送货,当然这所有曾经跟你无关了,你曾经领取实现,想什么就能去干什么了,进来溜达几圈都不成问题。等送货上门的时候,接到商家电话,回家一趟签收即可。这就是异步调用。

面试官:那并发(Concurrency)和并行(Parallelism)的区别呢?

解析:并行性和并发性是既类似又有区别的两个概念。

答:并行性是指两个或多个事件在同一时刻产生。而并发性是指连个或多个事件在同一时间距离内产生。

在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机环境下(一个处理器),每一时刻却仅能有一道程序执行,故宏观上这些程序只能是分时地交替执行。例如,在 1 秒钟工夫内,0-15ms 程序 A 运行;15-30ms 程序 B 运行;30-45ms 程序 C 运行;45-60ms 程序 D 运行,因而能够说,在 1 秒钟工夫距离内,宏观上有四道程序在同时运行,但宏观上,程序 A、B、C、D 是分时地交替执行的。

如果在计算机系统中有多个处理机,这些能够并发执行的程序就能够被调配到多个处理机上,实现并发执行,即利用每个处理机解决一个可并发执行的程序。这样,多个程序便能够同时执行。以此就能进步零碎中的资源利用率,减少零碎的吞吐量。

4)线程和过程的区别:(必考)

答:

  1. 过程是一个“执行中的程序”,是零碎进行资源分配和调度的一个独立单位;
  2. 线程是过程的一个实体,一个过程中领有多个线程,线程之间共享地址空间和其它资源(所以通信和同步等操作线程比过程更加容易);
  3. 线程上下文的切换比过程上下文切换要快很多。

    • (1)过程切换时,波及到以后过程的 CPU 环境的保留和新被调度运行过程的 CPU 环境的设置。
    • (2)线程切换仅须要保留和设置大量的寄存器内容,不波及存储管理方面的操作。

面试官:过程间如何通信?线程间如何通信?

答:过程间通信依附 IPC 资源,例如管道(pipes)、套接字(sockets)等;

线程间通信依附 JVM 提供的 API,例如 wait()、notify()、notifyAll() 等办法,线程间还能够通过共享的主内存来进行值的传递。

5)什么是阻塞(Blocking)和非阻塞(Non-Blocking)?

答:阻塞和非阻塞通常用来形容多线程间的相互影响。比方一个线程占用了临界区资源,那么其余所有须要这个而资源的线程就必须在这个临界区中进行期待。期待会导致线程挂起,这种状况就是阻塞。此时,如果占用资源的线程始终不违心开释资源,那么其余所有阻塞在这个临界区上的线程都不能工作。

非阻塞的意思与之相同,它强调没有一个线程能够障碍其余线程执行。所有的线程都会尝试一直前向执行。

面试官:临界区是什么?

答:临界区用来示意一种公共资源或者说是共享资源,能够被多个线程应用。然而每一次,只能有一个线程应用它,一旦临界区资源被占用,其余线程要想应用这个资源,就必须期待。

比方,在一个办公室里有一台打印机,打印机一次只能执行一个工作。如果小王和小明同时须要打印文件,很显然,如果小王先下发了打印工作,打印机就开始打印小王的文件了,小明的工作就只能期待小王打印完结后能力打印,这里的打印机就是一个临界区的例子。

在并行程序中,临界区资源是爱护的对象,如果意外呈现打印机同时执行两个打印工作,那么最可能的后果就是打印进去的文件就会是损坏的文件,它既不是小王想要的,也不是小明想要的。

6)什么是死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)?

答:死锁、饥饿和活锁都属于多线程的活跃性问题,如果发现上述几种状况,那么相干线程可能就不再沉闷,也就说它可能很难再持续往下执行了。

  1. 死锁应该是最蹩脚的一种状况了,它示意两个或者两个以上的过程在执行过程中,因为竞争资源或者因为彼此通信而造成的一种阻塞的景象,若无外力作用,它们都将无奈推动上来。此时称零碎处于死锁状态或零碎产生了死锁,这些永远在相互期待的过程称为死锁过程。
  2. 饥饿是指某一个或者多个线程因为种种原因无奈取得所须要的资源,导致始终无奈执行。比方:
    1)它的线程优先级可能太低,而高优先级的线程一直抢占它须要的资源,导致低优先级的线程无奈工作。在自然界中,母鸡喂食雏鸟时,很容易呈现这种状况,因为雏鸟很多,食物无限,雏鸟之间的食物竞争可能十分厉害,小雏鸟因为常常抢不到食物,有可能会被饿死。线程的饥饿也十分相似这种状况。
    2)另外一种可能是,某一个线程始终占着要害资源不放,导致其余须要这个资源的线程无奈失常执行,这种状况也是饥饿的一种。
    与死锁相比,饥饿还是有可能在将来一段时间内解决的(比方高优先级的线程曾经实现工作,不再疯狂的执行)
  3. 活锁是一种十分乏味的状况。不晓得大家是不是有遇到过这样一种状况,当你要坐电梯下楼,电梯到了,门开了,这时你正筹备进来,但不巧的是,门外一个人挡着你的来路,他想进来。于是你很绅士的靠左走,避让对方,但同时对方也很绅士,但他靠右走心愿避让你。后果,你们又撞上了。于是乎,你们都意识到了问题,心愿尽快避让对方,你立刻向右走,他也立刻向左走,后果又撞上了!不过介于人类的只能,我置信这个动作反复 2、3 次后,你应该能够顺利解决这个问题,因为这个时候,大家都会本能的对视,进行交换,保障这种状况不再产生。
    但如果这种状况产生在两个线程间可能就不会那么侥幸了,如果线程的智力不够,且都秉承着“谦让”的准则,被动将资源开释给别人应用,那么就会呈现资源一直在两个线程中跳动,而没有一个线程能够同时拿到所有的资源而失常执行。这种状况就是活锁。

7)多线程产生死锁的 4 个必要条件?

答:

  1. 互斥条件:一个资源每次只能被一个线程应用;
  2. 申请与放弃条件:一个线程因申请资源而阻塞时,对已取得的资源放弃不放;
  3. 不剥夺条件:过程曾经取得的资源,在未应用完之前,不能强行剥夺;
  4. 循环期待条件:若干线程之间造成一种头尾相接的循环期待资源关系。

面试官:如何防止死锁?(常常接着问这个问题哦~)

答:指定获取锁的程序,举例如下:

  1. 比方某个线程只有取得 A 锁和 B 锁能力对某资源进行操作,在多线程条件下,如何防止死锁?
  2. 取得锁的程序是肯定的,比方规定,只有取得 A 锁的线程才有资格获取 B 锁,按程序获取锁就能够防止死锁!!!

8)如何指定多个线程的执行程序?

解析:面试官会给你举个例子,如何让 10 个线程依照程序打印 0123456789?(写代码实现)

答:

  1. 设定一个 orderNum,每个线程执行完结之后,更新 orderNum,指明下一个要执行的线程。并且唤醒所有的期待线程。
  2. 在每一个线程的开始,要 while 判断 orderNum 是否等于本人的要求值!!不是,则 wait,是则执行本线程。

9)Java 中线程有几种状态?

答:六种(查看 Java 源码也能够看到是 6 种),并且某个时刻 Java 线程只能处于其中的一个状态。

  1. 新建(NEW)状态:示意新创建了一个线程对象,而此时线程并没有开始执行。
  2. 可运行(RUNNABLE)状态:线程对象创立后,其它线程(比方 main 线程)调用了该对象的 start() 办法,才示意线程开始执行。当线程执行时,处于 RUNNBALE 状态,示意线程所需的所有资源都曾经筹备好了。该状态的线程位于可运行线程池中,期待被线程调度选中,获取 cpu 的使用权。
  3. 阻塞(BLOCKED)状态:如果线程在执行过程终于到了 synchronized 同步块,就会进入 BLOCKED 阻塞状态,这时线程就会暂停执行,直到取得申请的锁。
  4. 期待(WAITING)状态:当线程期待另一个线程告诉调度器一个条件时,它本人进入期待状态。在调用 Object.wait 办法或 Thread.join 办法,或者是期待 java.util.concurrent 库中的 Lock 或 Condition 时,就会呈现这种状况;
  5. 计时期待(TIMED_WAITING)状态:Object.wait、Thread.join、Lock.tryLock 和 Condition.await 等办法有超时参数,还有 Thread.sleep 办法、LockSupport.parkNanos 办法和 LockSupport.parkUntil 办法,这些办法会导致线程进入计时期待状态,如果超时或者呈现告诉,都会切换会可运行状态;
  6. 终止(TERMINATED)状态:当线程执行结束,则进入该状态,示意完结。

留神:从 NEW 状态登程后,线程不能再回到 NEW 状态,同理,处于 TERMINATED 状态的线程也不能再回到 RUNNABLE 状态。


(二)高并发编程 -JUC 包

在 Java 5.0 提供了 java.util.concurrent(简称 JUC)包,在此包中减少了在并发编程中很罕用的实用工具类,用于定义相似于线程的自定义子系统,包含线程池、异步 IO 和轻量级工作框架。

1)sleep() 和 wait( n)、wait() 的区别:

答:

  1. sleep 办法:是 Thread 类的静态方法,以后线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠工夫到了,会解除阻塞,进行可运行状态,期待 CPU 的到来。睡眠不开释锁(如果有的话);
  2. wait 办法:是 Object 的办法,必须与 synchronized 关键字一起应用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,会解除阻塞。然而,只有从新占用互斥锁之后才会进入可运行状态。睡眠时,开释互斥锁。

2)synchronized 关键字:

答:底层实现:

  1. 进入时,执行 monitorenter,将计数器 +1,开释锁 monitorexit 时,计数器 -1;
  2. 当一个线程判断到计数器为 0 时,则以后锁闲暇,能够占用;反之,以后线程进入期待状态。

含意:(monitor 机制)

Synchronized 是在加锁,加对象锁。对象锁是一种分量锁(monitor),synchronized 的锁机制会依据线程竞争状况在运行时会有偏差锁(繁多线程)、轻量锁(多个线程拜访 synchronized 区域)、对象锁(分量锁,多个线程存在竞争的状况)、自旋锁等。

该关键字是一个几种锁的封装。

3)volatile 关键字:

答:该关键字能够保障可见性不保障原子性。

性能:

  1. 主内存和工作内存,间接与主内存产生交互,进行读写操作,保障可见性;
  2. 禁止 JVM 进行的指令重排序。

解析:对于指令重排序的问题,能够查阅 DCL 双检锁生效相干材料。

4)volatile 能使得一个非原子操作变成原子操作吗?

答:能。

一个典型的例子是在类中有一个 long 类型的成员变量。如果你晓得该成员变量会被多个线程拜访,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,须要分成两步,如果一个线程正在批改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。然而对一个 volatile 型的 long 或 double 变量的读写是原子。

面试官:volatile 修饰符的有过什么实际?

答:

  1. 一种实际是用 volatile 润饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是 64 位宽,因而对这两种类型的读是分为两局部的,第一次读取第一个 32 位,而后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。
  2. volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的利用。简略的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保障任何线程都能看到你写的值,同时,在写之前,也能保障任何数值的更新对所有线程是可见的,因为内存屏障会将其余所有写的值更新到缓存。

5)ThreadLocal(线程局部变量)关键字:

答:当应用 ThreadLocal 保护变量时,其为每个应用该变量的线程提供独立的变量正本,所以每一个线程都能够独立的扭转本人的正本,而不会影响其余线程对应的正本。

ThreadLocal 外部实现机制:

  1. 每个线程外部都会保护一个相似 HashMap 的对象,称为 ThreadLocalMap,里边会蕴含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
  2. Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建设起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
  3. Entry 对 Key 的援用是弱援用;Entry 对 Value 的援用是强援用。

6)线程池有理解吗?(必考)

答:java.util.concurrent.ThreadPoolExecutor 类就是一个线程池。客户端调用 ThreadPoolExecutor.submit(Runnable task) 提交工作,线程池外部保护的工作者线程的数量就是该线程池的线程池大小,有 3 种状态:

  • 以后线程池大小:示意线程池中理论工作者线程的数量;
  • 最大线程池大小(maxinumPoolSize):示意线程池中容许存在的工作者线程的数量下限;
  • 外围线程大小(corePoolSize):示意一个不大于最大线程池大小的工作者线程数量下限。
  1. 如果运行的线程少于 corePoolSize,则 Executor 始终首选增加新的线程,而不进行排队;
  2. 如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将申请退出队列,而不是增加新线程;
  3. 如果无奈将申请退出队列,即队列曾经满了,则创立新的线程,除非创立此线程超出 maxinumPoolSize,在这种状况下,工作将被回绝。

面试官:咱们为什么要应用线程池?

答:

  1. 缩小创立和销毁线程的次数,每个工作线程都能够被反复利用,可执行多个工作。
  2. 能够依据零碎的承受能力,调整线程池中工作线程的数目,搁置因为耗费过多的内存,而把服务器累趴下(每个线程大概须要 1 MB 内存,线程开的越多,耗费的内存也就越大,最初死机)

面试官:外围线程池外部实现理解吗?

答:对于外围的几个线程池,无论是 newFixedThreadPool() 办法,newSingleThreadExecutor() 还是 newCachedThreadPool() 办法,尽管看起来创立的线程有着齐全不同的性能特点,但其实外部实现均应用了 ThreadPoolExecutor 实现,其实都只是 ThreadPoolExecutor 类的封装。

为何 ThreadPoolExecutor 有如此弱小的性能呢?咱们能够来看一下 ThreadPoolExecutor 最重要的构造函数:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

函数的参数含意如下:

  • corePoolSize:指定了线程池中的线程数量
  • maximumPoolSize:指定了线程池中的最大线程数量
  • keepAliveTime:当线程池线程数量超过 corePoolSize 时,多余的闲暇线程的存活工夫。即,超过了 corePoolSize 的闲暇线程,在多长时间内,会被销毁。
  • unit: keepAliveTime 的单位。
  • workQueue:工作队列,被提交但尚未被执行的工作。
  • threadFactory:线程工厂,用于创立线程,个别用默认的即可。
  • handler:回绝策略。当工作太多来不及解决,如何回绝工作。

7)Atomic 关键字:

答:能够使根本数据类型以原子的形式实现自增自减等操作。参考博客:concurrent.atomic 包下的类 AtomicInteger 的应用

8)创立线程有哪几种形式?

答:有两种创立线程的办法:一是实现 Runnable 接口,而后将它传递给 Thread 的构造函数,创立一个 Thread 对象; 二是间接继承 Thread 类。

面试官:两种形式有什么区别呢?

  1. 继承形式:

    • (1)Java 中类是单继承的, 如果继承了 Thread 了, 该类就不能再有其余的间接父类了.
    • (2)从操作上剖析, 继承形式更简略, 获取线程名字也简略.(操作上, 更简略)
    • (3)从多线程共享同一个资源上剖析, 继承形式不能做到.
  2. 实现形式:

    • (1)Java 中类能够多实现接口, 此时该类还能够继承其余类, 并且还能够实现其余接口(设计上, 更优雅).
    • (2)从操作上剖析, 实现形式略微简单点, 获取线程名字也比较复杂, 得应用 Thread.currentThread()来获取以后线程的援用.
    • (3)从多线程共享同一个资源上剖析, 实现形式能够做到(是否共享同一个资源).

9)run() 办法和 start() 办法有什么区别?

答:start() 办法会新建一个线程并让这个线程执行 run() 办法;而间接调用 run() 办法常识作为一个一般的办法调用而已,它只会在以后线程中,串行执行 run() 中的代码。

10)你怎么了解线程优先级?

答:Java 中的线程能够有本人的优先级。优先极高的线程在竞争资源时会更有劣势,更可能抢占资源,当然,这只是一个概率问题。如果运行不好,高优先级线程可能也会抢占失败。

因为线程的优先级调度和底层操作系统有亲密的关系,在各个平台上体现不一,并且这种优先级产生的结果也可能不容易预测,无奈精准管制,比方一个低优先级的线程可能始终抢占不到资源,从而始终无奈运行,而产生饥饿(尽管优先级低,然而也不能饿死它啊)。因而,在要求严格的场合,还是须要本人在应用层解决线程调度的问题。

在 Java 中,应用 1 到 10 示意线程优先级,个别能够应用内置的三个动态标量示意:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

数字越大则优先级越高,但无效范畴在 1 到 10 之间,默认的优先级为 5。

11)在 Java 中如何进行一个线程?

答:Java 提供了很丰盛的 API 但没有为进行线程提供 API。

JDK 1.0 原本有一些像 stop(),suspend() 和 resume() 的管制办法然而因为潜在的死锁威逼因而在后续的 JDK 版本中他们被弃用了,之后 Java API 的设计者就没有提供一个兼容且线程平安的办法来进行任何一个线程。

当 run() 或者 call() 办法执行完的时候线程会主动完结,如果要手动完结一个线程,你能够用 volatile 布尔变量来退出 run() 办法的循环或者是勾销工作来中断线程。

12)多线程中的忙循环是什么?

答:忙循环就是程序员用循环让一个线程期待,不像传统办法 wait(),sleep() 或 yield() 它们都放弃了 CPU 控制权,而忙循环不会放弃 CPU,它就是在运行一个空循环。这么做的目标是为了保留 CPU 缓存。

在多核零碎中,一个期待线程醒来的时候可能会在另一个内核运行,这样会重建缓存,为了防止重建缓存和缩小期待重建的工夫就能够应用它了。

13)10 个线程和 2 个线程的同步代码,哪个更容易写?

答:从写代码的角度来说,两者的复杂度是雷同的,因为同步代码与线程数量是互相独立的。然而同步策略的抉择依赖于线程的数量,因为越多的线程意味着更大的竞争,所以你须要利用同步技术,如锁拆散,这要求更简单的代码和专业知识。

14)你是如何调用 wait()办法的?应用 if 块还是循环?为什么?

答:wait() 办法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其余条件可能还没有满足,所以在解决前,循环检测条件是否满足会更好。上面是一段规范的应用 wait 和 notify 办法的代码:

// The standard idiom for using the wait method
synchronized (obj) {while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}

参见 Effective Java 第 69 条,获取更多对于为什么应该在循环中来调用 wait 办法的内容。

15)什么是多线程环境下的伪共享(false sharing)?

答:伪共享是多线程零碎(每个处理器有本人的部分缓存)中一个家喻户晓的性能问题。伪共享产生在不同处理器的上的线程对变量的批改依赖于雷同的缓存行,如下图所示:

伪共享问题很难被发现,因为线程可能拜访齐全不同的全局变量,内存中却碰巧在很相近的地位上。如其余诸多的并发问题,防止伪共享的最根本形式是认真审查代码,依据缓存行来调整你的数据结构。

16)用 wait-notify 写一段代码来解决生产者 - 消费者问题?

解析:这是常考的根底类型的题,只有记住在同步块中调用 wait() 和 notify()办法,如果阻塞,通过循环来测试期待条件。

答:

import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Java program to solve Producer Consumer problem using wait and notify
 * method in Java. Producer Consumer is also a popular concurrency design pattern.
 *
 * @author Javin Paul
 */
public class ProducerConsumerSolution {public static void main(String args[]) {Vector sharedQueue = new Vector();
        int size = 4;
        Thread prodThread = new Thread(new Producer(sharedQueue, size), "Producer");
        Thread consThread = new Thread(new Consumer(sharedQueue, size), "Consumer");
        prodThread.start();
        consThread.start();}
}

class Producer implements Runnable {

    private final Vector sharedQueue;
    private final int SIZE;

    public Producer(Vector sharedQueue, int size) {
        this.sharedQueue = sharedQueue;
        this.SIZE = size;
    }

    @Override
    public void run() {for (int i = 0; i < 7; i++) {System.out.println("Produced:" + i);
            try {produce(i);
            } catch (InterruptedException ex) {Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
    }

    private void produce(int i) throws InterruptedException {

        // wait if queue is full
        while (sharedQueue.size() == SIZE) {synchronized (sharedQueue) {System.out.println("Queue is full" + Thread.currentThread().getName()
                                    + "is waiting , size:" + sharedQueue.size());

                sharedQueue.wait();}
        }

        // producing element and notify consumers
        synchronized (sharedQueue) {sharedQueue.add(i);
            sharedQueue.notifyAll();}
    }
}

class Consumer implements Runnable {

    private final Vector sharedQueue;
    private final int SIZE;

    public Consumer(Vector sharedQueue, int size) {
        this.sharedQueue = sharedQueue;
        this.SIZE = size;
    }

    @Override
    public void run() {while (true) {
            try {System.out.println("Consumed:" + consume());
                Thread.sleep(50);
            } catch (InterruptedException ex) {Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
    }

    private int consume() throws InterruptedException {
        // wait if queue is empty
        while (sharedQueue.isEmpty()) {synchronized (sharedQueue) {System.out.println("Queue is empty" + Thread.currentThread().getName()
                                    + "is waiting , size:" + sharedQueue.size());

                sharedQueue.wait();}
        }

        // Otherwise consume element and notify waiting producer
        synchronized (sharedQueue) {sharedQueue.notifyAll();
            return (Integer) sharedQueue.remove(0);
        }
    }
}

Output:
Produced: 0
Queue is empty Consumer is waiting , size: 0
Produced: 1
Consumed: 0
Produced: 2
Produced: 3
Produced: 4
Produced: 5
Queue is full Producer is waiting , size: 4
Consumed: 1
Produced: 6
Queue is full Producer is waiting , size: 4
Consumed: 2
Consumed: 3
Consumed: 4
Consumed: 5
Consumed: 6
Queue is empty Consumer is waiting , size: 0

17)用 Java 写一个线程平安的单例模式(Singleton)?

解析:有多种办法,但重点把握的是双重校验锁。

答:

1. 饿汉式单例

饿汉式单例是指在办法调用前,实例就曾经创立好了。上面是实现代码:

public class Singleton {private static Singleton instance = new Singleton();

    private Singleton (){}

    public static Singleton getInstance() {return instance;}
}

2. 退出 synchronized 的懒汉式单例

所谓懒汉式单例模式就是在调用的时候才去创立这个实例,咱们在对外的创立实例办法上加如 synchronized 关键字保障其在多线程中很好的工作:

public class Singleton {    

    private static Singleton instance;    

    private Singleton (){}    

    public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();    
    }    
    return instance;    
    }    
}  

3. 应用动态外部类的形式创立单例

这种形式利用了 classloder 的机制来保障初始化 instance 时只有一个线程,它跟饿汉式的区别是:饿汉式只有 Singleton 类被加载了,那么 instance 就会被实例化(没有达到 lazy loading 的成果),而这种形式是 Singleton 类被加载了,instance 不肯定被初始化。只有显式通过调用 getInstance() 办法时才会显式装载 SingletonHoder 类,从而实例化 singleton

public class Singleton {private Singleton() { }

    private static class SingletonHolder {// 动态外部类  
        private static Singleton singleton = new Singleton();}

    public static Singleton getInstance() {return SingletonHolder.singleton;}
}

4. 双重校验锁

为了达到线程平安,又能进步代码执行效率,咱们这里能够采纳 DCL 的双查看锁机制来实现,代码实现如下:

public class Singleton {  
  
    private static Singleton singleton;  

    private Singleton() {}  

    public static Singleton getInstance(){if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();  
                }  
            }  
        }  
        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 和动态代码块的个性类似,在应用枚举时,构造方法会被主动调用,利用这一个性也能够实现单例:

public class ClassFactory{   
      
    private enum MyEnumSingleton{  
        singletonFactory;  
          
        private MySingleton instance;  
          
        private MyEnumSingleton(){// 枚举类的构造方法在类加载是被实例化  
            instance = new MySingleton();}  
   
        public MySingleton getInstance(){return instance;}  
    }   
   
    public static MySingleton getInstance(){return MyEnumSingleton.singletonFactory.getInstance();  
    }  
}  

小结:对于 Java 中多线程编程,线程平安等常识始终都是面试中的重点和难点,还须要熟练掌握。

退出移动版