【JUC并发】八股总结五:synchronized及常见问题

102 阅读6分钟

synchronized 和 volatile 的区别。主要三点

量级性能、可见性原子性、用途/解决问题,分析。

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile 关键字主要用于解决变量在多个线程之间的 可见性,而 synchronized 关键字解决的是多个线程之间访问资源的 同步性(就像volatile一般不在多线程中做i++这种计数操作)

synchronized 关键字的作用

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

synchronized加载静态方法和实例方法的区别

这个知识点在八锁案例中可以体现

静态方法锁类

实例方法锁对象

😖synchronized 关键字的底层原理(难点)

下面讲的是重量级锁,通过对象头 关联 monitor

知道moniterenter字节码,知道对象锁,对象头标记字段 关联monitor,monitor大概的属性就行

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 字节码指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

  1. 执行 monitorenter指令的时候,线程会获取对象的锁监视器 (monitor)
  2. 它的底层由monitor监视器实现的,monitor是jvm级别的(C++实现) ,线程获得锁需要使用对象(锁)关联monitor(重量级锁的情况会关联monitor),这个monitor是在对象头markword字段进行关联的。
  3. monitor内部有三个属性,分别是owner、entrylist、waitset,其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting(调用了wait方法)状态的线程

synchronized 和 ReentrantLock 的区别

JVM和AQS

等待可中断,非阻塞竞争,公平锁,精确唤醒

同:都是可重入锁

不同: (答出前四点就可以了)

  1. synchronized 关键字是依赖于 JVM 虚拟机实现的。ReentrantLock 是 JDK /API 层面实现,需要调用lock()、unlock()等API实现。
  2. ReentrantLock增加了一些高级功能。ReentrantLock的tryLock方法可以非阻塞竞争锁,synchronized是阻塞竞争。
  3. ReentrantLock增加了一些高级功能。ReentrantLock可以实现公平锁,而synchronized只能非公平。
  4. ReentrantLock增加了一些高级功能。ReentrantLock的newCondition实现精确唤醒线程,而同步代码块中的wait notify不能精确唤醒。
  5. ReentrantLock 等待可中断:lockInterruptibly(),正在等待的线程可以放弃等待。

什么场景用reentrantLock?

reentrantLock的使用场景相对于synchronized的优势?

tryLock: 满足高性能,秒杀,缓存击穿

根据上面ReentrantLock的灵活的优点。在非阻塞竞争这块的场景可以用reentrantLock的tryLock。

  1. 比如有个秒杀场景(类似于抢单这种,没获取到锁的线程直接返回) ,需要使用分布式锁
  2. 或者是在缓存击穿逻辑过期方案,使用分布式锁的方案中,也是使用tryLock。

这两种场景要求没有获取到锁的线程立即返回,满足系统的高性能,不阻塞。所以需要使用tryLock非阻塞获取锁

😖synchronized 锁升级流程(需要了解,问得少,难点)

可以看黑马 满老师

04.034-synchronized优化原理-偏向锁-状态_哔哩哔哩_bilibili

synchronized锁升级过程-CSDN博客

总过程:无锁,偏向锁,轻量级锁,重量级锁
前置知识,首先知道对象头包含的信息。分为markWord和KlassWord;MarkWord有分代年龄、hashCode和锁标识

偏向锁

04.034-synchronized优化原理-偏向锁-状态_哔哩哔哩_bilibili

Java6中引入,只有第一次使用时 会CAS地 将线程 ID设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后如果是同一个线程直接访问就行,同时也不需要解锁(就是说退出同步代码快之后,偏向锁标识不会取消)。

如果每次都是同一个线程访问加锁,那么加的就是偏向锁。

锁膨胀:

当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。这也是我们经常所说的锁膨胀

轻量级锁

04.030-synchronized优化原理-轻量级锁_哔哩哔哩_bilibili

分为几个步骤

  1. 线程在栈帧中创建一个锁记录(包含锁记录地址,对象指针),对象指针指向对象。
  2. CAS的方式,将锁记录中的 锁记录地址 和 对象头中的markWord相关 信息交换。(交换完成后相当于互相指向,锁记录指向对象,对象头指向锁记录)
  3. 如果交换成功,标识轻量级加锁成功。如果失败,表示有其他线程已经加了轻量/重量级锁。
  4. 最后如果解锁,就将锁记录撤销,并将markWord交换回来。

自旋锁/自适应自旋锁(锁膨胀):

  • 如果一个线程加了轻量级锁,其他线程也想来加锁,首先会进行自旋而不是阻塞,这个自旋次数是自适应的(就是说如果当前线程之前自旋后成功的概率很高,那么会多自旋几次,否则少自旋几次)。
  • 如果自旋成功,则还是轻量级锁,如果自旋多次失败,那么该线程将会阻塞并且升级会重量级锁!!
  • 所以说只有重量级锁会阻塞线程。

重量级锁

重量级锁就不多介绍,对象头存monitor地址,monitor中有(owner,entryList,waitList)。

锁升级总结

  1. 只有一个线程偏向锁cas获取,将线程ID放在markWord中,获取之后不需要解锁。
  2. 有另一个线程来(不是同时访问),锁膨胀为轻量级锁。栈帧中有锁记录锁记录指向对象并存markword信息,对象头中指针指向锁记录
  3. 线程以自适应自旋的方式获取轻量级锁。如果获取不到则会升级成重量级锁。重量级锁是真正的互斥锁,只有重量级锁才会使得线程阻塞。

为什么重量级锁开销大?

系统会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

lock和tryLock在高并发场景下分布式锁的使用?

都是根据具体场景看的

如果需要快速响应请求,可以考虑使用tryLock(秒杀),但是某些场景会给用户不好的体验。

如果必须要获取结果或者强一致性,考虑使用lock阻塞。