JAVA基础-volatile 、final 、synchronized和ReentrantLock

78 阅读23分钟

volatile

volatile关键字的作用:实现内存可见性,防止重排序,保证单次读/写的原子性。

volatile可见性的实现原理

volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现。

内存屏障

  • 内存屏障,又称内存栅栏,是一个 CPU 指令。
  • 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。

lock 指令

在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令。

在 Pentium 和早期的 IA-32 处理器中,lock 前缀会使处理器执行当前指令时产生一个 LOCK# 信号,会对总线进行锁定,其它 CPU 对内存的读写请求都会被阻塞,直到锁释放。 后来的处理器,加锁操作是由高速缓存锁代替总线锁来处理。 因为锁总线的开销比较大,锁总线期间其他 CPU 没法访问内存。 这种场景多缓存的数据一致通过缓存一致性协议(MESI)来保证。

lock 前缀的指令在多核处理器下会引发两件事情:

  • 将当前处理器缓存行的数据写回到系统内存。
  • 写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。

缓存一致性

缓存是分段(line)的,一个段对应一块存储空间,称之为缓存行,它是 CPU 缓存中可分配的最小存储单元,大小 32 字节、64 字节、128 字节不等,这与 CPU 架构有关,通常来说是 64 字节。 LOCK# 因为锁总线效率太低,因此使用了多组缓存。 为了使其行为看起来如同一组缓存那样。因而设计了 缓存一致性协议。 缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于 " 嗅探(snooping)" 协议。 所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线。 缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个 CPU 缓存可以读写内存)。 CPU 缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。 当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。 只要某个处理器写内存,其它处理器马上知道这块内存在它们的缓存段中已经失效。

为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

所有多核处理器下还会完成:当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。

volatile有序性的实现原理

volatile 的 happens-before 关系

happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

volatile 禁止重排序

为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。

Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

JMM 会针对编译器制定 volatile 重排序规则表。“NO”表示禁止重排序。

image.png

为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。

image.png

image.png

image.png

final

final 基础使用

修饰类

当final修饰类时,表明这个类不能够被继承。即这个类不能有子类。 注意:final类的所有方法都隐式为final,所以给任何方法加final关键字没有任何意义。

修饰方法

  • final修饰的方法不能被重写。
  • private方法是隐式的final方法。
  • final方法可以被重载。

修饰参数

无法在方法中更改参数引用所指向的对象。这个特性主要用来向匿名内部类传递数据。

修饰变量

final修饰的变量,并不一定都是编译期常量。

    //编译期常量
    final int i = 1;
    final static int J = 1;
    final int[] a = {1,2,3,4};
    //非编译期常量
    final int k = new Random().nextInt();

static final

一个既是static又是final 的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过。

blank final

Java允许生成空白final,也就是说被声明为final但又没有给出定值的字段,但是必须在该字段被使用之前被赋值,这给予我们两种选择:

  • 在定义处进行赋值(这不叫空白final)
  • 在构造器中进行赋值,保证了该值在被使用前赋值。

final域 重排序规则

写final域 重排序规则

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。

写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:

  • JMM禁止编译器把final域的写重排序到构造函数之外;
  • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

image.png
由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。

读final域重排序规则

读final域 重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。

image.png

读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。

final域为引用类型

对final修饰的对象的成员域写操作

针对引用数据类型,final域写针对编译器和处理器重排序增加了这样的约束:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是使用。

对final修饰的对象的成员域读操作

暂无

按照final修饰的数据类型分类:

  • 基本数据类型:
    • final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
    • final域读:禁止初次读对象的引用与读该对象包含的final域的重排序。
  • 引用数据类型:
    • 额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序。

synchronized

synchronized使用:

  • 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
  • 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;
  • synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。

对象锁

对象锁有两种:

  • 方法锁(默认锁对象是当前实例对象,也就是this);
  • 同步代码块锁(可以使用this或者指定对象)。
public class SynchronizedTest {
    private  Object bLock=new Object();
    
    //synchronized方法锁,使用this对象进行加锁
    public synchronized void test1() {
        System.out.print("");
    }

    public void test2(){
        //synchronized 同步代码块锁,使用bLock对象加锁
        synchronized (bLock){
            System.out.print("");
        }
        //synchronized 同步代码块锁,使用this对象加锁
        synchronized (this){
            System.out.print("");
        }
    }
}

类锁

指synchronize修饰静态的方法或指定锁对象为Class对象

public class SynchronizedTest {
  
    //synchronized 类锁,使用SynchronizedTest.class对象进行加锁
    public static synchronized void test1() {
        System.out.print("");
    }

    public void test2(){
        //synchronized 同步代码块锁,使用SynchronizedTest.class对象加锁
        synchronized (SynchronizedTest.class){
            System.out.print("");
        }
    }
}

