阿里P7大牛亲自教你!【Java面试题总结 3

7、Thread 类中的 yield 方法有什么做用?java


yield()应该作的是让当前运行线程回到可运行状态,以容许具备相同优先级的其余线程得到运行机会。所以,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。可是,实际中没法保证yield()达到让步目的,由于让步的线程还有可能被线程调度程序再次选中。git

结论:yield()从未致使线程转到等待/睡眠/阻塞状态。在大多数状况下,yield()将致使线程从运行状态转到可运行状态,但有可能没有效果。面试

8、为何说 Synchronized 是非公平锁?缓存


当锁被释放后,任何一个线程都有机会竞争获得锁,这样作的目的是提升效率,但缺点是可能产生线程饥饿现象。安全

9、请谈谈 volatile 有什么特色,为何它能保证变量对全部线程的可见性?数据结构


volatile只能做用于变量,保证了操做可见性和有序性,不保证原子性。并发

在Java的内存模型中分为主内存和工做内存,Java内存模型规定全部的变量存储在主内存中,每条线程都有本身的工做内存。app

主内存和工做内存之间的交互分为8个原子操做:框架

  1. lockide

  2. unlock

  3. read

  4. load

  5. assign

  6. use

  7. store

  8. write

volatile修饰的变量,只有对volatile进行assign操做,才能够load,只有load才能够use,,这样就保证了在工做内存操做volatile变量,都会同步到主内存中。

10、为何说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?


Synchronized的并发策略是悲观的,无论是否产生竞争,任何数据的操做都必须加锁。

乐观锁的核心是CAS,CAS包括内存值、预期值、新值,只有当内存值等于预期值时,才会将内存值修改成新值。

11、乐观锁必定就是好的吗?


乐观锁认为对一个对象的操做不会引起冲突,因此每次操做都不进行加锁,只是在最后提交更改时验证是否发生冲突,若是冲突则再试一遍,直至成功为止,这个尝试的过程称为自旋。

乐观锁没有加锁,但乐观锁引入了ABA问题,此时通常采用版本号进行控制;

也可能产生自旋次数过多问题,此时并不能提升效率,反而不如直接加锁的效率高;

只能保证一个对象的原子性,能够封装成对象,再进行CAS操做;

12、请尽量详尽地对比下 Synchronized 和 ReentrantLock 的异同。


一、类似点

它们都是阻塞式的同步,也就是说一个线程得到了对象锁,进入代码块,其它访问该同步块的线程都必须阻塞在同步代码块外面等待,而进行线程阻塞和唤醒的代码是比较高的。

二、功能区别

Synchronized是java语言的关键字,是原生语法层面的互斥,须要JVM实现;ReentrantLock 是JDK1.5以后提供的API层面的互斥锁,须要lock和unlock()方法配合try/finally代码块来完成。

Synchronized使用较ReentrantLock 便利一些;

锁的细粒度和灵活性:ReentrantLock强于Synchronized;

三、性能区别

Synchronized引入偏向锁,自旋锁以后,二者的性能差很少,在这种状况下,官方建议使用Synchronized。

(1)Synchronized

Synchronized会在同步块的先后分别造成monitorenter和monitorexit两个字节码指令。

在执行monitorenter指令时,首先要尝试获取对象锁。若是这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计数器+1,相应的执行monitorexit时,计数器-1,当计数器为0时,锁就会被释放。若是获取锁失败,当前线程就要阻塞,知道对象锁被另外一个线程释放为止。

(2)ReentrantLock

ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下三项:

等待可中断,持有锁的线程长期不释放的时候,正在等待的线程能够选择放弃等待,这至关于Synchronized避免出现死锁的状况。经过lock.lockInterruptibly()来实现这一机制;

公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序得到锁,Synchronized锁是非公平锁;ReentrantLock默认也是非公平锁,能够经过参数true设为公平锁,但公平锁表现的性能不是很好;

锁绑定多个条件,一个ReentrantLock对象能够同时绑定多个对象。ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒须要唤醒的线程们,而不是像Synchronized要么随机唤醒一个线程,要么唤醒所有线程。

十3、ReentrantLock 是如何实现可重入性的?


一、什么是可重入性

