一、锁的分类

自旋锁
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

乐观锁
假定没有冲突,在修改数据时如果发现和之前获取的不一致,则读取最新数据,重试修改。

悲观锁
假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。

独享锁(写锁)
给资源加上写锁,线程可以修改资源。其它线程不能再加锁(只能写)

共享锁(读锁)
给资源加上读锁后只能读不能改,其它线程也只能加读锁,不能加写锁(可供多个线程获取)

重入锁
同一个线程多次获取同一把锁,不会出现死锁

公平锁与非公平锁
争抢锁的顺序,如果按先来后到为公平锁,反之为不公平锁。

二、同步关键字 synchronized 的特性

  1. 可以用于实例方法,静态方法,隐式指定锁对象
  2. 用于代码块时,显示指定锁对象
  3. 锁的作用域:对象锁,类锁,分布式锁
  4. 可重入锁,独享锁,悲观锁,非公平锁
  5. 锁消除,锁粗化
    下面针对锁消除和锁粗化两个特性重点说明一下:

锁消除
删除不必要的加锁操作。在连续进行加锁和解锁的情况下,触发JIT 编译将锁忽略的情况叫锁消除。举例:

// 举例
 public static void main(String[] args) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("A");
        stringBuffer.append("B");
        stringBuffer.append("C");
        stringBuffer.append("D");
        stringBuffer.append("E");
        System.out.println(stringBuffer.toString());
    }
// 查看StringBuffer append 方法中使用了synchronized 
 @Override
 public synchronized StringBuffer append(String str) {
       toStringCache = null;
       super.append(str);
       return this;
 }

这里由于append() 中使用了 synchronized ,因此在 main() 中 每一次append 不会都进行加锁。

锁粗化
将小粒度的锁合并为大粒度的锁称为锁虚化:

public void test(){
        int i =0;
        synchronized (this){
            i++;
        }

        synchronized (this){
            i--;
        }
        
        System.out.println(i);
    }

下面是粗化后的样子(其实就可以理解为锁合并):

public void test1(){
    int i =0;
    synchronized (this){
          i++;
          i--;
    }
    System.out.println(i);
}

三、synchronized(this) 的原理

我们都知道,Java对象存储在堆(Heap)内存中,那么一个对象包含:对象头、对象体和对齐字节。

对象头
对象头中的Mark Word 主要用来表示对象的线程锁的状态,还可以用来配合GC,存放该对象的hashCode

对象头部的 Mark Word 每个状态如下:

mysql 公平锁 还是非公平锁 synchronized实现公平锁_java

Unlocked 未锁定
Light-weight locked  轻量级锁
Heavy-weight locked  重量级锁
Marked for GC 
Biased/biasable 偏向锁
偏向锁

JDK6 以后,默认开启了偏向锁这个优化,通过JVM 参数 -XX:-UseBiasedLocking 来禁用偏向锁。若偏向锁开启,只有一个线程抢锁,可获取到偏向锁(因此称为偏向,理解为偏向于某一个线程)。在Mark Word 中的体现:

mysql 公平锁 还是非公平锁 synchronized实现公平锁_synchronized_02


有无开启偏向锁是由状态0、1 决定的,有无锁定则查看有无 thread id

轻量级锁

在未锁定的状态下,通过CAS 来抢锁,抢到的是轻量级锁。抢锁过程如下:

mysql 公平锁 还是非公平锁 synchronized实现公平锁_java_03


两个线程同时进行CAS 操作,然后只有线程1操作成功,修改 Mark word

mysql 公平锁 还是非公平锁 synchronized实现公平锁_java_04

重量级锁

轻量级锁中的自旋有一定的次数限制,超过了这个次数限制,轻量级锁就会升级为重量级锁。

mysql 公平锁 还是非公平锁 synchronized实现公平锁_synchronized_05


依据上图中的描述,现在T2 要执行抢锁,执行步骤:

  1. T1 线程调用 obj.wait() ,T1 就进入了 waitSet 进行等待,此时 owner =null
  2. T2 线程抢锁成,owner=T2, 执行obj.notify() 将T1唤醒
  3. T1 线程继续进入entryList 中排队等待
  4. T2 执行完代码块,就执行 exitMonitor ,将owner 赋值为 null
  5. T1 线程抢锁…
锁的升级过程

mysql 公平锁 还是非公平锁 synchronized实现公平锁_公平锁_06


参考资料:

mysql 公平锁 还是非公平锁 synchronized实现公平锁_mysql 公平锁 还是非公平锁_07

四、面试题之 ReentrantLocksynchronized的区别?

1)可重入性

从上面已经可以知道 synchronized 关键字是可重入的,而 ReentrantLock 顾名思义也是可以重入的。

2)锁的实现不同

synchronized :依赖于JVM 实现,实现方式难以看到。
ReentrantLock : JDK 实现,有直接的源码可供阅读

3)性能不同

synchronized : 在优化以前,由于自旋产生CPU 性能大耗,当JDK 6 引入偏向锁,轻量级锁后,两者的性能差别不大。可以说两者都采用了CAS 技术,视图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

4)功能不同

使用的便利性: synchronized 优于 ReentrantLock ,因为 ReentrantLock 需要手动声明加锁和解锁。
锁的细粒度和灵活度: ReentrantLock 优于 synchronized

ReentrantLock 独有的能力:

1) ReentrantLock 区分是公平锁还是非公平锁,而synchronized 只能是非公平锁
2)ReentrantLock 提供了Condition 类,用来实现分组唤醒需要唤醒的线程,而 synchronized 要么随机唤醒一个线程,要么唤醒全部线程。
3) ReentrantLock 提供了 lockInterruptibly() 中断等待锁的线程。而 synchronized 是不能中断的。

五、synchronized 与Lock 比较

synchronized

优点:

  1. 使用简单,语义清晰
  2. 由JVM 提供,提供了多种优化方案(锁粗化,锁消除,偏向锁,轻量级锁)
  3. 锁的释放由JVM 完成,不用人工干预,也降低了死锁的可能

缺点:
无法实现一些锁的高级功能:公平锁,中断锁,超时锁,读写锁,共享锁。

Lock

优点

  1. 所有synchronized 的缺点
  2. 可以实现更多的功能

缺点
需要手动释放锁,如使用不当可能造成死锁