Synchronized原理分析

Synchronized加锁和释放锁原理

深入JVM字节码,使用javap 反编译下面代码的class文件

public class SynchronizedTest {
    private Object bLock = new Object();

    public void test() {
        //synchronized 同步代码块锁,使用SynchronizedTest.class对象加锁
        synchronized (bLock) {
            System.out.print("");
        }
    }
}

得到字节码信息:

image.png 关注红色方框里的monitorentermonitorexit即可。

MonitorenterMonitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

  • monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
  • 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
  • 这把锁已经被别的线程获取了,等待锁释放

monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

Synchronized可重入原理:加锁次数计数器

同一锁中,每个对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会加一,释放锁后就会将monitor计数器减一,线程不需要再次获取同一把锁。

可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

保证可见性的原理:内存模型和happens-before规则

Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。

JVM中锁的优化

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销

  • 锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
  • 锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本的Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
  • 轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒(具体处理步骤下面详细讨论)。
  • 偏向锁(Biased Locking):是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。
  • 适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。

锁的类型

在Java SE 1.6里Synchronied同步锁,一共有四种状态:无锁偏向锁轻量级锁重量级锁,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率。

锁膨胀方向: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (此过程是不可逆的)。

偏向锁

一开始的时候是无锁状态。然后此时第一个线程进来了,在对象头的Mark Word中看到此时是无锁状态,就把此时的锁升级为偏向锁,并将自己的线程id用CAS的方式赋值到Mark Word中。然后就进入到了该线程的同步块中。

轻量级锁

如果此时有第二个线程进来,它会去查看当前偏向锁指向的线程id是否是自己,结果发现不是,但是此时还是会CAS去尝试修改线程id指向自己,去赌一下第一个线程此时已经用完了释放了。如果释放了,它会将锁改为无锁状态,将线程id置空。然后第二个线程拿到这个资源,将线程id赋值给自己,锁升级为偏向锁。如果第一个线程此时没释放,则JVM会在第一个线程到达安全点的时候撤销当前的偏向锁。下一步当前线程栈中会分配锁记录,并拷贝Mark Word到锁记录中。然后两个线程用CAS的方式去修改Mark Word中的指针指向自己,假如说第一个线程修改成功了,然后将锁升级为轻量级锁,去执行同步语句块中的内容。

重量级锁

修改失败的第二个线程会进入自旋状态,自旋结束后会继续去尝试CAS修改指针指向自己。如果自旋失败超过一定次数的时候(这个次数会动态进行调整),会请求JVM将此时的锁状态升级为重量级锁,这是依赖于底层操作系统的调度库来实现的。接着将Mark Word指向重量级锁Monitor的指针,然后挂起当前第二个线程(被放在Monitor的_EntryList中)。等一个线程执行完毕后,会查看当前Mark Word中的指针是否仍然指向自己,如果是自己的话就释放锁,否则不是自己的话,说明此时已经升级成了重量级锁,除了释放锁之后,还会唤醒阻塞的线程,进行新一轮的锁竞争。在此之后,该锁就一直会是重量级锁存在了

锁升级的过程:

锁的升级过程是单向的,不能退化,只能是从偏向锁到轻量级锁到重量级锁的过程。记录这几种锁状态的标记是在对象头的Mark Word中(一个对象的结构分为对象头、实际存放的数据和补齐位。而一个对象头中又分为Mark Word标记字段、指向类元数据信息的指针和数组长度(只有数组对象才有)),32位虚拟机的情况如下所示(64位虚拟机Mark Word的结构和32位的差不太多,多了一些没有使用的bit位)

image.png 其中从偏向锁升级到轻量级锁的时候,会复制一份Mark Word到线程栈上去,里面记录了之前偏向锁的信息,而hashcode可以通过对象的hash方法计算出来。

image.png

synchronized的缺陷和注意事项

缺陷:

  • 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
  • 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
  • 无法知道是否成功获得锁

注意事项:

  • 锁对象不能为空,因为锁的信息都保存在对象头里
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
  • 避免死锁
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错。
  • synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。

ReentrantLock

ReentrantLock基本使用

ReentrantLock常用方法

image.png

ReentrantLock使用

使用ReentrantLock实现同步锁

public class ReentrantLockTest {
    private ReentrantLock mReentrantLock = new ReentrantLock();
    private int mNum = 0;

    public int test1() {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                mReentrantLock.lock();
                try {
                    for (int j = 0; j < 1000; j++) {
                        mNum++;
                    }
                } finally {
                    mReentrantLock.unlock();
                }
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.print(mNum);
        return mNum;
    }
}

ReentrantLock是可重入锁