一个线程持有锁时,当其余线程尝试获取该锁时,会被阻塞;而这个线程尝试获取本身持有锁时,若是成功说明该锁是可重入的,反之则不可重入。

二、synchronized是如何实现可重入性

synchronized关键字通过编译后,会在同步块的先后分别造成monitorenter和monitorexit两个字节码指令。每一个锁对象内部维护一个计数器,该计数器初始值为0,表示任何线程均可以获取该锁并执行相应的方法。根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁,若是这个对象没有被锁定,或者当前线程已经拥有了对象的锁,把锁的计数器+1,相应的在执行monitorexit指令后锁计数器-1,当计数器为0时,锁就被释放。若是获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

三、ReentrantLock如何实现可重入性

ReentrantLock使用内部类Sync来管理锁,因此真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公公平锁)和FairSync(公平锁)。Sync经过继承AQS实现,在AQS中维护了一个private volatile int state来计算重入次数,避免频繁的持有释放操做带来的线程问题。

四、ReentrantLock代码实例

// Sync继承于AQS

abstract static class Sync extends AbstractQueuedSynchronizer {

  ...

}

// ReentrantLock默认是非公平锁

public ReentrantLock() {

        sync = new NonfairSync();

 }

// 能够经过向构造方法中传true来实现公平锁

public ReentrantLock(boolean fair) {

    sync = fair ? new FairSync() : new NonfairSync();

}

protected final boolean tryAcquire(int acquires) {

// 当前想要获取锁的线程

    final Thread current = Thread.currentThread();

    // 当前锁的状态

    int c = getState();

    // state == 0 此时此刻没有线程持有锁

    if (c == 0) {

        // 虽然此时此刻锁是能够用的,可是这是公平锁,既然是公平,就得讲究先来后到,

        // 看看有没有别人在队列中等了半天了

        if (!hasQueuedPredecessors() &&

            // 若是没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,

            // 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=

            // 由于刚刚还没人的,我判断过了

            compareAndSetState(0, acquires)) {



            // 到这里就是获取到锁了,标记一下,告诉你们,如今是我占用了锁

            setExclusiveOwnerThread(current);

            return true;

        }

    }

      // 会进入这个else if分支,说明是重入了,须要操做:state=state+1

    // 这里不存在并发问题

    else if (current == getExclusiveOwnerThread()) {

        int nextc = c + acquires;

        if (nextc < 0)

            throw new Error("Maximum lock count exceeded");

        setState(nextc);

        return true;

    }

    // 若是到这里,说明前面的if和else if都没有返回true,说明没有获取到锁

    return false;

}
?五、代码分析



当一个线程在获取锁过程当中,先判断state的值是否为0,若是是表示没有线程持有锁,就能够尝试获取锁。  

当state的值不为0时,表示锁已经被一个线程占用了,这时会作一个判断current==getExclusiveOwnerThread(),这个方法返回的是当前持有锁的线程,这个判断是看当前持有锁的线程是否是本身,若是是本身,那么将state的值+1,表示重入返回便可。



十4、什么是锁消除和锁粗化?

--------------



一、锁消除



所消除就是虚拟机根据一个对象是否真正存在同步状况,若不存在同步状况,则对该对象的访问无需通过加锁解锁的操做。



好比StringBuffer的append方法,由于append方法须要判断对象是否被占用,而若是代码不存在锁竞争,那么这部分的性能消耗是无心义的。因而虚拟机在即时编译的时候就会将上面的代码进行优化,也就是锁消除。

@Override

public synchronized StringBuffer append(String str) {

toStringCache = null;

super.append(str);

return this;

}

从源码能够看出,append方法用了?synchronized关键字,它是线程安全的。但咱们可能仅在线程内部把StringBuffer当作局部变量使用;StringBuffer仅在方法内做用域有效,不存在线程安全的问题,这时咱们能够经过编译器将其优化,将锁消除,前提是Java必须运行在server模式,同时必须开启逃逸分析;

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。

public static String createStringBuffer(String str1, String str2) {

    StringBuffer sBuf = new StringBuffer();

    sBuf.append(str1);// append方法是同步操做

    sBuf.append(str2);

    return sBuf.toString();

}

