Java-Concurrent

Java-并发

那么请谈谈 AQS 框架是怎么回事儿?

AQS 全称 AbstractQueuedSynchronizer,抽象队列同步器,即一个用来构建锁和同步器的框架。
底层使用了 CAS 计数来保证操作的原子性,同时利用 FIFO 队列实现线程之间的锁竞争,将基础的同步相关抽象细节放在 AQS内部,这也是其他ReentrantLock、CountDownLatch 等工具类的底层实现机制。
AQS 的使用原理是当一个线程请求共享资源的时候会判断是否能够成功锁定该共享资源,如果可以就会把这个共享资源设置为锁定状态,如果当前共享资源已经被锁定,那就把这个线程阻塞住,放入到队列中进行等待。

CAS

CAS 全称是 compare and swap,即比较并交换,它是一种原子操作,同时也是一种乐观机制。基本原理是如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。
许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
ABA问题:(1)线程1读取内存中数据为A;(2)线程2将该数据修改为B;(3)线程2将该数据修改为A;(4)线程1对数据进行CAS操作,在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。
只能保证一个共享变量的原子操作:对多个共享变量操作时,循环CAS就无法保证操作的原子性,但是有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。(AtomixReference 保证引用对象之间的原子性,使用版本号来记录每一次的修改)
循环时间开销很大:CAS 的自旋操作如果长时间一直不成功,可能会为CPU带来更大的开销。

Synchronized 用过吗,其原理是什么?

Synchronized:Java中的关键字,是一种同步锁,可以用来修饰代码块、方法、静态方法、类。底层原理是以基于进入和退出管程(Moniter)对象实现的,无论是显示同步(monitorenter 和 moniterexit)还是隐式同步均是如此。
synchronized 属于重量级锁,效率低,因为监视器锁(monitor)是依赖于底层操作系统的 Mutex Lock 实现的,而操作系统实现线程之间的切换需要从用户态转换到核心态,这个状态之间的转换需要相对较长的时间,时间成本很高。

你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?

锁的本质是monitorenter和monitorexit字节码指令的一个Reference类型的参数,即要锁定和解锁的对象。
如果Synchronized明确指定了锁对象,比如Synchronized() 变量名)、 Synchronized(this)等,说明加解锁对象为该对象。
如果没有明确指定:若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁对象;若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。

什么是可重入性,为什么说 Synchronized 是可重入锁?

synchronized 的可重入性:在一个线程再次请求获得自己持有的锁时,请求会成功。即在synchronized 方法内部再次调用synchronized 方法是被允许的。

为什么说 Synchronized 是非公平锁?

非公平行为主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。

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

Synchronized 显然是一个悲观锁,因为它的并发策略是悲观的,无论有没有线程竞争,任何的操作都会加锁。

JVM 对 Java 的原生锁做了哪些优化?

偏向锁:如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,立刻获取到锁,省去了大量有关锁申请的操作,从而提高程序的性能。通过对比对象头部信息中的 Mark Word thread id 解决加锁问题。
轻量级锁:偏向锁失败则会升级为轻量级锁,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。轻量级锁是通过用 CAS 操作 Mark Word 和自旋来解决加锁问题
自旋锁:轻量级锁失败后,为了避免线程在操作系统层面挂起,会进行称为自旋锁的优化。在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,最后也升级为重量级锁。 其中synchronized 在轻量级锁获取失败后,不会使用自旋锁,而是直接升级为重量级锁,在重量级锁获取失败后才会进入自旋锁。
适应性自旋锁:所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

乐观锁和悲观锁

乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。
当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加(悲观)锁。一旦加锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。

乐观锁一定就是好的吗?

乐观锁避免了悲观锁独占对象,同时也提高了并发性能。乐观锁以CAS 为基础,会出现ABA问题、只能保证一个共享变量的原子操作性。

互斥锁

互斥锁(Mutex)是指在同一时刻只能有一个线程对共享资源进行操作,具有排他性和唯一性。
互斥锁由底层操作系统实现,会由用户态切换为核心态,来实现线程的切换,但是会存在一定的开销成本。
开销的成本主要存在于两次线程上下文切换的成本,当两个进程同属于一个进程,因为虚拟内存是共享的,所以在切换的时候只需要切换线程私有的数据、寄存器等不共享的数据即可。

自旋锁

自旋锁是指在线程获取锁失败后,线程会进入忙等待状态,不断重试获取锁知道获取锁成功才会退出。
自旋锁是由CPU提供的CAS函数来实现的,在用户态上完成加锁和解锁操作,相比互斥锁不会有线程上下文切换。
加锁的步骤:查看锁的状态如果空闲执行第二步,把锁设置为当前线程持有。
CAS函数将这两步合并为一条硬件指令形成原子指令,保证要么全部成功要么全部失败。

读写锁

