《2021最新Java面试题全集-2021年第二版》不断更新完善!

    

第三章 多线程

1:并行和并发有什么区别?

·       并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。

·       并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。

·       在一台处理器上同时处理多个任务,在多台处理器上同时处理多个任务。

所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

 

2:线程和进程的区别?

进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。

进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。

线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行,线程之间共享一块内存区域,通信比较方便,线程的入口执行顺序这些过程被应用程序所控制。

 

3:守护线程是什么?

守护线程(即daemon thread),是个服务线程,持续运行以接受服务请求,来为其它线程提供服务

 

4:Java中如何调度线程?

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

所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。

在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指:按照特定机制为多个线程分配 CPU 的使用权.

有两种调度模型:分时调度模型和抢占式调度模型。

分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占 用的 CPU 的时间片这个也比较好理解。

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

 

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

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

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

不可变对象天生是线程安全的。它们的常量()是在构造函数中创建的。既然 它们的状态无法修改,这些常量永远不会变。

不可变对象永远是线程安全的。只有满足如下状态,一个对象才是不可变的:

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

2)所有域都是 final 类型

3)它被正确创建(创建期间没有发生 this 引用的逸出)

 

6:什么是多线程中的上下文切换?

在上下文切换过程中,CPU 会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。

从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的页码信息是保存在进程控制块(PCB)中的。PCB 还经 常被称作切换桢”(switchframe)页码信息会一直保存到 CPU 的内存 中,直到他们被再次使用。

上下文切换是存储和恢复 CPU 状态的过程,它使得线程执行能够从中断点恢复执 行。上下文切换是多任务操作系统和多线程环境的基本特征。

 

7:Hotspot JVM中运行的系统线程有?

Hotspot JVM 后台运行的系统线程主要有下面几个:

虚拟机线程
VM thread

这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有: stop-theworld 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。

周期性任务线程

这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。

GC 线程

这些线程支持 JVM 中不同的垃圾回收活动。

编译器线程

这些线程在运行时将字节码动态编译成本地平台相关的机器码。

信号分发线程

这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

 

 

8:创建线程有哪几种方式?

1)继承Thread类创建线程类

·       定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

·       创建Thread子类的实例,即创建了线程对象。

·       调用线程对象的start()方法来启动该线程。

 

2)实现Runnable接口创建线程类

·       定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

·       创建 Runnable实现类的实例,并依此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象。

·       调用线程对象的start()方法来启动该线程。

 

3)通过CallableFuture创建线程

·       创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

·       创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

·       使用FutureTask对象作为Thread对象的target创建并启动新线程。

·       调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

 

9:说一下 runnable callable 有什么区别?

·       Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;

·       Callable接口中的call()方法是有返回值的,是一个泛型,和FutureFutureTask配合可以用来获取异步执行的结果

 

10:线程有哪些状态?

http://blogphoto-1251192294.costj.myqcloud.com/YouDaoNote/%E7%BA%BF%E7%A8%8B%E5%87%A0%E7%A7%8D%E7%8A%B6%E6%80%81.png

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

·       创建状态。在生成线程对象,并没有调用该对象的start方法,这时线程处于创建状态。

·       就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

·       运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。

·       阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep,suspendwait等方法都可以导致线程阻塞。阻塞的情况分三种:

1)等待阻塞:运行( running )的线程执行 o.wait ()方法,JVM会把该线程放入等待队列( waitting queue )中。

2)同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。

3)其他阻塞: 运行( running )的线程执行Thread.sleep ( long ms ) t.join ()方法,或者发出了 I/O 请求时,JVM会把该线程置为阻塞状态。当 sleep ()状态超时、join()等待线程终止或者超时、或者I/O 处理完毕时,线程重新转入可运行( runnable )状态

·       死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪  

 

11:在监视器(Monitor)内部,是如何做线程同步的?

监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。

每一个监视器都和一个对象引用相关联,线程在获取锁之前不允许执行同步代码。

 

12:sleep() wait() 有什么区别?

sleep()方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。

因为sleep() static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait()Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notifynotifyAll方法来唤醒等待的线程

 

13:线程的sleep()方法和yield()方法有什么区别?

1sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

2)线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;

3sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;

4sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

14:notify() notifyAll()有什么区别?

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

·       当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。

·       优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

 

15:为什么 wait, notify notifyAll 这些方法不在 thread 类里面?

一个原因是 JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。

由于 waitnotify notifyAll 都是锁级别的操作,所以把他 们定义在 Object 类中因为锁属于对象。

另外一个是:Java 的每个对象中都有一个锁(monitor,也可以成为监视器) 并且 wait()notify() 等方法用于等待对象的锁,或者通知其他线程对象的监视器可用。

Java 的线程中 并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object 类的一 部分,这样 Java 的每一个类都有用于线程间通信的基本方法。

 

 

16:为什么 wait(), notify() notifyAll ()必须在同步方法或 者同步块中被调用?

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify() 方法。

同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。

由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

 

17:线程的 run() start()有什么区别?

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

通过调用Thread类的start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态,并没有运行。

然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

 

 

18:什么是CAS

CASCompare and Swap)即比较交换。

CAS是一种基于锁的操作,而且是乐观锁。当多个线程尝试使用 CAS 同时更新 同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作 中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A) 和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。

可以用CAS来实现乐观锁,在java.util.concurrent.atomic 包下面的原子变量类就是使用了CAS 来实现乐观锁的。

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

19:CAS存在的问题?

1ABA 问题:

比如说一个线程 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 就无法保证操作的原子性,这个时候就可以用锁。

4CAS 造成 CPU 利用率增加

