Synchronized

Synchronized 相关问题

Synchronized ,其原理是什么?java

Synchronized 是由 JVM 实现的一种实现互斥同步的一种方式,若是你查看被 Synchronized 修饰过的程序块编译后的字节码,会发现,被 Synchronized 修饰过的程序块,在编译先后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令程序员

这两个指令是什么意思呢?算法

在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁:数组

若是这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器 +1;当执行 monitorexit 指令时将锁计数器 -1;当计数器为 0 时,锁就被释放了。安全

若是获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。数据结构

Java 中 Synchronize 经过在对象头设置标记,达到了获取锁和释放锁的目的。多线程

问题二:你刚才提到获取对象的锁,这个“锁”究竟是什么?如何肯定对象的锁?并发

“锁”的本质实际上是 monitorenter 和 monitorexit 字节码指令的一个 Reference 类型的参数,即要锁定和解锁的对象。咱们知道,使用 Synchronized 能够修饰不一样的对象,所以,对应的对象锁能够这么肯定。框架

    1. 若是 Synchronized 明确指定了锁对象,好比 Synchronized(变量名)、Synchronized(this) 等,说明加解锁对象为该对象。工具

    2. 若是没有明确指定:

若 Synchronized 修饰的方法为非静态方法,表示此方法对应的对象为锁对象;

若 Synchronized 修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。

注意,当一个对象被锁住时,对象里面全部用 Synchronized 修饰的方法都将产生堵塞,而对象里非 Synchronized 修饰的方法可正常被调用,不受锁影响。

问题三:什么是可重入性,为何说 Synchronized 是可重入锁?

可重入性是锁的一个基本要求,是为了解决本身锁死本身的状况。

好比下面的伪代码,一个类中的同步方法调用另外一个同步方法,假如 Synchronized 不支持重入,进入 method2 方法时当前线程得到锁,method2 方法里面执行 method1 时当前线程又要去尝试获取锁,这时若是不支持重入,它就要等释放,把本身阻塞,致使本身锁死本身。

对 Synchronized 来讲,可重入性是显而易见的,刚才提到,在执行 monitorenter 指令时,若是这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器 +1,其实本质上就经过这种方式实现了可重入性。

问题四:JVM 对 Java 的原生锁作了哪些优化

在 Java 6 以前,Monitor 的实现彻底依赖底层操做系统的互斥锁来实现,也就是咱们刚才在问题二中所阐述的获取/释放锁的逻辑。

因为 Java 层面的线程与操做系统的原生线程有映射关系,若是要将一个线程进行阻塞或唤起都须要操做系统的协助,这就须要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代 JDK 中作了大量的优化。

一种优化是使用自旋锁,即在把线程进行阻塞操做以前先让线程自旋等待一段时间,可能在等待期间其余线程已经解锁,这时就无需再让线程执行阻塞操做,避免了用户态到内核态的切换。

现代 JDK 中还提供了三种不一样的 Monitor 实现,也就是三种不一样的锁:

  • 偏向锁(Biased Locking)

  • 轻量级锁

  • 重量级锁

这三种锁使得 JDK 得以优化 Synchronized 的运行,当 JVM 检测到不一样的竞争情况时,会自动切换到适合的锁实现,这就是锁的升级、降级。

  • 当没有竞争出现时,默认会使用偏向锁。

JVM 会利用 CAS 操做,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,因此并不涉及真正的互斥锁,由于在不少应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁能够下降无竞争开销。

  • 若是有另外一线程试图锁定某个被偏斜过的对象,JVM 就撤销偏斜锁,切换到轻量级锁实现。

  • 轻量级锁依赖 CAS 操做 Mark Word 来试图获取锁,若是重试成功,就使用普通的轻量级锁;不然,进一步升级为重量级锁。

问题五:为何说 Synchronized 是非公平锁?

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

问题六:什么是锁消除和锁粗化?

  • 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。

程序员怎么会在明知道不存在数据竞争的状况下使用同步呢?不少不是程序员本身加入的。

  • 锁粗化:原则上,同步块的做用范围要尽可能小。可是若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做在循环体内,频繁地进行互斥同步操做也会致使没必要要的性能损耗。

锁粗化就是增大锁的做用域。

Synchronized 显然是一个悲观锁,由于它的并发策略是悲观的:

无论是否会产生竞争,任何的数据操做都必需要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程须要被唤醒等操做。

随着硬件指令集的发展,咱们可使用基于冲突检测的乐观并发策略。先进行操做,若是没有其余线程征用数据,那操做就成功了;

若是共享数据有征用,产生了冲突,那就再进行其余的补偿措施。这种乐观的并发策略的许多实现不须要线程挂起,因此被称为非阻塞同步。

乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操做数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改成新值。