读写锁是由读锁和写锁构成,如果要对数据进行读取则对读锁加锁,如果是要修改数据则对写锁加锁。
当写锁没有被加锁的时候,此时多个线程可以并发的持有读锁。而写锁被加锁后,其余线程获取读锁都会被阻塞,而且其他线程获取写锁也会阻塞。
公平读写锁是指在读写锁的基础上,用队列将获取锁的线程排队,无论是写线程还是读线程都按照先进先出的原则加锁即可,这样即可以实现并发,也不会出现线程饥饿现象。

什么是锁消除和锁粗化?

锁消除:锁消除是一项虚拟机的一种优化,在即时编译(JIT)阶段,通过上下全文的扫描去除不可能存在共享资源竞争的锁,以此来消除没有必要的锁。
锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

Lock、ReentrantLock、ReadWriteLock、ReentrantReadWriteLock 原理

Lock:Lock是一个类,通过这个类可以实现同步访问。主要通过lock()、unlock()方法来实现加锁和解锁。
ReentrantLock(可重入锁):锁的分配机制:基于线程的分配,而不是基于方法调用的分配、
ReadWriteLock:一个用来获取读锁,一个用来获取写锁。、
ReentrantReadWriteLock:readLock()和writeLock()用来获取读锁和写锁。

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

ReentrantLock内部自定义了同步器Sync( Sync既实现了AQS,又实现了AOS , 而AOS提供了一种互斥锁持有的方式),
其实就是加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了。

ReentrantLock的公平锁和非公平锁只有两处不同:
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?

JUC 指的是java.util.concurrent包内的其他工具类:
提供了CountDownLatch、CyclicBarrier、Semaphore等,比Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。
提供了ConcurrentHashMap、有序的ConcunrrentSkipListMap , 或者通过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等,各种线程安全的容器。
提供了ArrayBlockingQueue、SynchorousQueue或针对特定场景的PriorityBlockingQueue等,各种并发队列实现。
强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等。

请谈谈 ReadWriteLock 和 StampedLock。

有时候不需要大量竞争的写操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java提供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。
JDK在后期引入了StampedLock,在提供类似读写锁的功能的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。

跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?

这两者锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。当然分布式锁也是一样的道理。
Synchronized通过在对象头中设置标记Mark Word实现了这一目的,是一种JVM原生的锁实现方式。
ReentrantLock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架。

请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。

功能角度:ReentrantLock 是Lock的实现类,是一个互斥的同步锁。 ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一样使用),甚至实现Synchronized没有的高级功能:
等待可中断、带超时的获取锁尝试、可以判断是否有线程在排队等待获取锁、可以响应中断请求、可以实现公平锁。
锁释放角度:Synchronized 在JVM层面上实现的,在代码执行出现异常时,JVM会自动释放锁定;但是使用Lock则不行,Lock是通过lock()代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。
性能角度:在竞争不激烈时,Synchronized的性能要优于ReetrantLock;在高竞争情况下 ,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。

如何让 Java 的线程彼此同步?你了解过哪些同步器?请分别介绍下。

JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier和Semaphore。
CountDownLatch 同步计数器,允许一个或多个线程等待某些操作完成。
CyclicBarrier 循环栅栏,它实现让一组线程等待至某个状态之后再全部同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使用。
Semaphore 信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的,其原理是通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。

CyclicBarrier 和 CountDownLatch 看起来很相似,请对比下呢?

CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier没有这种限制,可以重用。
CountDownLatch的基本操作组合是countDown()/await(),调用await()的线程阻塞等待CountDownLatch计数器为0,即让一个线程等待其他多个线程到达某个状态后然后自己开始运行。而CyclicBarrier 仅通过await() 让多个线程到达某一个状态后相互等待,等待所有线程全部到达该状态再执行后续的任务。

sleep、wait、yield、join

sleep()是Thread的静态方法,当方法被调用时线程会让出CPU进入阻塞状态,但是不会让出资源锁。
wait()必须在synchronized函数或者synchronized 代码块中运行,并且必须在notify()之前调用,wait()被调用之后会释放资源锁,还会让出CPU资源。wait()还是Object类的方法。
yield()会让同优先级线程进入到就绪状态,有更多的执行机会。同时也是Thread的静态方法。
join()执行后线程进入阻塞状态,直到join方法结束或者中断才会再次运行。

wait()和notify()不仅是普通方法或者是同步工具,而是线程之间的通信机制。还有一个原因就是每一个对象都可以上锁。而线程想要进入代码的临界区就必须有监视器,这样可以锁定并等待锁。

Java 中的线程池是如何实现的?

在Java中,线程池被抽象为一个静态内部类 Worker,基于AQS实现,线程主要存放在线程池的 HashSet workers 成员变量中。
而需要执行的任务则存放在成员变量 workQueue( BlockingQueue workQueue )中 。
这样整体上线程池实现的基本思想就是:从workQueue中不断取出需要执行的任务,放在Workers中进行处理。

创建线程池的几个核心构造参数?