CAS 里面是一个循环判断的过程,如果线程一直没有获取到状态,CPU资源会一直被占用。

 

20:Executor Executors 的区别?

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

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

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

使用 ThreadPoolExecutor 可以创建自定义线程池。

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

 

21:创建线程池有哪几种方式?

1newFixedThreadPool(int nThreads)

创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

2newCachedThreadPool()

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

3newSingleThreadExecutor()

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。

4newScheduledThreadPool(int corePoolSize)

创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer

 

22:线程池都有哪些状态?

线程池有5种状态:RunningShutDownStopTidyingTerminated

线程池各个状态切换框架图

 

23:线程池中 submit() execute()方法有什么区别?

·       接收的参数不一样

·       submit有返回值,而execute没有

·       submit方便Exception处理

 

24: Java 程序中怎么保证多线程的运行安全?

线程安全在三个方面体现:

·       原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);

·       可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);

·       有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

25:多线程锁的升级原理是什么?

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

锁升级的图示过程: 

 

26:什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象.

若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

 

27:怎么防止死锁?

死锁的四个必要条件:

·       互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

·       请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放

·       不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

·       环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

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

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。

此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

 

28:什么是活锁?

活锁指的是:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

 

29:什么是饥饿

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

Java 中导致饥饿的原因:

1:高优先级线程吞噬所有的低优先级线程的 CPU 时间。

2:线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

3:线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait ),因为其他线程总是被持续地获得唤醒。

 

30:ThreadLocal 是什么?有哪些使用场景?

线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。

但是在某些环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

 

31:ThreadLocal有什么缺陷?如果是线程池里的线程用ThreadLocal会有什么问题?

Threadlocal 实现原理是:map<thread,threadlocalmap<>>可以避免出现资源竞争从而导致效率低下。

Threadlocalmap<threadlocal,T> (自定义实现,没map),它的entry继承了弱引用 WeakReference,当threadlocal不再引用时且JVM进行GC时就会回收对应的entry,同时当threadlocal.set(value)thread local.get()remove时,都会回收不再使用的entry

缺陷:Threadlocal线程还存活,但是引用了大的对象,该对象无法被清除,会引起OOMclosablethreadlocal解决了该问题。

线程池里用Threadlocal要注意线程会被复用,当线程结束时,记得清除该Thradlocal的数据,避免用了上一个线程的数据。

 

 

32:说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

    JVM中,是通过进入和退出Monitor对象来实现方法同步和代码块同步的:

方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor

代码块的同步是利用monitorentermonitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当JVM执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

1)普通同步方法,锁是当前实例对象

2)静态同步方法,锁是当前类的class对象

3)同步方法块,锁是括号里面的对象

 

33:synchronized volatile 的区别是什么?

·       volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

·       volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。

·       volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。

·       volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

·       volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

 

34:synchronized Lock 有什么区别?

·       首先synchronizedJava内置关键字,在JVM层面,Lock是个Java类;

·       synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

·       synchronized会自动释放锁(a 线程执行完同步代码会释放锁 b 线程执行过程中发生异常会释放锁)Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

·       synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

·       synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);

·       Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

 

35:synchronized ReentrantLock 区别是什么?

synchronizedJava的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLocksynchronized的扩展性体现在几点上: 

·       ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁 

·       ReentrantLock可以获取各种锁的信息

·       ReentrantLock可以灵活地实现多路通知 

另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafepark方法加锁,synchronized操作的应该是对象头中mark word

 

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

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

当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当 有其他线程需要读取时,它会去内存中读取新值。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详 细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger

volatile 用于多线程环境下的单次操作(单次读或者单次写)

 

37:volatile 类型变量提供什么保证?

volatile 变量提供顺序和可见性保证。

JVM 或者 JIT 为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。

volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。

某些情况下,volatile 还能提供原子性,如读 64 数据类型,像 long double 都不是原子的,但 volatile类型的doublelong就是原子的。

 

38:为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

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

2)存在数据依赖关系的不允许重排序

需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

 

39:说一下 atomic 的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。

原子类:AtomicBooleanAtomicIntegerAtomicLongAtomicReference 原子数组:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray 原子属性更新器:AtomicLongFieldUpdaterAtomicIntegerFieldUpdater AtomicReferenceFieldUpdater

解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean 来反映中间有没有变过)AtomicStampedReference(通过引入一个 int 来累 加来反映中间有没有变过)

 

40:什么是 AQS

AQS AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广 泛的大量的同步器,比如:ReentrantLockSemaphore ReentrantReadWriteLockSynchronousQueueFutureTask 等。

 

41:AQS定义两种资源共享方式

1Exclusive(独占):

只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

1)公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

2)非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

2Share(共享):

多个线程可同时执行,如Semaphore/CountDownLatch

ReentrantReadWriteLock可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁,允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了

 

42:CycliBarriar CountdownLatch 有什么区别?

CyclicBarrier 可以重复使用,而 CountdownLatch不能重复使用。

Java concurrent 包里面的 CountDownLatch 其实可以把它看作一个计数器, 只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器, 也就是同时只能有一个线程去减这个计数器里面的值。

你可以向 CountDownLatch 对象设置一个初始的数字作为计数值,任何调用这个 对象上的 await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 止。

所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待 的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法 被重置。如果需要重置计数,请考虑使用 CyclicBarrier

CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏 障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程 必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待 线程后可以重用,所以称它为循环 barrier

 

43:ConcurrentHashMap 的并发度是什么

ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着最 多同时可以有 16 条线程操作 ConcurrentHashMap,这也是 ConcurrentHashMap Hashtable 的最大优势。