Java并发进阶面试题总结

今天继续来看看有关Java多线程的高阶面试题。java

  • synchronized关键字
  • volatile关键字
  • ThreadLocal
  • 线程池
  • 阻塞队列
  • Atomic 原子类
  • AQS

synchronized关键字

说一说本身对于synchronized关键字的了解

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字能够保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。git

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,由于监视器锁(monitor)是依赖于底层的操做系统的 Mutex Lock 来实现的。操做系统实现线程之间的切换时须要从用户态转换到内核态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高,这也是为何早期的 synchronized 效率低的缘由。庆幸的是在 Java 6 以后 Java 官方对从 JVM 层面对synchronized 较大优化,因此如今的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减小锁操做的开销。github

怎么使用synchronized关键字

synchronized关键字最主要的三种使用方式:面试

  • 修饰实例方法:做用于当前对象实例加锁,进入同步代码前要得到当前对象实例的锁
  • 修饰静态方法:也就是给当前类加锁,会做用于类的全部对象实例,由于静态成员不属于任何一个实例对象,是类成员( static 代表这是该类的一个静态资源,无论new了多少个对象,只有一份)。因此若是一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B须要调用这个实例对象所属类的静态 synchronized 方法,是容许的,不会发生互斥现象,由于访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
  • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要得到给定对象的锁。

讲一下synchronized关键字的底层原理

一、synchronized 同步语句块的状况segmentfault

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

反编译后缓存

1.png

从上面咱们能够看出:多线程

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每一个Java对象的对象头中,synchronized 锁即是经过这种方式获取锁的,也是为何Java中任意对象能够做为锁的缘由) 的持有权。当计数器为0则能够成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,代表锁被释放。若是获取对象锁失败,那当前线程就要阻塞等待,直到锁被另一个线程释放为止。并发

二、synchronized 修饰方法的的状况框架

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

反编译后dom

2.png

synchronized 修饰的方法并无 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 经过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

说说JDK1.6以后的synchronized关键字底层作了哪些优化?

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减小锁操做的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁能够升级不可降级,这种策略是为了提升得到锁和释放锁的效率。

有关这些锁的详细知识能够参考:死磕synchronized底层原理

谈谈synchronized和ReentrantLock的区别

一、二者都是可重入锁
二、synchronized依赖于JVM 而ReentrantLock依赖于 API
三、相比synchronized,ReentrantLock增长了一些高级功能。
主要来讲主要有三点: ①等待可中断;②可实现公平锁;③可实现选择性通知

volatile关键字

说说你对volatile关键字的理解

就我理解的而言,被volatile修饰的共享变量,就具备了如下两点特性:

  1. 保证了不一样线程对该变量操做的内存可见性;
  2. 禁止指令重排序

谈谈volatile底层的实现机制

下面这段话摘自《深刻理解Java虚拟机》:
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。
lock前缀指令实际上至关于一个内存屏障,内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;
  • 它会强制将对缓存的修改操做当即写入主存;
  • 若是是写操做,它会致使其余CPU中对应的缓存行无效。

说说synchronized关键字和volatile关键字的区别

  • volatile关键字是线程同步的轻量级实现,因此volatile性能确定比synchronized关键字要好。可是volatile关键字只能用于变量,而synchronized关键字能够修饰方法以及代码块。synchronized关键字在JavaSE1.6以后进行了主要包括为了减小得到锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各类优化以后执行效率有了显著提高,实际开发中使用 synchronized 关键字的场景仍是更多一些
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字二者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

有关volatile的更多知识请移步:浅谈volatile关键字

ThreadLocal

ThreadLocal是怎么为每一个线程建立副本的?

首先,在每一个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,key值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当经过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,而且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。而后在当前线程里面,若是要使用副本变量,就能够经过get方法在threadLocals里面查找。

想看源码解析的请移步:初始ThreadLocal

ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。因此,若是 ThreadLocal 没有被外部强引用的状况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如咱们不作任何措施的话,value 永远没法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种状况,在调用set()、get()、remove()方法的时候,会清理掉key为null的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