这样处理的逻辑是,首先检查某块内存的值是否跟以前我读取时的同样,如不同则表示期间此内存值已经被别的线程更改过,舍弃本次操做,不然说明期间没有其余线程对此内存值操做,能够把新值设置给此块内存。

CAS 具备原子性,它的原子性由 CPU 硬件指令实现保证,即便用 JNI 调用 Native 方法调用由 C++ 编写的硬件级别指令,JDK 中提供了 Unsafe 类执行这些操做。

问题八:乐观锁必定就是好的吗?

乐观锁避免了悲观锁独占对象的现象,同时也提升了并发性能,但它也有缺点:

  1. 乐观锁只能保证一个共享变量的原子操做。若是多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,无论对象数量多少及对象颗粒度大小。

  2. 长时间自旋可能致使开销大。假如 CAS 长时间不成功而一直自旋,会给 CPU 带来很大的开销。

  3. ABA 问题。CAS 的核心思想是经过比对内存值与预期值是否同样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是 A,后来被一条线程改成 B,最后又被改为了 A,则 CAS 认为此内存值并无发生改变,但其实是有被其余线程改过的,这种状况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一

可重入锁 ReentrantLock 及其余显式锁相关问题

问题一:跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不一样?

其实,锁的实现原理基本是为了达到一个目的:

让全部的线程都能看到某种标记。

Synchronized 经过在对象头中设置标记实现了这一目的,是一种 JVM 原生的锁实现方式,而 ReentrantLock 以及全部的基于 Lock 接口的实现类,都是经过用一个 volitile 修饰的 int 型变量,并保证每一个线程都能拥有对该 int 的可见性和原子修改,其本质是基于所谓的 AQS 框架。

问题二:那么请谈谈 AQS 框架是怎么回事儿?