```



逃逸分析:好比上面的代码,它要看sBuf是否可能逃出它的做用域?若是将sBuf做为方法的返回值进行返回,那么它在方法外部可能被看成一个全局对象使用,就有可能发生线程安全问题,这时就能够说sBuf这个对象发生逃逸了,于是不该将append操做的锁消除,但咱们上面的代码没有发生锁逃逸,锁消除就能够带来必定的性能提高。?



二、锁粗化



锁的请求、同步、释放都会消耗必定的系统资源,若是高频的锁请求反而不利于系统性能的优化,锁粗化就是把屡次的锁请求合并成一个请求,扩大锁的范围,下降锁请求、同步、释放带来的性能损耗。



十5、跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不一样?

---------------------------------------------------



一、都是可重入锁;



二、ReentrantLock内部是实现了Sync,Sync继承于AQS抽象类。Sync有两个实现,一个是公平锁,一个是非公平锁,经过构造函数定义。AQS中维护了一个state来计算重入次数,避免频繁的持有释放操做带来的线程问题。



三、ReentrantLock只能定义代码块,而Synchronized能够定义方法和代码块;



四、Synchronized是JVM的一个内部关键字,ReentrantLock是JDK1.5以后引入的一个API层面的互斥锁;



五、Synchronized实现自动的加锁、释放锁,ReentrantLock须要手动加锁和释放锁,中间能够暂停;



六、Synchronized因为引进了偏向锁和自旋锁,因此性能上和ReentrantLock差很少,但操做上方便不少,因此优先使用Synchronized。



[](https://gitee.com/vip204888/java-p7)十6、那么请谈谈 AQS 框架是怎么回事儿?

-------------------------------------------------------------------------------------



一、AQS是AbstractQueuedSynchronizer的缩写,它提供了一个FIFO队列,能够当作是一个实现同步锁的核心组件。



AQS是一个抽象类,主要经过继承的方式来使用,它自己没有实现任何的同步接口,仅仅是定义了同步状态的获取和释放的方法来提供自定义的同步组件。



二、AQS的两种功能:独占锁和共享锁



三、AQS的内部实现



AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,若是当前线程竞争失败,那么AQS会把当前线程以及等待状态信息构形成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁之后,会从队列中唤醒一个阻塞的节点(线程)。



![](https://img-blog.csdnimg.cn/20210529205800577.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2d1b3J1aV9qYXZh,size_16,color_FFFFFF,t_70)



AQS队列内部维护的是一个FIFO的双向链表,这种结构的特色是每一个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。因此双向链表能够从任意一个节点开始很方便的范文前驱和后继节点。每一个Node实际上是由线程封装,当线程争抢锁失败后会封装成Node加入到AQS队列中。



[](https://gitee.com/vip204888/java-p7)十7、AQS 对资源的共享方式?

-------------------------------------------------------------------------------



AQS定义两种资源共享方式



*   Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

    

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

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

*   **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 咱们都会在后面讲到。

    



ReentrantReadWriteLock 能够当作是组合式,由于ReentrantReadWriteLock也就是读写锁容许多个线程同时对某一资源进行读。



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



[](https://gitee.com/vip204888/java-p7)十8、如何让 Java 的线程彼此同步?

-----------------------------------------------------------------------------------



1.  synchronized

2.  volatile

3.  ReenreantLock

4.  使用局部变量实现线程同步

    



[](https://gitee.com/vip204888/java-p7)十9、你了解过哪些同步器?请分别介绍下。

-----------------------------------------------------------------------------------



一、Semaphore同步器



特征:



经典的信号量,经过计数器控制对共享资源的访问  

Semaphore(int count):建立拥有count个许可证的信号量  

acquire()/acquire(int num) : 获取1/num个许可证  

release/release(int num) : 释放1/num个许可证



二、CountDownLatch同步器



特征:



必须发生指定数量的事件后才能够继续运行(好比赛跑比赛,裁判喊出3,2,1以后你们才同时跑)  

CountDownLatch(int count):必须发生count个数量才能够打开锁存器  

await:等待锁存器  

countDown:触发事件



三、CyclicBarrier同步器



特征:



适用于只有多个线程都到达预约点时才能够继续执行(好比斗地主,须要等齐三我的才开始)  

CyclicBarrier(int num) :等待线程的数量  

CyclicBarrier(int num, Runnable action) :等待线程的数量以及全部线程到达后的操做  

await() : 到达临界点后暂停线程



四、交换器(Exchanger)同步器



五、Phaser同步器



[](https://gitee.com/vip204888/java-p7)二10、Java 中的线程池是如何实现的

----------------------------------------------------------------------------------



建立一个阻塞队列来容纳任务,在第一次执行任务时建立足够多的线程,并处理任务,以后每一个工做线程自动从任务队列中获取线程,直到任务队列中任务为0为止,此时线程处于等待状态,一旦有工做任务加入任务队列中,即刻唤醒工做线程进行处理,实现线程的可复用性。



线程池通常包括四个基本组成部分:



一、线程池管理器



用于建立线程池,销毁线程池,添加新任务。



二、工做线程



线程池中线程,可循环执行任务,在没有任务时处于等待状态。



三、任务队列



用于存放没有处理的任务,一种缓存机制。



四、任务接口



每一个任务必须实现的接口,供工做线程调度任务的执行,主要规定了任务的开始和收尾工做,和任务的状态。



二11、建立线程池的几个核心构造参数

------------------



```

