java锁的前世今生

java常见锁的发展以及简单说明

Amount Of Article Reading: times

主要按照时间顺序对常见的 java 锁以及一些常见的工具类进行说明,
本文浅谈辄止,旨在有一个整体的认识,深入讨论将放到后面的文章当中。

1. Java5 之前

说到锁就不得不提 synchronized 这个关键字了,它翻译成中文就是“同步”的意思。

我们通常使用synchronized关键字来给一段代码或一个方法上锁, 而synchronized加锁是面向对象的,三种加锁方式分别为:

  • 代码块:锁为括号里面的对象
    public void blockLock() {
        synchronized (this) {
            // code
        }
    }
    
  • 静态方法:锁为当前 Class 对象
    public static synchronized void classLock() {
        // code
    }
    
  • 实例方法:锁为当前实例
    public synchronized void instanceLock() {
        // code
    }
    

synchronized 是一个关键字,底层原理属于 JVM 层面。通过 monitorenter 指令和 monitorexit 指令实现同步,依赖于底层的 Mutex(互斥量)。唤醒和挂起一个线程都需要内核态和系统态之间的切换。

什么是系统态和内核态

blog_java_lock-1.png

用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。

系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。

当计算机系统执行用户应用时,系统处于用户模式。然而,当用户应用通过系统调用,请求操作系统服务时,系统必须从用户模式切换到内核模式,以满足请求

在 Java5 及之前 ,synchronized 都需要依赖操作系统,用户态和内核态间的切换需要相对较长的事件,这是为何 synchronized 效率较低的原因,也说明 synchronized 是一种 重量级锁

2. Java5

2.1. 原理说明

阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

因此在 Java5 的时候,便为了解决上述问题,引入了一下两项作为基本依赖。

  • CAS(无锁):
    • 说明:原子操作,确保多线程并发修改同一数值不发生错误。
    • 原理:比较并修改。此处不深入说明。
    • 实现:调用 Unsafe 中方法实现。
    • 具体实现类:java.util.concurrent.atomic包下的 Atomic 类,如 Atomicinteger。
  • 更灵活的阻塞机制
    • 原理:阻塞机制底层是 Linux 内核基于等待队列 wait_queue 和等待事件 wait_event 来实现的。 具体解析可以查看文末的参考文章
    • 实现:调用 Unsafe 中的方法实现,
    • 具体实现类:LockSupport

并在以上基础上提出了AQSAQSAbstractQueuedSynchronizer的简称,即抽象队列同步器,从字面意思上理解:

  • 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
  • 队列:使用先进先出(FIFO)队列存储数据;
  • 同步:实现了同步的功能。

blog_java_lock-5

AQS 是一个用来构建锁和同步器的框架模版,使用 AQS 能简单且高效地构造出应用广泛的同步器。

2.2. 新的锁与通信工具类

基于 CAS,LockSupport,以及 AQS,实现的新的锁和通信工具类如下:

blog_java_lock-2

2.3. 同步容器与并发容器

Java5 的时候也引入了并发容器。首先提一下这两个概念:

  • 同步容器
    • 说明:通过 synchronized 关键字修饰容器保证同一时刻内只有一个线程在使用容器,从而使得容器线程安全。
    • 原理:synchronized
    • 实现类:Vector,HashTable,Collections.SynchronizedXxxx
  • 并发容器
    • 说明:并发容器指的是允许多线程同时使用容器,并且保证线程安全。
    • 原理:锁、CAS、COW(读写分离)、分段锁(Java7 中提出)。
    • 实现类:

      blog_java_lock-3

同步容器实现同步的方式是通过对方法加锁(synchronized)方式实现的,这样读写均需要锁操作,导致性能低下。

而即使是 Vector 这样线程安全的类,在面对多线程下的复合操作的时候也是需要通过客户端加锁的方式保证原子性。比如一下示例(摘抄自《深入浅出Java多线程》):

public class TestVector {
    private Vector<String> vector;

    //方法一
    public  Object getLast(Vector vector) {
        int lastIndex = vector.size() - 1;
        return vector.get(lastIndex);
    }

    //方法二
    public  void deleteLast(Vector vector) {
        int lastIndex = vector.size() - 1;
        vector.remove(lastIndex);
    }

    //方法三
    public  Object getLastSysnchronized(Vector vector) {
        synchronized(vector){
            int lastIndex = vector.size() - 1;
            return vector.get(lastIndex);
        }
    }

    //方法四
    public  void deleteLastSysnchronized(Vector vector) {
        synchronized (vector){
            int lastIndex = vector.size() - 1;
            vector.remove(lastIndex);
        }
    }
}

如果方法一和方法二为一个组合的话。那么当方法一获取到了vector的size之后,方法二已经执行完毕,这样就导致程序的错误。

如果方法三与方法四组合的话。通过锁机制保证了在vector上的操作的原子性。

并发容器针对不同的应用场景进行设计,提高了容器的并发访问性,同时定义了线程安全的复合操作。在多线程编程下,推荐使用并发容器代替同步容器。

3. Java6

java1.5 并发包的作者是 Doug Lea,而非 Java 官方。也许是动摇了synchronized“亲儿子”的地位。在 jdk1.6 时,synchronized也迎来了一波优化。

synchronized 锁经过优化之后有以下四种状态:

  • 无锁(即 CAS):无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
  • 偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
  • 轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
  • 重量级锁:也就是 Java6 之前的 synchronized 锁。底层使用操作系统互斥量(Mutex)。

锁升级过程如下:

blog_java_lock-4

同时锁的升级是 不可逆的

4. 整理:锁的分类

根据上面提到的内容,可以按照以下方式对锁进行分类。

  • 锁的有无
    • 乐观锁:
      • 乐观锁又称为“无锁”,顾名思义,它是乐观派。
      • 乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。
      • 而一旦多个线程发生冲突,乐观锁通常是使用一种称为 CAS 的技术来保证线程执行的安全性。
    • 悲观锁
      • 悲观锁就是我们常说的锁。
      • 对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,
      • 所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
  • synchronized 与锁
    • 无锁
    • 偏向锁
    • 轻量锁
    • 重量锁

在1.5之前只有synchronized一个锁。

synchronized是可重入(也就是一个线程在获得锁的情况下,可以第二次获取同一个锁),非公平(不管谁先谁后,notifyAll之后一起抢,没有先后之分),排它(一个锁只能由一个线程获取),不可中断(在等待锁的过程中不可中断)的锁。

但1.5时新推出的锁,有的拥有和synchronized完全不同的性质,所以按照性质分类可以把锁分为:

  • 锁的性质分类
    • 可重入锁和非可重入锁
      • 可重入锁:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。
      • 非可重入锁:可以理解为加锁范围是调用,在一次调用不释放锁的情况下,无法再次获取锁,即使是同一个线程。
    • 公平锁与非公平锁
      • 公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。
      • 非公平锁:非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。线程调度交由操作系统。
    • 读写锁和排它锁
      • 读写锁:同一时刻只允许一个线程进行访问。
      • 排它锁:内部维护了两个锁:一个读锁,一个写锁。同一时刻可以有多个线程获得读锁。读读的过程共享,而读写、写读、写写的过程互斥

5. 参考资料

  1. 《深入浅出 Java 多线程》
  2. Unsafe 类 park 和 unpark 方法源码深入分析(mutex+cond)
  3. java 多线程发展史
  4. Java CAS 原理剖析
  5. Java 并发——关键字 synchronized 解析
  6. 思维导图(右上角MindMap按钮,加载可能会慢些)