AQS(AbstractQueuedSynchronizer 类)是一个用来构建锁和同步器的框架,各类 Lock 包中的锁(经常使用的有 ReentrantLock、ReadWriteLock),以及其余如 Semaphore、CountDownLatch,甚至是早期的 FutureTask 等,都是基于 AQS 来构建。

  1. AQS 在内部定义了一个 volatile int state 变量,表示同步状态:当线程调用 lock 方法时 ,若是 state=0,说明没有任何线程占有共享资源的锁,能够得到锁并将 state=1;若是 state=1,则说明有线程目前正在使用共享变量,其余线程必须加入同步队列进行等待。

  2. AQS 经过 Node 内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工做,当有线程获取锁失败后,就被添加到队列末尾。

    • Node 类是对要访问同步代码的线程的封装,包含了线程自己及其状态叫 waitStatus(有五种不一样 取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每一个 Node 结点关联其 prev 结点和 next 结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个 FIFO 的过程。

    • Node 类有两个常量,SHARED 和 EXCLUSIVE,分别表明共享模式和独占模式。所谓共享模式是一个锁容许多条线程同时操做(信号量 Semaphore 就是基于 AQS 的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操做,多余的请求线程须要排队等待(如 ReentranLock)。

  3. AQS 经过内部类 ConditionObject 构建等待队列(可有多个),当 Condition 调用 wait() 方法后,线程将会加入等待队列中,而当 Condition 调用 signal() 方法后,线程将从等待队列转移动同步队列中进行锁竞争。

  4. AQS 和 Condition 各自维护了不一样的队列,在使用 Lock 和 Condition 的时候,其实就是两个队列的互相移动。

问题三:请尽量详尽地对比下 Synchronized 和 ReentrantLock 的异同。

ReentrantLock 是 Lock 的实现类,是一个互斥的同步锁。

从功能角度,ReentrantLock 比 Synchronized 的同步操做更精细(由于能够像普通对象同样使用),甚至实现 Synchronized 没有的高级功能,如:

  • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程能够选择放弃等待,对处理执行时间很是长的同步块颇有用。

  • 带超时的获取锁尝试:在指定的时间范围内获取锁,若是时间到了仍然没法获取则返回。

  • 能够判断是否有线程在排队等待获取锁。

  • 能够响应中断请求:与 Synchronized 不一样,当获取到锁的线程被中断时,可以响应中断,中断异常将会被抛出,同时锁会被释放。

  • 能够实现公平锁。

从锁释放角度,Synchronized 在 JVM 层面上实现的,不但能够经过一些监控工具监控 Synchronized 的锁定,并且在代码执行出现异常时,JVM 会自动释放锁定;可是使用 Lock 则不行,Lock 是经过代码实现的,要保证锁定必定会被释放,就必须将 unLock() 放到 finally{} 中。

从性能角度,Synchronized 早期实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大。

可是在 Java 6 中对其进行了很是多的改进,在竞争不激烈时,Synchronized 的性能要优于 ReetrantLock;在高竞争状况下,Synchronized 的性能会降低几十倍,可是 ReetrantLock 的性能能维持常态。

问题四:ReentrantLock 是如何实现可重入性的?

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

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

一般所说的并发包(JUC)也就是 java.util.concurrent 及其子包,集中了 Java 并发的各类基础工具类,具体主要包括几个方面:

  • 提供了 CountDownLatch、CyclicBarrier、Semaphore 等,比 Synchronized 更加高级,能够实现更加丰富多线程操做的同步结构。

  • 提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者经过相似快照机制实现线程安全的动态数组 CopyOnWriteArrayList 等,各类线程安全的容器。

  • 提供了 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue 等,各类并发队列实现。

  • 强大的 Executor 框架,能够建立各类不一样类型的线程池,调度任务运行等。

问题六:请谈谈 ReadWriteLock 和 StampedLock。

虽然 ReentrantLock 和 Synchronized 简单实用,可是行为上有必定局限性,要么不占,要么独占。实际应用场景中,有时候不须要大量竞争的写操做,而是以并发读取为主,为了进一步优化并发操做的粒度,Java 提供了读写锁。

读写锁基于的原理是多个读操做不须要互斥,若是读锁试图锁定时,写锁是被某个线程持有,读锁将没法得到,而只好等待对方操做结束,这样就能够自动保证不会读取到有争议的数据。

ReadWriteLock 表明了一对锁,下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多、并发写少的时候,可以比纯同步版本凸显出优点:

读写锁看起来比 Synchronized 的粒度彷佛细一些,但在实际应用中,其表现也并不尽如人意,主要仍是由于相对比较大的开销。

因此,JDK 在后期引入了 StampedLock,在提供相似读写锁的同时,还支持优化读模式。优化读基于假设,大多数状况下读操做并不会和写操做冲突,其逻辑是先试着修改,而后经过 validate 方法确认是否进入了写模式,若是没有进入,就成功避免了开销;若是进入,则尝试获取读锁。

 

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

JUC 中的同步器三个主要的成员:CountDownLatch、CyclicBarrier 和 Semaphore,经过它们能够方便地实现不少线程之间协做的功能。

CountDownLatch 叫倒计数,容许一个或多个线程等待某些操做完成。看几个场景:

  • 跑步比赛,裁判须要等到全部的运动员(“其余线程”)都跑到终点(达到目标),才能去算排名和颁奖。

  • 模拟并发,我须要启动 100 个线程去同时访问某一个地址,我但愿它们能同时并发,而不是一个一个的去执行。

用法:CountDownLatch 构造方法指明计数数量,被等待线程调用 countDown 将计数器减 1,等待线程使用 await 进行线程等待。一个简单的例子:

 

CyclicBarrier 叫循环栅栏,它实现让一组线程等待至某个状态以后再所有同时执行,并且当全部等待线程被释放后,CyclicBarrier 能够被重复使用。CyclicBarrier 的典型应用场景是用来等待并发线程结束。

CyclicBarrier 的主要方法是 await(),await() 每被调用一次,计数便会减小 1,并阻塞住当前线程。当计数减至 0 时,阻塞解除,全部在此 CyclicBarrier 上面阻塞的线程开始运行。

在这以后,若是再次调用 await(),计数就又会变成 N-1,新一轮从新开始,这即是 Cyclic 的含义所在。CyclicBarrier.await() 带有返回值,用来表示当前线程是第几个到达这个 Barrier 的线程。

举例说明以下:

Semaphore,Java 版本的信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的,其原理是经过 acquire() 获取一个许可,若是没有就等待,而 release() 释放一个许可。

若是 Semaphore 的数值被初始化为 1,那么一个线程就能够经过 acquire 进入互斥状态,本质上和互斥锁是很是类似的。可是区别也很是明显,好比互斥锁是有持有者的,而对于 Semaphore 这种计数器结构,虽然有相似功能,但其实不存在真正意义的持有者,除非咱们进行扩展包装。

问题八:CyclicBarrier 和 CountDownLatch 看起来很类似,请对比下呢?

它们的行为有必定类似度,区别主要在于:

  • CountDownLatch 是不能够重置的,因此没法重用,CyclicBarrier 没有这种限制,能够重用。

  • CountDownLatch 的基本操做组合是 countDown/await,调用 await 的线程阻塞等待 countDown 足够的次数,无论你是在一个线程仍是多个线程里 countDown,只要次数足够便可。 CyclicBarrier 的基本操做组合就是 await,当全部的伙伴都调用了 await,才会继续进行任务,并自动进行重置。

CountDownLatch 目的是让一个线程等待其余 N 个线程达到某个条件后,本身再去作某个事(经过 CyclicBarrier 的第二个构造方法 public CyclicBarrier(int parties, Runnable barrierAction),在新线程里作事能够达到一样的效果)。而 CyclicBarrier 的目的是让 N 多线程互相等待直到全部的都达到某个状态,而后这 N 个线程再继续执行各自后续(经过 CountDownLatch 在某些场合也能完成相似的效果)。

相关文章
相关标签/搜索