常见并发编程面试题

编程

1、在 java 中守护线程和用户线程的区别?

java

中的线程分为两种:守护线程(

Daemon

)和用户线程(

User

)。

任何线程都可以设置为守护线程和用户线程,通过方法

Thread.setDaemon(bool on)

true

则把该线程设置为守护线程,反之则为用户线

程。

Thread.setDaemon()

必须在

Thread.start()

之前调用,否则运行时会抛出异常。

两者的区别:

唯一的区别是判断虚拟机

(JVM)

何时离开,

Daemon

是为其他线程提供服务,

如果全部的

User Thread

已经结束,

Daemon

没有可服务的线程,

JVM

关闭。

扩展:

Thread Dump

打印出来的线程信息,含有

daemon

字样的线程即为守

护进程

 

2、线程与进程的区别?

进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。

一个程序至少有一个进程

,

一个进程至少有一个线程。

 

3、什么是多线程中的上下文切换?

多线程会共同使用一组计算机上的

CPU

,而线程数大于给程序分配的

CPU

数量时,为了让各个线程都有执行的机会,就需要轮转使用

CPU

。不同的线程切

换使用

CPU

发生的切换数据等就是上下文切换。

 

4、死锁与活锁的区别,死锁与饥饿的区别?

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而

造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

 

产生死锁的必要条件:

互斥条件:所谓互斥就是进程在某一时间内独占资源。

请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

不剥夺条件

:

进程已获得资源,在末使用完之前,不能强行剥夺。

循环等待条件

:

若干进程之间形成一种头尾相接的循环等待资源关系。

活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复

尝试,失败,尝试,失败。

 

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”,

而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无

法执行的状态。

 

5、synchronized 底层实现原理

synchronized (this)

原理:涉及两条指令:

monitorenter

monitorexit

;再说同

步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令

monitorenter

monitorexit

来实现,相对于普通方法,其常量池中多了

ACC_SYNCHRONIZED

标示符。

JVM

就是根据该标示符来实现方法的同步的:

当方法被调用时,调用指令将

会检查方法的

ACC_SYNCHRONIZED

访问标志是否被设置,如果设置了,执行线

程将先获取

monitor

,获取成功之后才能执行方法体,方法执行完后再释放

monitor

在方法执行期间,其他任何线程都无法再获得同一个

monitor

对象。

 

注意,这个问题可能会接着追问,java 对象头信息,偏向锁,轻量锁,重量 级锁及其他们相互间转化。

 

6、什么是线程组,为什么在 Java 中不推荐使用?

ThreadGroup

类,可以把线程归属到某一个线程组中,线程组中可以有线程

对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形

式。

1.

线程组

ThreadGroup

对象中比较有用的方法是

stop

resume

suspend

方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方

废弃掉了,所以线程组本身的应用价值就大打折扣了。

2.

线程组

ThreadGroup

不是线程安全的,这在使用过程中获取的信息并不全

是及时有效的,这就降低了它的统计使用价值。

 

7、什么是 Executors 框架?为什么使用 Executor 框架?

Executor

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

每次执行任务创建线程

new Thread()

比较消耗性能,创建一个线程是比较耗

时、耗资源的。

调用

new Thread()

创建的线程缺乏管理,而且可以无限制的创建,线程之间

的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替

也会消耗很多系统资源。

接使用

new Thread()

启动的线程不利于扩展,比如定时执行、定期执行、

定时定期执行、线程中断等都不便实现。

 

8、在 Java 中 Executor 和 Executors 的区别?

Executors

工具类的不同方法按照我们的需求创建了不同的线程池,来满足

业务的需求。

Executor

接口对象能执行我们的线程任务。

ExecutorService

接口继承了

Executor

接口并进行了扩展,提供了更多的方法

我们能获得任务执行的状态并且可以获取任务的返回值。

使用

ThreadPoolExecutor

可以创建自定义线程池。

 

9、什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic

classes)?

原子操作(

atomic operation

)意为”不可被中断的一个或一系列操作” 。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子

操作。

Java

中可以通过锁和循环 CAS 的方式来实现原子操作。

CAS

操作——

Compare & Set

,或是

Compare & Swap

,现在几乎所有的

CPU

指令都支持

CAS

原子操作。

 

java.util.concurrent.atomic

