JDK1.6引入了偏向锁,轻量级锁,让锁有了四个状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

锁的常见分类

可重入锁和非可重入锁

可重入锁:在锁的内部还可以再次获取锁,比如一个线程获得了某个对象的锁,此时锁还没有释放,当想再次获取锁的时候还是可以获取的。

非可重入锁:表示不能够再次获取锁。

自旋锁

使用互斥锁进行同步的开销很大,为提高效率应该尽量避免。自旋锁的思想是在一个线程请求一个共享数据时进行循环也就是自旋一段时间,如果能在这段获取到锁,就避免了进入阻塞状态。

因为循环操作占用CPU时间,自旋锁适用于持有锁的时间很短的一些场景。

JDK1.6引入了自适应自旋锁。自适应意味着自选的次数是可变的,是由上一次在同一个锁上的自旋次数以及是否成功获得锁来决定的。

锁消除

锁消除是指对于一些共享数据检测出不可能存在竞争关系,因为不存在竞争关系所以就没必要加锁了,所以对这些数据的加锁操作进行消除。

锁消除的原理是,通过逃逸分析,如果堆上的共享数据不可能逃逸出去被其他线程访问到,就把它们当做私有数据对待,就可以将它们的锁进行消除。

锁粗化

如果一些操作对同一个对象反复加锁和解锁,这样没有实际意义并且会导致性能的损耗。

如果Java探测到这样对一个对象的反复加锁,将会把锁的范围扩展也就是粗化到整个操作的外部,这样只需要加锁一次,减少了性能的损耗。

JDK1.6引入了偏向锁,轻量级锁,让锁有了四个状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

轻量级锁

轻量级锁是相对于重量级锁而言的,它用CAS操作来避免重量级锁的开销。轻量级锁是基于这样一个经验,那就是对于绝大部分锁在同步期间是不存在竞争的,因此就不需要使用互斥量来同步,可以先使用CAS来尝试同步,如果CAS失败了再改用互斥量进行同步。

偏向锁

偏向锁是指偏向于第一个获取锁对象的线程。这个线程之后再获取该锁就不再需要进行同步操作。甚至CAS操作也不需要。

当锁对象第一次被线程获得的时候,进入偏向状态。同时使用CAS操作将线程ID记录到MarkWord中,如果CAS操作成功,那么这个线程之后每次进入这个锁相关的同步块就不需要进行任何同步操作。

但当另外一个线程尝试获取该锁对象时,偏向锁状态就结束,此时恢复到无锁状态或轻量级锁状态

synchronized 关键字

  1. 说一说自己对于 synchronized 关键字的了解

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

在早期版本中,synchronized属于重量锁,效率十分低,它是依赖于底层操作系统,Java的线程是映射到操作系统原生线程之上的。如果要挂起或唤醒一个线程,都需要操作系统帮忙,而操作系统实现线程间的切换需要从用户态转换为内核态,这个状态间的切换需要一定的时间,这也是早期synchronized效率低下的原因。

在Java6之后,Java从JVM层面对synchronized底层做了优化。引入了自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁,来减少锁的开销。

  1. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗


    synchronized关键字的三种使用方式:
    • 修饰实例方法:作用域当前对象实例,进入同步代码之前要获得当前对象实例的锁
    • 修饰静态方法:会给当前类加锁,因为静态方法不属于任何实例,不管new了多少对象,静态方法只属于类。所以如果A线程调用实例对象的非静态synchronized 方法,线程B调用实例对象的静态synchronized 方法,不会发生互斥现象。
    • 修饰代码块:指定加锁对象,进入同步代码前需要获得给定对象的锁。
  2. 双重校验锁方式实现单例模式

    1. public class Singleton{
    2. private volatile static Singleton uniqueInstance;
    3. private Singleton(){
    4. }
    5. public static Singleton getUniqueInstance(){
    6. if(uniqueInstance==null){
    7. synchronized(Singleton.class){
    8. if(uniqueInstance==null){
    9. uniqueInstance = new Singleton();
    10. }
    11. }
    12. }
    13. return uniqueInstance;
    14. }
    15. }


    需要注意的是uniqueInstance需要使用volatile修饰,在多线程下不加volatile可能会出错。原因如下:
    uniqueInstance = new Singleton();这段代码其实分为三步:

    1. 为uniqueInstance分配内存空间。
    2. 初始化uniqueInstance。
    3. 将uniqueInstance指向分配的内存地址。

由于JVM有指令重排的特性,在多线程下可能造成线程拥有其他线程还没有初始化完全的实例,此时线程调用getUniqueInstance()发现uniqueInstance不为空,并返回了uniqueInstance,但此时uniqueInstance还未被初始化。而Volatile可以禁止JVM的指令重排

  1. 讲一下 synchronized 关键字的底层原理


    synchronized关键字的底层原理是属于JVM层面
    1. synchronized修饰同步语句块java synchronized(this){ //todo }
      同步语句块的底层实现使用的是monitorentermonitorexit指令,其中monitorenter指令指向同步代码块开始位置,monitorexit指向同步代码块结束位置。
      当指向monitorenter时,线程试图获取monitor的持有权,monitor监视器存在与Java的对象头,这也是java中任意对象可以作为锁的原因。当计数器为0则可以获取,获取后将锁计数器设置为1。在实现monitorexit后,将锁计数器设为0,表示锁释放。

image.png
Monitor由ObjectMonitor实现:

  1. _WaitSet:用来保存每个等待(被wait挂起)锁的线程对象。
  2. _owner:它指向持有ObjectMonitor对象的线程
  3. _EntryList:当多个线程同时访问一段同步代码时,会先存放到 _EntryList 集合中
  4. _count:来实现可重入

实现的逻辑为:当多个线程尝试获得锁时,会先存放在EntryList中,当线程获取到对象的monitor时,owner会设置为当前线程。同时count加1,如果线程调用wait() 方法或者同步方法执行完,就会释放当前持有的monitor,那么_owner变量就会被置为null,同时_count减1,并且该线程进入 WaitSet集合中,等待下一次被唤醒。

  1. synchronized修饰方法java public synchronized void method(){ //todo }
    synchronized修饰方法使用的是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该标识了执行相应的同步调用。
    1. 谈谈 synchronized 和 ReentrantLock 的区别


      相同点:两者都是可重入锁,同一个线程可以在锁的内部再次获得锁,这时锁的计数器都会自增1,所以到等到锁的计数器下降为0才能释放锁。
      不同点:
  2. synchronized依赖JVM实现,而ReentrantLock则属于JDK层实现的。因为ReentrantLock是JDK层的,我们可以查看它的源码,来看它是如何实现的。
  3. ReentrantLock比synchronized多了一些高级功能:
    • 等待可中断,正在等待的线程可以放弃等待,改为处理其他事情。
    • 实现公平锁,ReentrantLock可以指定是公平锁还是非公平锁,而Synchronized只能是非公平锁。公平锁即严格按照先进先出的原则去分配锁资源。
    • 可实现选择性通知,该功能可以通过一个lock绑定多个Condition实例,线程对象可以注册在指定的Condition实例中,通过选择性的调用特定Condition实例的await()或signal()来使线程等待或唤醒。