今天继续来看看有关Java多线程的高阶面试题。java
synchronized关键字
volatile关键字
ThreadLocal
线程池
阻塞队列
Atomic 原子类
AQS
说一说本身对于synchronized关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字能够保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。git
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,由于监视器锁(monitor)是依赖于底层的操做系统的 Mutex Lock 来实现的。操做系统实现线程之间的切换时须要从用户态转换到内核态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高,这也是为何早期的 synchronized 效率低的缘由。庆幸的是在 Java 6 以后 Java 官方对从 JVM 层面对synchronized 较大优化,因此如今的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减小锁操做的开销。github
怎么使用synchronized关键字
synchronized关键字最主要的三种使用方式:面试
讲一下synchronized关键字的底层原理
一、synchronized 同步语句块的状况segmentfault
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代码块"); } } }
反编译后缓存
从上面咱们能够看出:多线程
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
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底层的实现机制
下面这段话摘自《深刻理解Java虚拟机》:
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。
lock前缀指令实际上至关于一个内存屏障,内存屏障会提供3个功能:
说说synchronized关键字和volatile关键字的区别
有关volatile的更多知识请移步:浅谈volatile关键字
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()方法的区别是什么呢?
写一个生产者-消费者队列
可使用阻塞队列或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是指一个操做是不可中断的。即便是在多个线程一块儿执行的时候,一个操做一旦开始,就不会被其余线程干扰。
因此,所谓原子类说简单点就是具备原子/原子操做特征的类。
并发包java.util.concurrent
的原子类都存放在java.util.concurrent.atomic
下,以下图所示。
讲讲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 + volatile 和 native 方法来保证原子操做,从而避免 synchronized 的高开销,执行效率大为提高。
谈谈AQS
AQS的全称为(AbstractQueuedSynchronizer),AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用普遍的大量的同步器,好比咱们提到的ReentrantLock,Semaphore,其余的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
AQS原理分析
AQS核心思想是,若是被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工做线程,而且将共享资源设置为锁定状态。若是被请求的共享资源被占用,那么就须要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
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。又可分为公平锁和非公平锁:
ReentrantReadWriteLock 能够当作是组合式,由于ReentrantReadWriteLock也就是读写锁容许多个线程同时对某一资源进行读。
不一样的自定义同步器争用共享资源的方式也不一样。自定义同步器在实现时只须要实现共享资源 state 的获取与释放方式便可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。