下提供了大量的原子操作类,

比如原子类:

AtomicBoolean

AtomicInteger

AtomicLong

AtomicReference

原子数组:

AtomicIntegerArray

AtomicLongArray

AtomicReferenceArray

原子属性更新器:

AtomicLongFieldUpdater

AtomicIntegerFieldUpdater

AtomicReferenceFieldUpdater

 

10、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比

synchronized 它有什么优势?

Lock

接口比同步方法和同步块提供了更具扩展性的锁操作。

他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关

类的条件对象。

它的优势有:可以使锁更公平,可以使线程在等待锁的时候响应中断,可以

让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间,可以在

不同的范围,以不同的顺序获取和释放锁。

 

整体上来说

Lock

synchronized

的扩展版,

Lock

提供了无条件的、可轮询

(tryLock

方法

)

、定时的

(tryLock

带参方法

)

、可中断的

(lockInterruptibly)

、可多条

件队列的

(newCondition

方法

)

锁操作。另外Lock 的实现类基本都支持非公平锁(默 认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁 是高效的选择。

 

11、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现

生产者-消费者模型?

阻塞队列(

BlockingQueue

)是一个支持两个附加操作的队列。

这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。

当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,

消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费

者也只从容器里拿元素。

JDK7

提供了

7

个阻塞队列。在实现上,主要是利用了

Condition

Lock

的等

待通知模式。

 

12、什么是 Callable 和 Future?

Callable

接口类似于

Runnable

,从名字就可以看出来了,但是 Runnable 不会 返回结果,并且无法抛出返回结果的异常,而 Callable

功能更强大一些,被线程

执行后,可以返回值,这个返回值可以被

Future

拿到,也就是说,

Future

可以

拿到异步执行任务的返回值。

可以认为是带有回调的

Runnable

Future

接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果

 

13、什么是 FutureTask?

Java

并发程序中

FutureTask

表示一个可以取消的异步运算。

它有启动和 取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成

get

方法将会阻塞。

一个

FutureTask

对象可以对

调用了

Callable

Runnable

的对象进行包装,由于

FutureTask

也是调用了

Runnable

接口所以它可以提交给

Executor

来执行。

 

14、什么是并发容器的实现?

何为同步容器:可以简单地理解为通过

synchronized

来实现同步的容器,如

果有多个线程调用同步容器的方法,它们将会串行执行。比如

Vector

Hashtable

以及

Collections.synchronizedSet

synchronizedList

等方法返回的容器。

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸

缩性,例如在

ConcurrentHashMap

中采用了一种粒度更细的加锁机制,可以称为

分段锁,在这种锁机制下,允许任意数量的读线程并发地访问

map

,并且执行读

操作的线程和写操作的线程也可以并发的访问

map

,同时允许一定数量的写操作

线程并发地修改

map

,所以它可以在并发环境下实现更高的吞吐量。

 

15、多线程同步和互斥有几种实现方法,都是什么?

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个

线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤

醒。

 

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当

有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,

其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以

看成是一种特殊的线程同步。

 

线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核

模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用

户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

 

用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。

内核模式下的方法有:事件,信号量,互斥量。

 

16、什么是竞争条件?

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运

行的顺序时,则我们认为这发生了竞争条件(

race condition

)。

 

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

用 run()方法?

当你调用

start()

方法时你将创建新的线程,并且执行在

run()

方法里的代码。

但是如果你直接调用

run()

方法,它不会创建新的线程也不会执行调用线程

的代码,只会把

run

方法当作普通方法去执行。

 

18、在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?

CyclicBarrier

可以重复使用,而

CountdownLatch

不能重复使用。

 

19、什么是不可变对象,它对写并发应用有什么帮助?

不可变对象

(Immutable Objects)

即对象一旦被创建它的状态(对象的数据,

也即对象属性值)就不能改变,反之即为可变对象

(Mutable Objects)

不可变对象的类即为不可变类

(Immutable Class)

Java 平台类库中包含许多 不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal

等。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。

既然它们的状态无法修改,这些常量永远不会变。

 

不可变对象永远是线程安全的。

只有满足如下状态,一个对象才是不可变的;

它的状态不能在创建后再被修改;

所有域都是

final

类型;并且,

它被正确创建

 

20、notify()和 notifyAll()有什么区别?

当一个线程进入

wait

之后,就必须等其他线程

notify/notifyall,

使用

notifyall,

可以唤醒所有处于

wait

状态的线程,使其重新进入锁的争夺队列中,而

notify

只能唤醒一个。

如果没把握,建议

notifyAll

,防止

notigy

因为信号丢失而造成程序异常。

 

21、什么是可重入锁(ReentrantLock)?谈谈它的实现。

线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock 都是可重入的锁。

 

在实现上,就是线程每次获取锁 时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每 释放一次锁, 进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。

 

22、当一个线程进入某个对象的一个 synchronized 的实例方法后,其它线程是

否可进入此对象的其它方法?

如果其他方法没有

synchronized

的话,其他线程是可以进入的。

所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。

 

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

悲观锁:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所

以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

Java

里面的同步原语 synchronized 关键字的实现是悲观锁

 

乐观锁:

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,

所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数

据,可以使用版本号等机制。在

Java

原子变量类就是使用了乐观锁的一种实

现方式 CAS 实现的

 

乐观锁的实现方式:

A、使用版本标识来确定读到的数据与提交时的数据是否一致。提交 后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

B、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程 都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以 再次尝试。

 

24、什么是 CAS 操作,缺点是什么?

CAS

的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新

值,否则不做任何事儿,但是要返回原值是多少。

每一个

CAS

操作过程都包含三

个运算符:一个内存地址

V

,一个期望的值

A

和一个新值

B

,操作的时候如果这

个地址上存放的值等于这个期望的值

A

,则将地址上的值赋为新值

B

,否则不做

任何操作。

 

ABA 问题:

比如说一个线程

one

从内存位置

V

中取出

A

,这时候另一个线程

two

也从内

存中取出

A

,并且

two

进行了一些操作变成了

B

,然后

two

又将

V

位置的数据变

A

,这时候线程

one

进行

CAS

操作发现内存中仍然是

A

,然后

one

操作成功。

尽管线程

one

CAS

操作成功,但可能存在潜藏的问题。从

Java1.5

开始

JDK

atomic

包里提供了一个类

AtomicStampedReference

来解决

ABA

问题。

 

CAS 缺点:

循环时间长开销大:

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

 

只能保证一个共享变量的原子操作:

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

 

25、SynchronizedMap 和 ConcurrentHashMap 有什么区别?

SynchronizedMap

一次锁住整张表来保证线程安全,所以每次只能有一个线

程来访为

map

ConcurrentHashMap

使用分段锁来保证在多线程下的性能。

 

26、写时复制容器可以用于什么应用场景?

CopyOnWrite

并发容器用于对于绝大部分访问都是读,且

只是偶尔写

的并发

场景。比如白名单,黑名单,商品类目的访问和更新场景。

透露的思想

读写分离,读和写分开

最终一致性

使用另外开辟空间的思路,来解决并发冲突

 

27、volatile 有什么用?能否用一句话说明下 volatile 的应用场景?

volatile

保证内存可见性禁止指令重排

volatile

用于多线程环境下的一写多读,或者无关联的多写。

 

28、为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,

但是不能随意重排序,不是你想怎么排序就怎么排序,

它需要满足以下两个条件:

A、在单线程环境下不能改变程序运行的结果;

B、存在数据依赖关系的不允许重排序

 

29、在 java 中 wait 和 sleep 方法的不同?

最大的不同是在等待时

wait

会释放锁,而

sleep

一直持有锁。

Wait

通常被用

于线程间交互,

sleep

通常被用于暂停执行。

 

30、一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。

hread.UncaughtExceptionHandler

是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。

当一个未捕获异

常将造成线程中断的时候

JVM

会使用

Thread.getUncaughtExceptionHandler()

来查

询线程的

UncaughtExceptionHandler

并将线程和异常作为参数传递给

handler

uncaughtException()

方法进行处理。

 

31、为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面?

JAVA

提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获

得。

如果线程需要等待某些锁那么调用对象中的

wait()

方法就有意义了。

如果

wait()

方法定义在

Thread

类中,线程正在等待的是哪个锁就不明显了。

简单的说,

由于

wait

notify

notifyAll

都是锁级别的操作,所以把他们定义在

Object

类中

因为锁属于对象。

 

32、什么是 ThreadLocal 变量?

ThreadLocal

Java

里一种特殊的变量。每个线程都有一个

ThreadLocal

就是

每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。

 

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

interrupted()

isInterrupted()

的主要区别是前者会将中断状态清除而后者

不会。

Java

多线程的中断机制是用内部标识来实现的,调用

Thread.interrupt()

中断一个线程就会设置中断标识为

true

当中断线程调用静态方法

Thread.interrupted()

来检查中断状态时,中断状态会被清零。

而非静态方法

isInterrupted()

用来查询其它线程的中断状态且不会改变中断状态标识。

 

34、为什么 wait 和 notify 方法要在同步块中调用?

主要是因为 Java API 强制要求这样做,如果你不这么做,你的代码会抛出

IllegalMonitorStateException

异常。

 

35、为什么你应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等

待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒

来时,不能认为它原来的等待状态仍然是有效的,在

notify()

方法调用之后和等

待线程醒来之前这段时间它可能会改变。这就是在循环中使用

wait()

方法效果更

好的原因

 

36、怎么检测一个线程是否拥有锁?

java.lang.Thread

中有一个方法叫 holdsLock(),它返回

true

如果当且仅当

当前线程拥有某个具体对象的锁。

 

37、你如何在 Java 中获取线程堆栈?

kill -3 [java pid]

不会在当前终端输出,它会输出到代码执行的或指定的地方去。比如,

kill -3

tomcat pid,

输出堆栈到

log

目录下。

Jstack [java pid]

这个比较简单,在当前终端显示,也可以重定向到指定文件中。

或者使用

Java

提供的拟机线程系统的管理接口

ManagementFactory.getThreadMXBean()

 

38、Java 线程池中 submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,

execute()

方法的返回类型是

void

,它定

义在 Executor 接口中。

submit()

方法可以返回持有计算结果的

Future

对象,它定义在ExecutorService 接口中,它扩展了 Executor 接口

 

39、你对线程优先级的理解是什么?

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优

先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的

(OS dependent)

我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的 线程前执行

线程优先级是一个

int

变量

(

1-10)

1 代表最低优先级,10 代表 最高优先级

java

的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优

先级有关,如非特别需要,一般无需设置线程优先级。

 

40、你如何确保 main()方法所在的线程是 Java 程序最后结束的线程?

可以使用

Thread

类的

join()

方法(或者

CountDownLatch

工具类)来确保所

有程序创建的线程在

main()

方法退出前结束。

 

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

Thread

类的

sleep()

yield()

方法将在当前正在执行的线程上运行。

所以在其

他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是

静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以

在其他非运行线程调用这些方法。

 

42、现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在

T2 执行完后执行?

可以用

join

方法实现。

 

43、你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,

以此来保持它的完整性,你会怎样去实现它?

volatile

关键字,读写锁,写时复制等等都可以实现。

 

44、用 Java 实现阻塞队列

 

 

45、用 Java 写代码来解决生产者——消费者问题。

阻塞队列实现即可,也可以用

wait

notify

来解决这个问题,或者用

Semaphore

 

46、用 Java 编程一个会导致死锁的程序,你将怎么解决?

 

 

47、Java 中如何停止一个线程?

使用共享变量的方式

在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任

务的线程用来作为是否中断的信号,通知中断线程的执行。

使用

interrupt

方法终止线程

 

如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?

比如当一个线程由于需要等候键盘输入而被阻塞,或者调用

Thread.join()

方法,

或者

Thread.sleep()

方法,在网络中调用

ServerSocket.accept()

方法,或者调用了

DatagramSocket.receive()

方法时,都有可能导致线程阻塞,使线程处于处于不可

运行状态时,即使主程序中将该线程的共享变量设置为

true

,但该线程此时根本

无法检查循环标志,当然也就无法立即中断。

所以应该尽量使用

Thread

提供的

interrupt()

方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使

一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。

 

48、JVM 中哪个参数是用来控制线程的栈堆栈大小的

-Xss

 

49、如果同步块内的线程抛出异常锁会释放吗?

 

50、单例模式的双重检查实现是什么?为什么并不安全?如何在 Java 中创建线

程安全的 Singleton?

不安全的根本原因是重排序会导致

未初始化完成的对象可以被其他线程看见而导致错误。

创建安全的单例模式有:

延迟占位模式、在

声明的时候就 new 这个类的实例、枚举

 

51、写出 3 条你遵循的多线程最佳实践

给你的线程起个有意义的名字。

这样可以方便找

bug

或追踪。

OrderProcessor, QuoteProcessor or TradeProcessor

这种名字比

Thread-1.

Thread-2 and Thread-3

好多了,给线程起一个和它要完成的任务相关的名字,所

有的主要框架甚至

JDK

都遵循这个最佳实践。

 

避免锁定和缩小同步的范围 锁花费的代价高昂且上下文切换更耗费时间空

间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢 同步块,它给我拥有对锁的绝对控制权。

多用同步类少用 wait 和 notify

首先,CountDownLatch, Semaphore,

CyclicBarrier

Exchanger

这些同步类简化了编码操作,而用

wait

notify

很难

实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的

JDK

中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹

灰之力获得优化。

多用并发集合少用同步集合 这是另外一个容易遵循且受益巨大的最佳实践,

并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。

比如并发编程的黄金原则,尽量无锁化编程等等

……..

 

52、请概述线程池的创建参数,怎么样合理配置一个线程池的参数?

 

53、请概述锁的公平和非公平,JDK 内部是如何实现的。

公平锁是指所有试图获得锁的线程按照获取锁的顺序依次获得锁,而非公平

锁则是当前的锁状态没有被占用时

,

当前线程可以直接占用

,

而不需要等待。在实

现上,非公平锁逻辑基本跟公平锁一致,唯一的区别是,当前线程不需要判断同

步队列中是否有等待线程。

非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真

正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用

cpu

的时间片,

尽量的减少

cpu

空闲的状态时间。

使用场景的话呢,其实还是和他们的属性一一相关,比如:如果业务中线程 占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公 平锁可以保证不会有线程被饿死。

 

54、请概述 AQS

用来构建锁或者其他同步组件的基础框架,比如 ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 就是基于

AQS

实现的。

它使用了一

int 成员变量表示同步状态,通过内置的

FIFO

队列来完成资源获取线程的排队

工作。

它是

CLH

队列锁的一种变体实现。它可以实现

2

种同步方式:独占式,共 享式

 

AQS

的主要使用方式是继承,子类通过继承

AQS

并实现它的抽象方法来管

理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步

工具类就需要覆盖其中几个可重写的方法,如

tryAcquire

tryReleaseShared

等等。

 

这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与

同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同

步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程

的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注

的领域。

 

在内部,

AQS

维护一个共享资源

state

,通过内置的

FIFO

来完成获取资源线

程的排队工作。该队列由一个一个的

Node

结点组成,每个

Node

结点维护一个

prev

引用和

next

引用,分别指向自己的前驱和后继结点,构成一个双端双向链

 

同时与

Condition

相关的等待队列,节点类型也是

Node

,构成一个单向链表

 

55、请概述 volatile

volatile

关键字的作用主要有两点:

多线程主要围绕可见性和原子性两个特性而展开,使用

volatile

关键字修饰

的变量,保证了其在多线程之间的可见性,即每次读取到

volatile

变量,一定是

最新的数据。但是

volatile

不能保证操作的原子,对任意单个

volatile

变量的读

/

写具有原子性,但类似于

++

这种复合操作不具有原子性。

 

代码底层在执行时为了获取更好的性能会对指令进行重排序,多线程下可能

会出现一些意想不到的问题。使用 volatile 则会对禁止重排序,当然这也一定程 度上降低了代码执行效率

 

同时在内存语义上,当写一个 volatile 变量时,JMM 会把该线程对应的本地 内存中的共享变量值刷新到主内存,当读一个 volatile

变量时,

JMM

会把该线程

对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

Java

中对于

volatile

修饰的变量,编译器在生成字节码时,会在指令序列

中插入内存屏障来禁止特定类型的处理器重排序问题、强制刷新和读取。

 

在具体实现上,

volatile

关键字修饰的变量会存在一个“lock:”的前缀。它

不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock 会对 CPU 总线和 高速缓存加锁,可以理解为 CPU 指令级的一种锁

 

同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内 存的操作会使在其他 CPU 里缓存了该地址的数据无效

以上是 常见并发编程面试题 的全部内容, 来源链接: utcz.com/z/516322.html

回到顶部