static class Entry extends WeakReference<ThreadLocal<?>> {          
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
       }

弱引用介绍:

若是一个对象只具备弱引用,那就相似于无关紧要的生活用品。弱引用与软引用的区别在于:只具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程当中, 一旦发现了只具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程, 所以不必定会很快发现那些只具备弱引用的对象。

线程池

为何要使用线程池?

  • 下降资源消耗。经过重复利用已建立的线程下降线程建立和销毁形成的消耗。
  • 提升响应速度。当任务到达时,任务能够不须要等到线程建立就能当即执行。
  • 提升线程的可管理性。线程是稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一的分配,调优和监控。

执行execute()方法和submit()方法的区别是什么呢?

  1. execute()方法用于提交不须要返回值的任务,因此没法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交须要返回值的任务。线程池会返回一个Future类型的对象,经过这个Future对象能够判断任务是否执行成功,而且能够经过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后当即返回,这时候有可能任务没有执行完。

阻塞队列

写一个生产者-消费者队列

可使用阻塞队列或wait/notify,这里使用阻塞队列来实现

//消费者
public class Producer implements Runnable{
   private final BlockingQueue<Integer> queue;

   public Producer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       try {
           while (true){
               Thread.sleep(1000);//模拟耗时
               queue.put(produce());
           }
       }catch (InterruptedException e){

       }
   }

   private int produce() {
       int n=new Random().nextInt(10000);
       System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n);
       return n;
   }
}
//消费者
public class Consumer implements Runnable {
   private final BlockingQueue<Integer> queue;

   public Consumer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       while (true){
           try {
               Thread.sleep(2000);//模拟耗时
               consume(queue.take());
           }catch (InterruptedException e){

           }

       }
   }

   private void consume(Integer n) {
       System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n);

   }
}
//测试
public class Main {

   public static void main(String[] args) {
       BlockingQueue<Integer> queue=new ArrayBlockingQueue<Integer>(100);
       Producer p=new Producer(queue);
       Consumer c1=new Consumer(queue);
       Consumer c2=new Consumer(queue);

       new Thread(p).start();
       new Thread(c1).start();
       new Thread(c2).start();
   }
}

Atomic 原子类

介绍一下Atomic 原子类

Atomic翻译成中文是原子的意思。在这里Atomic是指一个操做是不可中断的。即便是在多个线程一块儿执行的时候,一个操做一旦开始,就不会被其余线程干扰。

因此,所谓原子类说简单点就是具备原子/原子操做特征的类。

并发包java.util.concurrent的原子类都存放在java.util.concurrent.atomic下,以下图所示。

1.png

讲讲AtomicInteger的使用

AtomicInteger 类经常使用方法

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //若是输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置以后可能致使其余线程在以后的一小段时间内仍是能够读到旧的值。

简单介绍一下AtomicInteger类的原理

AtomicInteger 类主要利用 CAS + volatilenative 方法来保证原子操做,从而避免 synchronized 的高开销,执行效率大为提高。

AQS

谈谈AQS

AQS的全称为(AbstractQueuedSynchronizer),AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用普遍的大量的同步器,好比咱们提到的ReentrantLock,Semaphore,其余的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

AQS原理分析

AQS核心思想是,若是被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工做线程,而且将共享资源设置为锁定状态。若是被请求的共享资源被占用,那么就须要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

1.png

AQS使用一个int成员变量state来表示同步状态,经过内置的FIFO队列来完成获取资源线程的排队工做。AQS使用CAS对该同步状态进行原子操做实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息经过protected类型的getState,setState,compareAndSetState进行操做

//返回同步状态的当前值
protected final int getState() {  
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操做)将同步状态值设置为给定值update若是当前同步状态的值等于expect(指望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS定义两种资源共享方式

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

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore等。

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

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

参考

Java并发进阶常见面试题总结

相关文章
相关标签/搜索