三、并发与多线程
- 锁
1、Java内存模型。
答:内存模型用于保证时间片导致原子性问题、多核多线程缓存一致性问题、处理器优化有序性问题。Java内存模型规定所有变量都存储在主内存中,每条线程有自己的工作内存,其中保存了主内存中变量的副本拷贝,工作线程对变量的操作必须在工作内存中进行。synchronized提供了monitorenter和monitorexit来保证原子性;使用volatile将被修改的变量立即同步到主内存实现可见性;volatile禁止指令重排、synchronized保证同一时刻只允许一条线程操作来实现有序性。
2、volatile原理(内存屏障)。
答:通过硬件层的内存屏障(与gc中的不同):1)能阻止屏障两侧的指令重排序;2)在指令前加入读屏障可以让高速缓存中的数据失效,强制从内存获取,在指令后加入写屏障可以让写入缓存的数据写入主内存。内存屏障类型:LoadLoad,StoreStore,StoreLoad,LoadStore。
3、synchronized原理。
答:对象头中的重量级锁(synchronized的对象锁)的指针指向monitor对象的起始地址,monitor中有两个队列_WaitSet和_EntryList,用来存储等待线程对象,_owner指向持有monitor对象的线程。方法同步:ACC_SYNCHRONIZED(当一个线程去访问时如果存在标识,则去获取monitor),代码块:MonitorEnter、Monitorexit。
4、什么是happen-before?
答:JMM可以通过happen-before关系向程序员提供跨线程的内存可见性保证,保证跨线程的语句执行顺序。synchronized、volatile、lock 等相关代码的操作顺序都属于该机制的一部分。
5、什么是CAS?
答:CAS(Compare And Swap)是用于实现多线程同步的CPU原子指令,它将内存中的将要修改的数据与预期的值进行比较,如果值相等,则将内存中的数值替换为新值,否则则不做操作。通过sun包下的Unsafe类实现,其下都是native方法。
使用场景:对于资源竞争较少的情况可以减少线程阻塞和唤醒切换以及用户态内核态间切换CPU的资源消耗,但是对于资源竞争严重,CAS自旋的概率会比较大,从而浪费更多的CPU资源。
6、锁优化。
答:1)偏向锁:第一个申请锁的线程会使用锁,以后线程进入时测试对象头中是否存储着指向当前线程的偏向锁->成功则表示已经获得锁,失败则需要测试偏向锁标志是否设为1->设置了则尝试使用CAS将偏向锁指向当前线程,没有设置则使用CAS来竞争锁。当存在锁竞争,解除偏向锁,进入轻量级锁。
2)轻量级锁:a.加锁,先在线程帧栈中复制对象中的Mark Word(Displaced Mark Word),然后尝试线程使用CAS将对象头中的Mark Word替换为指向锁记录的指针->成功则获取锁,失败则尝试自旋获取锁。b.解锁,使用CAS将Displaced Mark Word替换回对象头->成功则说明没有竞争,失败则表示存在锁竞争,锁膨胀为重量级锁。c.自旋线程自旋过程中—>成功获得资源,则对象仍然是轻量级锁,否则升级为重量级锁。
3)重量级锁:会引起内核态和用户态切换、线程阻塞造成的线程切换。
两种状态的主要差别在于:
- 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所占有的处理机是可被抢占的;
- 而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占用的处理机是不允许被抢占的。
7、AQS(AbstractQueuedSynchronizer,抽象队列同步器)。
答:是Java提供的底层同步工具类,在java.util.concurrent.locks包,是java自带synchronize关键字以外的锁机制。基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符。如果被请求的共享资源空闲,则当前的请求线程为有效工作线程,并将资源置为锁定,如果被占用,则将线程加入CLH尾部,节点自旋,如果前驱节点释放为头节点且成功获取同步状态,则将线程节点设为头节点,否则利用park挂起,等待被前驱节点唤醒,再进行上一步的自旋操作。
CLH:是一个虚拟的双向队列(不存在队列实例)FIFO,仅存在节点之间的关联关系。
基于AQS实现:独占锁(ReentrantLock),共享锁(Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier)。
8、synchronized,lock比较
答:1)lock是一个接口,synchronized是一个关键字;
2)synchronized在发生异常时会自动释放锁,lock需要手动释放;
3)synchronized使用Object对象本身的wait、notify、notifyAll调度机制,lock使用Condition;
4)synchronized是独占锁,lock是乐观锁。
lock优于synchronized的一些特性:
1)lock可以使用interrupt来中断等待;
2)lock可以通过trylock获知是否获取锁;
3)readlock实现读写分离可以提高线程读操作效率;
4)具有公平锁功能;
9、死锁产生条件以及预防。
答:产生必要条件:互斥、请求和保持条件、不剥夺条件、环路等待条件;预防方法:资源一次性分配(破坏请求)、只要有个一个资源分配不到就不给其他资源(破坏请求和保持)、可剥夺资源、资源分配有序。
10、在static方法和非static方法前面加synchronized有什么不同?
答:static获得的锁属于这个类的锁,会发生互斥;而非static方法获取得锁属于对象,不会发生互斥,这时候可以使用lock。
- JUC
1、JUC常用工具。
答:
2、LockSupport。
答:作用类似Object的wait/notify,底层基于Unsafe类的park/unpark,相比wait/notify的优势:LockSupport不要在同步代码块中执行,不要维护一个同步对象;unpark可以先于park调用,不用担心线程间执行顺序。
3、FutureTask。
答:Runnable的run返回值是void类,Callable的submit返回值类型是Future,future接口包含cancel、isCancelled、isDone、get()、get(long timeout, TimeUnit unit)。
FutureTask可以直接创建对象,实现了RunnableFuture接口,RunnableFuture继承Runnable接口和Future接口。FutureTask既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}
4、CompletableFuture。
答:对Future的扩展,提供了函数式编程能力。接口:1)thenApply:当前执行结果作为下一阶段入参;2)thenAccept:与thenApply区别是入参是Consumer;3)thenRun:不需要入参;4)thenCombine/thenAcceptBoth:整合两个计算结果,有/无返回值;
5)applyToEither与acceptEither:取最先返回的,有/无返回值;6)allOf/anyOf:多任务都完成/任一完成。
5、BlockingQueue介绍。
答:
抛出异常 | 特殊值 | 阻塞 | 超时 | |
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查 | element() | peek() | 不可用 | 不可用 |
BlockingQueue 的实现类 :ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue(实现Comparable)、SynchronousQueue。
6、Java实现生产者/消费者模式。
答:BlockingQueue,wait && notify,Lock && Condition,Semaphore。
7、CopyOnWriteArrayList为什么并发安全且性能比Vector好?
答:Vector是增删改查方法都加了synchronized,保证同步;CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,因为需要复制整个数组,所以增删改开销很大,内部有一个volatile数组,修改时会新建数组。
8、LinkedBlockingQueue和ConcurrentLinkedQueue适用场景?
答:适用阻塞队列的好处:多线程操作共同的队列时不需要额外的同步,另外就是队列会自动平衡负载,即那边(生产与消费两边)处理快了就会被阻塞掉,从而减少两边的处理速度差距。
当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。多个生产者,对于LBQ性能还算可以接受;但是多个消费者就不行了mainLoop需要一个timeout的机制,否则空转,cpu会飙升的。LBQ正好提供了timeout的接口,更方便使用。
- 线程
1、什么是竞态条件、临界区?
答:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
2、Thread的start和run有什么区别?多次start会发生什么?
答:直接执行run,线程会被当做主线程中的一个普通方法执行。java.lang.IllegalThreadStateException线程状态非法。
3、你知道 Java 的 Exchanger 吗?简单说说其特点及应用场景?
答:Exchanger是一个用于在两个线程之间交换数据的工具类,调用后会阻塞线程直到其他线程来获取exchanger数据。
4、ThreadLocal。
答:Threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。
实现原理:
1)ThreadLocal类中定义了一个ThreadLocalMap(Entry数组),ThreadLocalMap的key是ThreadLocal类的实例对象,value是用户的值;
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocalMap中用于存储数据的entry定义
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2)ThreadLocal类对象set时,首先获得当前线程的ThreadLocalMap类属性(Thread.currentThread().threadLocals),然后以ThreadLocal的对象为key,设定value。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
3)ThreadLocal变量的活动范围是某线程,当线程终止后,这些值会被作为垃圾回收。
5、ThreadLocal的内存泄漏问题。
答:强引用链:Thread Ref->Thread->ThreadLocalMap->Entry->value,如果ThreadLocal外部没有强引用就会被gc回收,key为null,就没法访问到value,这个value在线程存在的情况下就无法释放。
ThreadLocal为什么不使用强引用:因为使用强引用,即使引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal也不被回收。
解决方法:1)先从索引位置获取Entry e,如果e不为null并且key相同则返回e;
2)如果e为null或者key不一致则向下一个位置查询,符合则返回。否则key值为null,则擦除该位置的Entry,继续向下一个位置查询。因为是在调用get和set函数过程中删除的,所以很多情况下要手动调用remove删除。
3)JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题就不存在了。
1 static 防止无意义多实例
2 当static时,ThreadLocal ref生命延长-ThreadMap的key在线程生命期内始终有值-ThreadMap的value在线程生命期内不释放——故线程池下,static修饰ThreadLocal引用,必须(1)remove 或(2)手动 ThreadLocal ref = null
6、怎样保证线程按顺序执行?
答:join方法,wait/notify,Condition,CompulatableFuture等。
7、Excutors。
答:NewFixedThreadPool(固定数目线程)、NewCachedThreadPool(基于SynchronousQueue(不会为队列中的元素维护存储空间,而且维护一组线程,put和take同时发生,否则一直阻塞)对于提交的任务,有空闲的线程则用空闲的,否则创建一个新的,空闲线程保留60秒)、NewSingleThreadPool(单线程,异常时会产生新的)、NewScheduledThreadPool(定时执行任务)、NewWorkStealingPool(使用ForkJoinPool,可以充分利用多cpu,适合很耗时的操作)。
构造函数(除NewWorkStealingPool):ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepaAliveTime,TimeUnit timeUnit,BlockQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExceptionHandler rejectedHandler) //ThreadPoolExecutor构造方法
corePoolSize核心线程数,也可理解为最小线程数
newCachedThreadPool:0个
newSingleThreadExecutor:1个
newFixedThreadPool:看参数
newScheduledThreadPool:看参数个数
maximumPoolSize最大线程数(NewCachedThreadPool和NewScheduledThreadPool的为Integer.MAX_VALUE)
keepaAliveTime非核心线程非活跃存活时间长度
timeUnit存活时间单位,枚举等
workQueue阻塞队列,如ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue等
newSingleThreadExecutor:LinkedBlockingQueue
newFixedThreadPool:LinkedBlockingQueue
newCachedThreadPool:SynchronousQueue
newScheduledThreadPool:DelayedWorkQueue
threadFactory可定制线程,一般使用默认即可
rejectedHandler线程池满时拒绝策略,如Abort,Discard,CallerRuns,DiscardOldest
DiscardPolicy:丢弃任务,不抛异常;
DiscardOldestPolicy:不断尝试丢弃最前面的任务,尝试执行当前任务;
AbortPolicy:丢弃任务,抛出异常;
CallerRunsPolicy:由调用线程处理该任务。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
说明:Executors 返回的线程池对象的弊端如下:
1、FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2、CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
8、线程池的处理流程。
答:核心线程和非核心线程的区别是,非核心线程用完以后就被回收了。
9、wait与sleep有什么不同?
答:1)wait是Object方法,sleep是Thread方法;
2)wait会释放锁,sleep不会;
3)wait要在同步代码块中使用,sleep不用;
4)wait不需要捕获异常,sleep需要。
10、如何尽可能提高多线程并发性能?
答:1)减少临界区;
2)使用ThreadLocal;
3)减少线程切换;
4)使用读写锁或CopyOnWrite
11、多线程实现的方式?
答:继承Thread类、实现Runnable接口、FeatureTask、Executors。
12、sleep(0)作用?
答:让线程释放时间片,参加cpu竞争。
13、线上宕机,线程池阻塞队列怎么办?
答:队列中积压的任务会丢失。在提交一个任务到线程池里去之前,现在数据库里面插入这个任务的信息,更新他的状态:未提交、已提交、已完成。提交成功后,更新他的状态是已提交状态。
- 其他
1、delayQueue和时间轮算法的异同。
答:delayQueue基于最小堆的排序,插入、移除时间复杂度为O(log(n));
时间轮算法:如下图一个轮子,有8个“槽”,可以代表未来的一个时间。如果以秒为单位,中间的指针每隔一秒钟转动到新的“槽”上面,就好像手表一样。如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)。这个圈数需要记录在槽中的数据结构里面。这个数据结构最重要的是两个指针,一个是触发任务的函数指针,另外一个是触发的总第几圈数。时间轮可以用简单的数组或者是环形链表来实现。插入、移除时间复杂度为O(1)。
2、CAP理论。
答:一致性、可用性、分区容错性。一般互联网公司都是保证AP,并达到最终一致性。
3、分布式锁
答:redis->redisson:缓存失效(30s有效期+watchdog);
zookeeper->curator:羊群效应(有序加锁,下一个客户端在上一个锁上加watch,只会通知一个客户端获取到锁)、脑裂(zookeeper默认Quorums,超过半数选举)
4、分布式锁的过期时间小于业务的执行时间该如何续期?
答:使用watch dog,每隔一定周期检查客户端是否还持有锁,如果还持有则延长锁的生存时间。