public class ReentrantLockTest {
    private ReentrantLock mReentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
        new Thread(() -> reentrantLockTest.test1()).start();
    }

    public void test1() {
        mReentrantLock.lock();
        try {
            System.out.println(Thread.currentThread().getId()+" test1");
            test2();
        } finally {
            mReentrantLock.unlock();
        }
    }

    public void test2() {
        mReentrantLock.lock();
        try {
            System.out.println(Thread.currentThread().getId()+" test2");
        } finally {
            mReentrantLock.unlock();
        }
    }
}
//输出
14 test1
14 test2

ReentrantLock Condition的使用

public class ReentrantLockTest {
    private ReentrantLock mReentrantLock = new ReentrantLock();
    private Condition mCondition1=mReentrantLock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
        new Thread(() -> {
            reentrantLockTest.test1();
        }).start();
        Thread.sleep(100);
        new Thread(() -> reentrantLockTest.test2()).start();
        Thread.sleep(10000);
    }

    public void test1() {
        mReentrantLock.lock();
        try {
            System.out.println("进去test1");
            System.out.println("test1进入wait状态");
            mCondition1.await();
            System.out.println("test1被signal");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            mReentrantLock.unlock();
        }
    }

    public void test2() {
        mReentrantLock.lock();
        try {
            System.out.println("进去test2");
            mCondition1.signal();
            System.out.println("test2唤醒test1");
        }finally {
            mReentrantLock.unlock();
        }
    }
}
输出:
进去test1
test1进入wait状态
进去test2
test2唤醒test1
test1被signal

ReentrantLock源码解析

ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
继承关系:

image.png

Sync抽象类

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

   // 获取锁
    abstract void lock();

    // 非公平方式获取
    final boolean nonfairTryAcquire(int acquires) {
        // 当前线程
        final Thread current = Thread.currentThread();
        // 获取状态
        int c = getState();
        if (c == 0) {// 表示没有线程正在竞争该锁
            if (compareAndSetState(0, acquires)) {// 比较并设置状态成功,状态0表示锁没有被占用
               // 设置当前线程独占
                setExclusiveOwnerThread(current);
                return true;// 成功
            }
        }
        else if (current == getExclusiveOwnerThread()) {// 当前线程拥有该锁
            int nextc = c + acquires;// 增加重入次数
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);// 设置状态
            return true;// 成功
        }
        return false;// 失败
    }

    // 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())// 当前线程不为独占线程
            throw new IllegalMonitorStateException();// 抛出异常
        boolean free = false;/ 释放标识
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);// 已经释放,清空独占
        }
        setState(c);// 设置标识
        return free;
    }
    // 判断资源是否被当前线程占有
    protected final boolean isHeldExclusively() {
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }
    // 新生一个条件
    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    // 返回资源的占用线程
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
    // 返回状态
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }
    // 资源是否被占用
    final boolean isLocked() {
        return getState() != 0;
    }

    // 自定义反序列化逻辑
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

NonfairSync类

// 非公平锁
static final class NonfairSync extends Sync {
// 版本号
    private static final long serialVersionUID = 7316153563782823691L;

    // 获得锁
    final void lock() {
        if (compareAndSetState(0, 1))// 比较并设置状态成功,状态0表示锁没有被占用
            setExclusiveOwnerThread(Thread.currentThread());// 把当前线程设置独占了锁
        else // 锁已经被占用,或者set失败
            // 以独占模式获取对象,忽略中断
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

从lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。

FairSync类

// 公平锁
static final class FairSync extends Sync {
   // 版本序列化
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
    // 以独占模式获取对象,忽略中断
        acquire(1);
    }

  // 尝试公平获取锁
    protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取状态
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {// 不存在已经等待更久的线程并且比较并且设置状态成功
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;// 状态不为0,即资源已经被线程占据
            if (nextc < 0) // 超过了int的表示范围
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。

ReentrantLock

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
  
    private final Sync sync;

    public ReentrantLock() {
        sync = new NonfairSync();
    }

  
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }


    public void lock() {
        sync.lock();
    }

  
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

   ......

查看ReentrantLock源码发现,ReentrantLock的方法最终委托给NonfairSync和FairSync进行实现。

了解当ReentrantLock使用FairSync锁时大致流程

调用lock方法获取到锁和unlock方法调用流程:

image.png

image.png 调用lock方法没有获取到锁调用流程:

image.png 当锁被释放后没有获取到锁线程流程图:

image.png

ReentrantLock和synchronized区别

  • synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
  • synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock.isLocked判断;
  • synchronized是非公平锁,ReentrantLock可以通过设置fair来指定是公平还是非公平;
  • synchronized是不可以被中断的,而ReentrantLock.lockInterruptibly方法是可以被中断的;
  • 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;
  • ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
  • synchronized在特定的情况下对于已经在等待的线程可能是后来的线程先获得锁,而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;