corePoolSize: 线程池的核心线程数。
maximumPoolSize: 线程池允许的最大线程数 。
keepAliveTime: 超过核心线程池数时闲置线程的存活时间。
unit: keepAliveTime的单位。
workQueue: 保存执行任务的队列(即runnable 任务)。
threadFactory: 创建自定义的线程工厂,通过该线程工厂可以给每个新建的线程设置一个具有识别度的线程名。
handler: 线程池的饱和处理策略。

动态线程池

corePoolSize: 线程池的核心线程数。使用setCorePoolSize() 方法。
maximumPoolSize: 线程池允许的最大线程数 。使用setMaximumPoolSize() 方法。
workQueue: 保存执行任务的队列(即runnable 任务)。参考LinkedBlockingQueue 的代码,将原有的capacity参数的修饰符final去掉,然后加上set()和get()方法即可。

线程预热:可以通过prestartAllCoreThread() 方法预热所有线程或者使用prestartCoreThread() 方法预热一个线程。

核心线程是否可被回收:通常情况下是不可以被回收的,但是可以通过 allowCoreThreadTimeOut() 方法来修改 allowCoreThreadTimeOut 参数实现核心线程回收。该变量默认为false。

ScheduledThreadPoolExecutor 创建corePoolSize 为0的线程池,然后执行schedule() 方法执行一个任务,会导致cpu单核达到100%。

创建ScheduledThreadPoolExecutor 线程池,scheduleWithFixedDelay() 和 scheduleAtFixedRate() 执行任务,如果任务执行过程中抛出异常,而执行任务又没有捕获异常就会导致线程池停止执行。解决办法就是再任务执行中添加try-catch捕获异常。

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

线程池默认初始化后不启动Worker,等待有请求时才启动。
每次调用执行execute() 方法时,线程池会做如下判断:
1、当前运行的线程数量小于corePoolSize,则马上创建任务执行。
2、当前运行的线程数量大于等于corePoolSize,则将任务放入到队列中。
3、如果这个时候队列已满,而正在运行的线程数量小于maximumPoolSize ,那么就要创建非核心线程执行任务。
4、队列已满,且运行的线程数量大于maximumPoolSize,则抛出RejectExecuteException。
当一个线程闲置的时间超过keepAliveTime,就会将他停止,最终收缩corePoolSize大小。

既然提到可以通过配置不同参数创建出不同的线程池,那么 Java 中默认实现好的线程池又有哪些呢?请比较它们的异同以及说说队列的拒绝策略和可以使用的队列。

Executors.newSingleThreadExecutor 线程池:该线程持当前只有一个线程执行,相当于串行化执行任务。
Executors.newFixedThreadPool 线程池:固定大小的线程持,来一个任务创建一个线程,直到达到上限。而且这个队列是无界的。
Executors.newCachedThreadPool 线程池:无界线程池,可以根据任务增加线程池内部线程来处理任务。
Executors.newScheduledThreadPool 线程池:核心线程持固定,但大小无线的线程池。
new ThreadPoolExecutor() 类,自定义线程池。

AbortPolicy:拒绝并抛出异常RejectExecuteException。
CallerRunsPolicy:重试提交当前的任务,即再次调用运行该任务的execute()方法。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:抛弃当前任务。
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

如何在 Java 线程池中提交线程?

execute():ExecutorService.execute方法接收一个Runable实例,它用来执行一个任务:
submit():ExecutorService.submit()方法返回的是Future对象。可以用isDone()来查询Future是否已经完成 ,当任务完成时 ,它具有一个结果,可以调用get()来获取结果。

什么是 Java 的内存模型,Java 中各个线程是怎么彼此看到对方的变量的?

Java中定义主内存与工作内存的概念:所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存。

请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

关键字volatile是Java虚拟机提供的最轻量级的同步机制。
一个变量被volatile修饰后,就会获得两种功能:禁止指令重排序。保证该变量在不同工作内存中的立即可见性(Java中的内存间操作)。

既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?

不对,volatile只能保证所有线程的可见性,但是不能保证操作的原子性,所以不是并发安全的。

请对比下 volatile 对比 Synchronized 的异同。

总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。

请谈谈 ThreadLocal 是怎么解决并发安全的?

ThreadLocal是提供一种保存线程私有信息的机制,在该线程的生命周期内有效,所以很方便的在一个线程内存储线程关联业务全局需要的id等数据。
ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内。原理就是在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
ThreadLocal 本地线程工具类,内部通过初始化当前线程的ThreadLocalMap参数来存储当前线程内的共享数据。ThreadLocalMap内部以Entry数组存储数据,其中Entry自身为虚引用键为当前ThreadLocal的对象,而value为强引用保存的是存储的值。

很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?

ThreadLocal的实现是基于一个ThreadLocalMap,而在ThreadLocalMap中的每一个Entry的key继承自WeakReference是一个弱引用,但是值value又是一个强引用。这也是为了解决key没有被引用时避免因为不能回收内存而造成的内存泄漏。但是这又会引出来一个问题,key被回收变成了null,导致该 Entry 无法被移除,从而又使Entry无法回收导致内存泄漏。综合以上一定要在 ThreadLocal中自己remove,并且不要和线程池配合使用(worker不会自己退出)。