// Java线程池的完整构造函数

public ThreadPoolExecutor(

  int corePoolSize, // 线程池长期维持的最小线程数,即便线程处于Idle状态,也不会回收。

  int maximumPoolSize, // 线程数的上限

  long keepAliveTime, // 线程最大生命周期。

  TimeUnit unit, //时间单位                                 

  BlockingQueue<Runnable> workQueue, //任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增长,则须要一个容器来容纳这些任务,这就是任务队列。

  ThreadFactory threadFactory, // 线程工厂。定义如何启动一个线程,能够设置线程名称,而且能够确认是不是后台线程等。

  RejectedExecutionHandler handler // 拒绝任务处理器。因为超出线程数量和队列容量而对继续增长的任务进行处理的程序。

)

```



二12、线程池中的线程是怎么建立的?是一开始就随着线程池的启动建立好的吗?

-------------------------------------



线程池中的线程是在第一次提交任务submit时建立的



建立线程的方式有继承Thread和实现Runnable,重写run方法,start开始执行,wait等待,sleep休眠,shutdown中止。



一、newSingleThreadExecutor:单线程池。



顾名思义就是一个池中只有一个线程在运行,该线程永不超时,并且因为是一个线程,当有多个任务须要处理时,会将它们放置到一个无界阻塞队列中逐个处理,它的实现代码以下:



```

public static ExecutorService newSingleThreadExecutor() {

    return new FinalizableDelegatedExecutorService

            (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,

             new LinkedBlockingQueue<Runnable()));

}

```



它的使用方法也很简单,下面是简单的示例:



```

public static void main(String[] args) throws ExecutionException,InterruptedException {

    // 建立单线程执行器

    ExecutorService es = Executors.newSingleThreadExecutor();

    // 执行一个任务

    Future<String> future = es.submit(new Callable<String>() {

        @Override

        public String call() throws Exception {

            return "";

        }

    });

    // 得到任务执行后的返回值

    System.out.println("返回值:" + future.get());

    // 关闭执行器

    es.shutdown();

}

```



二、newCachedThreadPool:缓冲功能的线程。



创建了一个线程池,并且线程数量是没有限制的(固然,不能超过Integer的最大值),新增一个任务即有一个线程处理,或者复用以前空闲的线程,或者重亲启动一个线程,可是一旦一个线程在60秒内一直处于等待状态时(也就是一分钟无事可作),则会被终止,其源码以下: 




### 最后

**因为篇幅限制,小编在此截出几张知识讲解的图解,有须要的程序猿(媛)能够点赞后[戳这里免费领取所有资料](https://gitee.com/vip204888/java-p7)获取哦**

![P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌](https://upload-images.jianshu.io/upload_images/24616006-fc1cbc1ca7186f03?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌](https://upload-images.jianshu.io/upload_images/24616006-7d9a9d4b2a67dcad?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌](https://upload-images.jianshu.io/upload_images/24616006-c054c80f9d5697ae?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌](https://upload-images.jianshu.io/upload_images/24616006-b04277920e45fb1a?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌](https://upload-images.jianshu.io/upload_images/24616006-81e94a8066f94cae?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
相关文章
相关标签/搜索