【并发】J.U.C之Java锁

news2024/12/29 8:48:41

java锁

锁的种类和特点

在这里插入图片描述

无锁/偏向锁/轻量级锁/重量级锁

这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。

首先为什么Synchronized能实现线程同步?

在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。

Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?

我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:

锁状态存储内容存储内容
无锁对象的hashCode、对象分代年龄、是否是偏向锁(0)01
偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。

拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

整体的锁状态升级流程如下:

在这里插入图片描述

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

可重入锁/不可重入锁

可重入锁是指,在线程当前已经持有这把锁了,在不释放这把锁的情况下,再次获取这把锁。

不可重入锁是指,在线程当前已经只有这把锁了,必须得先释放掉持有的锁,才能够重新获取锁。

对于可重入锁而言,最典型的就是 ReentrantLock 了,正如它的名字一样,reentrant 的意思就是可重入,它也是 Lock 接口最主要的一个实现类。
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下。

还是打水的例子,有多个人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。

在这里插入图片描述

但如果是非可重入锁的话,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。

在这里插入图片描述

之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

在这里插入图片描述

共享锁/独占锁

最好能够全是共享锁和独占锁的是读写锁。读文件的时候,可以多个线程读取一个文件,但是在写入的时候只允许一个线程进行写入。

当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.

当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.

通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁长期占用, 而等待的写模式锁请求长期阻塞。
独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

下图为ReentrantReadWriteLock的部分源码:

在这里插入图片描述

我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。

在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识。 在最开始提及AQS的时候我们也提到了state字段(int类型,32位),该字段用来描述有多少线程获持有锁。

在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:

在这里插入图片描述

了解了概念之后我们再来看代码,先看写锁的加锁源码:

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState(); // 取到当前锁的个数
	int w = exclusiveCount(c); // 取写锁的个数w
	if (c != 0) { // 如果已经有线程持有了锁(c!=0)
    // (Note: if c != 0 and w == 0 then shared count != 0)
		if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败
			return false;
		if (w + exclusiveCount(acquires) > MAX_COUNT)    // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
      throw new Error("Maximum lock count exceeded");
		// Reentrant acquire
    setState(c + acquires);
    return true;
  }
  if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
		return false;
	setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
	return true;
}

这段代码首先取到当前锁的个数c,然后再通过c来获取写锁的个数w。因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount©; ),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。
在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。
如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败。
如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!
tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。

接着是读锁的代码:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;                                   // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

此时,我们再回头看一下互斥锁ReentrantLock中公平锁和非公平锁的加锁源码:
在这里插入图片描述

我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。

公平锁/非公平锁

公平锁是指,在线程拿不到锁的时候会进入到等待,开始排队,在队列中等待时间长的会优先分配到锁,先来先得。

非公平锁是指,在线程拿不到锁的时候,开始排队,但是分配上是会忽略掉排队时间,随机分配到一个线程上.
特点
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。
在这里插入图片描述

假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。

但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。
在这里插入图片描述

接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁。

img

根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

下面我们来看一下公平锁与非公平锁的加锁方法的源码:

img

通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。

img

再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

乐观锁/悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
在这里插入图片描述
根据从上面的概念描述我们可以发现:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
    光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:
// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {
	// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
	lock.lock();
	// 操作同步资源
	lock.unlock();
}

// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。
    当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:
在这里插入图片描述

根据定义我们可以看出各属性的作用:

  • unsafe: 获取并操作内存的数据。
  • valueOffset: 存储value在AtomicInteger中的偏移量。
  • value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。

接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:

// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:

  1. ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
    JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
    Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
cas自旋一直不成功会怎么样

会死循环
如何解决

  • 添加 volatile
volatile int value = 0;
 public void increase() {
     int oldValue;
     int newValue;
     do {
         //读取共享变量的当前值
         oldValue = value;
         //计算共享变量的新值
         newValue = oldValue + 1;
     } while (!cas(oldValue, newValue));
 }
  • 限制了CAS自旋的次数
    例如BlockingQueue的SynchronousQueue。

自旋锁/非自旋锁

自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。

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

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
在这里插入图片描述

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

在这里插入图片描述

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,本文中仅做名词介绍,不做深入讲解,感兴趣的同学可以自行查阅相关资料。

可中断锁/不可中断锁

在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。

JVM锁优化

synchronized和Lock的对比

lock的常用方法

lock的常用方法
方法概览:

public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();

}

lock()

用于加锁,然后在try代码块中进行相关业务逻辑的处理,然后在finally中释放锁。如果不进行try cache中释放,一旦发生异常,则无法正常释放锁。

lock() 在执行的过程中是不能被中断,一旦进入死锁那便会永久等待。

Lock lock = new ReentrantLock();
lock.lock();
try {
  // 进入锁的保护,处理代码
} finally{
  // 解锁
  lock.unlock();
}

tryLock()

为了避免lock()会永久等待的问题,tryLock()会尝试获取锁,如果锁没有被其他线程占用则直接获取到锁,否则立刻返回false。

通常情况下使用 if 语句判断 tryLock() 的返回结果,根据是否获取到锁来执行不同的业务逻辑

Lock lock = new ReentrantLock();
if(lock.tryLock()) {
     try{
         //处理任务
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则做其他事情
}

tryLock(long time, TimeUnit unit)

tryLock() 的重载方法是 tryLock(long time, TimeUnit unit),这个方法和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间。

如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。

这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时,在等待了一段指定的超时时间后,线程会主动放弃这把锁的获取,避免永久等待;

在等待的期间,也可以随时中断线程,这就避免了死锁的发生。

lockInterruptibly()

和lock类似,区别的是lockInterruptibly()能够响应线程中断。

Lock lock = new ReentrantLock();
try {
  locklockInterruptibly();
  try {
          System.out.println("操作资源");
  } finally {
      lock.unlock();
  }
} catch (InterruptedException e) {
    e.printStackTrace();
}

unlock()

对于 ReentrantLock 而言,执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0 就代表当前这把锁已经完全释放了。

如果减 1 后计数器不为 0,说明这把锁之前被“重入”了,那么锁并没有真正释放,仅仅是减少了持有的次数。

newCondition()

会生成一个,和锁对象绑定的Condition实例,用于处理线程的等待和通知。

public interface Condition {
     //使当前线程加入 await() 等待队列中,并释放当锁,当其他线程调用signal()会重新请求锁。与Object.wait()类似。
    void await() throws InterruptedException;

    //调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。
    //调用该方法后,结束等待的唯一方法是其它线程调用该条件对象的signal()或signalALL()方法。等待过程中如果当前线程被中断,该方法仍然会继续等待,同时保留该线程的中断状态。 
    void awaitUninterruptibly();

    // 调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。
    //nanosTimeout指定该方法等待信号的的最大时间(单位为纳秒)。若指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间;
    //若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数。 
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    //与await()基本一致,唯一不同点在于,指定时间之内没有收到signal()或signalALL()信号或者线程中断时该方法会返回false;其它情况返回true。
    boolean await(long time, TimeUnit unit) throws InterruptedException;

   //适用条件与行为与awaitNanos(long nanosTimeout)完全一样,唯一不同点在于它不是等待指定时间,而是等待由参数指定的某一时刻。
    boolean awaitUntil(Date deadline) throws InterruptedException;

    //唤醒一个在 await()等待队列中的线程。与Object.notify()相似
    void signal();

   //唤醒 await()等待队列中所有的线程。与object.notifyAll()相似
    void signalAll();
}

在这里插入图片描述

AQS

Java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略(【死磕Java并发】-----深入分析synchronized的实现原理),但是与Lock相比synchronized还是存在一些缺陷的:虽然synchronized提供了便捷性的隐式获取锁释放锁机制(基于JVM机制),但是它却缺少了获取锁与释放锁的可操作性,可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。

在介绍Lock之前,我们需要先熟悉一个非常重要的组件,掌握了该组件JUC包下面很多问题都不在是问题了。该组件就是AQS。 AQS,AbstractQueuedSynchronizer,即队列同步器。

它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者( Doug Lea )期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。

AQS解决了,实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。 在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩行,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。 AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。

AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

AQS主要提供了如下一些方法:

  • getState():返回同步状态的当前值;
  • setState(int newState):设置当前同步状态;
  • compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
  • tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取- 同步状态;
  • tryRelease(int arg):独占式释放同步状态;
  • tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
  • tryReleaseShared(int arg):共享式释放同步状态;
  • isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
  • acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
  • acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
  • tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
  • acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
  • acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
  • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;
  • release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
  • releaseShared(int arg):共享式释放同步状态;
    后面LZ将会就CLH队列,同步状态的获取、释放做详细介绍

CLH同步队列

AQS内部维护着一个FIFO队列,该队列就是CLH同步队列。 CLH同步队列是一个FIFO双向队列 ,AQS依赖它来完成 同步状态的管理 ,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。 在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),其定义如下:

static final class Node {
    /** 共享 */
    static final Node SHARED = new Node();

    /** 独占 */
    static final Node EXCLUSIVE = null;

    /**
     * 因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态;
     */
    static final int CANCELLED =  1;

    /**
     * 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
     */
    static final int SIGNAL    = -1;

    /**
     * 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,改节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
     */
    static final int CONDITION = -2;

    /**
     * 表示下一次共享式同步状态获取将会无条件地传播下去
     */
    static final int PROPAGATE = -3;

    /** 等待状态 */
    volatile int waitStatus;

    /** 前驱节点 */
    volatile Node prev;

    /** 后继节点 */
    volatile Node next;

    /** 获取同步状态的线程 */
    volatile Thread thread;

    Node nextWaiter;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {
    }

    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

CLH同步队列结构图如下:

202202131353247741.png

入列

学了数据结构的我们,CLH队列入列是再简单不过了,无非就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。代码我们可以看看addWaiter(Node node)方法:

private Node addWaiter(Node mode) {
    //新建Node
    Node node = new Node(Thread.currentThread(), mode);
    //快速尝试添加尾节点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //CAS设置尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //多次尝试
    enq(node);
    return node;
}

addWaiter(Node node)先通过快速尝试设置尾节点,如果失败,则调用enq(Node node)方法设置尾节点

    private Node enq(final Node node) {
    //多次尝试,直到成功为止
    for (;;) {
        Node t = tail;
        //tail不存在,设置为首节点
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //设置为尾节点
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在上面代码中,两个方法都是通过一个CAS方法compareAndSetTail(Node expect, Node update)来设置尾节点,该方法可以确保节点是线程安全添加的。在enq(Node node)方法中,AQS通过“死循环”的方式来保证节点可以正确添加,只有成功添加后,当前线程才会从该方法返回,否则会一直执行下去。 过程图如下:

202202131353253252.png

出列

CLH同步队列遵循FIFO,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单,head执行该节点并断开原首节点的next和当前节点的prev即可,注意在这个过程是不需要使用CAS来保证的,因为只有一个线程能够成功获取到同步状态。过程图如下:

202202131353260833.png

同步状态的获取与释放

在前面提到过,AQS是构建Java同步组件的基础,我们期待它能够成为实现大部分同步需求的基础。AQS的设计模式采用的模板方法模式,子类通过继承的方式,实现它的抽象方法来管理同步状态,对于子类而言它并没有太多的活要做,AQS提供了大量的模板方法来实现同步,主要是分为三类:独占式获取和释放同步状态、共享式获取和释放同步状态、查询同步队列中的等待线程情况。自定义子类使用AQS提供的模板方法就可以实现自己的同步语义。

独占式

独占式,同一时刻仅有一个线程持有同步状态。

独占式同步状态获取

acquire(int arg)方法为AQS提供的模板方法,该方法为独占式获取同步状态,但是该方法对中断不敏感,也就是说由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。代码如下:

public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

各个方法定义如下:

  1. tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。
  2. addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。
  3. acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
  4. selfInterrupt:产生一个中断。
    acquireQueued方法为一个自旋的过程,也就是说当前线程(Node)进入同步队列后,就会进入一个自旋的过程,每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。如下:
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        //中断标志
        boolean interrupted = false;
        /*
         * 自旋过程,其实就是一个死循环而已
         */
        for (;;) {
            //当前线程的前驱节点
            final Node p = node.predecessor();
            //当前线程的前驱节点是头结点,且同步状态成功
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //获取失败,线程等待--具体后面介绍
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

从上面代码中可以看到,当前线程会一直尝试获取同步状态,当然前提是只有其前驱节点为头结点才能够尝试获取同步状态,理由:

保持FIFO同步队列原则。
头节点释放同步状态后,将会唤醒其后继节点,后继节点被唤醒后需要检查自己是否为头节点。
acquire(int arg)方法流程图如下:

202202131353269881.png

独占式获取响应中断

AQS提供了acquire(int arg)方法以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中等待着获取同步状态。为了响应中断,AQS提供了acquireInterruptibly(int arg)方法,该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常InterruptedException。

public final void acquireInterruptibly(int arg)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            if (!tryAcquire(arg))
                doAcquireInterruptibly(arg);
        }

首先校验该线程是否已经中断了,如果是则抛出InterruptedException,否则执行tryAcquire(int arg)方法获取同步状态,如果获取成功,则直接返回,否则执行doAcquireInterruptibly(int arg)。doAcquireInterruptibly(int arg)定义如下:

   private void doAcquireInterruptibly(int arg)
            throws InterruptedException {
            final Node node = addWaiter(Node.EXCLUSIVE);
            boolean failed = true;
            try {
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        throw new InterruptedException();
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }

doAcquireInterruptibly(int arg)方法与acquire(int arg)方法仅有两个差别。1.方法声明抛出InterruptedException异常,2.在中断方法处不再是使用interrupted标志,而是直接抛出InterruptedException异常。

独占式超时获取

AQS除了提供上面两个方法外,还提供了一个增强版的方法:tryAcquireNanos(int arg,long nanos)。该方法为acquireInterruptibly方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回false,否则返回true。如下:

   public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

tryAcquireNanos(int arg, long nanosTimeout)方法超时获取最终是在doAcquireNanos(int arg, long nanosTimeout)中实现的,如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    //nanosTimeout <= 0
    if (nanosTimeout <= 0L)
        return false;
    //超时时间
    final long deadline = System.nanoTime() + nanosTimeout;
    //新增Node节点
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        //自旋
        for (;;) {
            final Node p = node.predecessor();
            //获取同步状态成功
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            /*
             * 获取失败,做超时、中断判断
             */
            //重新计算需要休眠的时间
            nanosTimeout = deadline - System.nanoTime();
            //已经超时,返回false
            if (nanosTimeout <= 0L)
                return false;
            //如果没有超时,则等待nanosTimeout纳秒
            //注:该线程会直接从LockSupport.parkNanos中返回,
            //LockSupport为JUC提供的一个阻塞和唤醒的工具类,后面做详细介绍
            if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            //线程是否已经中断了
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

针对超时控制,程序首先记录唤醒时间deadline ,deadline = System.nanoTime() + nanosTimeout(时间间隔)。如果获取同步状态失败,则需要计算出需要休眠的时间间隔nanosTimeout(= deadline - System.nanoTime()),如果nanosTimeout <= 0 表示已经超时了,返回false,如果大于spinForTimeoutThreshold(1000L)则需要休眠nanosTimeout ,如果nanosTimeout <= spinForTimeoutThreshold ,就不需要休眠了,直接进入快速自旋的过程。原因在于 spinForTimeoutThreshold 已经非常小了,非常短的时间等待无法做到十分精确,如果这时再次进行超时等待,相反会让nanosTimeout 的超时从整体上面表现得不是那么精确,所以在超时非常短的场景中,AQS会进行无条件的快速自旋。 整个流程如下:

202202131353277382.png

独占式同步状态释放

当线程获取同步状态后,执行完相应逻辑后就需要释放同步状态。AQS提供了release(int arg)方法释放同步状态:

public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }

该方法同样是先调用自定义同步器自定义的tryRelease(int arg)方法来释放同步状态,释放成功后,会调用unparkSuccessor(Node node)方法唤醒后继节点(如何唤醒LZ后面介绍)。 这里稍微总结下:

在AQS中维护着一个FIFO的同步队列,当线程获取同步状态失败后,则会加入到这个CLH同步队列的对尾并一直保持着自旋。在CLH同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。

共享式

共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

共享式同步状态获取

AQS提供acquireShared(int arg)方法共享式获取同步状态:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        //获取失败,自旋获取同步状态
        doAcquireShared(arg);
}

从上面程序可以看出,方法首先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。自选式获取同步状态如下:

                private void doAcquireShared(int arg) {
                    /共享式节点
                    final Node node = addWaiter(Node.SHARED);
                    boolean failed = true;
                    try {
                        boolean interrupted = false;
                        for (;;) {
                            //前驱节点
                            final Node p = node.predecessor();
                            //如果其前驱节点,获取同步状态
                            if (p == head) {
                                //尝试获取同步
                                int r = tryAcquireShared(arg);
                                if (r >= 0) {
                                    setHeadAndPropagate(node, r);
                                    p.next = null; // help GC
                                    if (interrupted)
                                        selfInterrupt();
                                    failed = false;
                                    return;
                                }
                            }
                            if (shouldParkAfterFailedAcquire(p, node) &&
                                    parkAndCheckInterrupt())
                                interrupted = true;
                        }
                    } finally {
                        if (failed)
                            cancelAcquire(node);
                    }
                }

tryAcquireShared(int arg)方法尝试获取同步状态,返回值为int,当其 >= 0 时,表示能够获取到同步状态,这个时候就可以从自旋过程中退出。 acquireShared(int arg)方法不响应中断,与独占式相似,AQS也提供了响应中断、超时的方法,分别是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg,long nanos),这里就不做解释了。

共享式同步状态释放

获取同步状态后,需要调用release(int arg)方法释放同步状态,方法如下:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

因为可能会存在多个线程同时进行释放同步状态资源,所以需要确保同步状态安全地成功释放,一般都是通过CAS和循环来完成的。

阻塞和唤醒线程

在线程获取同步状态时如果获取失败,则加入CLH同步队列,通过通过自旋的方式不断获取同步状态,但是在自旋的过程中则需要判断当前线程是否需要阻塞,其主要方法在acquireQueued():

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

通过这段代码我们可以看到,在获取同步状态失败后,线程并不是立马进行阻塞,需要检查该线程的状态,检查状态的方法为 shouldParkAfterFailedAcquire(Node pred, Node node) 方法,该方法主要靠前驱节点判断当前线程是否应该被阻塞,代码如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前驱节点
    int ws = pred.waitStatus;
    //状态为signal,表示当前线程处于等待状态,直接放回true
    if (ws == Node.SIGNAL)
        return true;
    //前驱节点状态 > 0 ,则为Cancelled,表明该节点已经超时或者被中断了,需要从同步队列中取消
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    }
    //前驱节点状态为Condition、propagate
    else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

这段代码主要检查当前线程是否需要被阻塞,具体规则如下:

  1. 如果当前线程的前驱节点状态为SINNAL,则表明当前线程需要被阻塞,调用unpark()方法唤醒,直接返回true,当前线程阻塞
  2. 如果当前线程的前驱节点状态为CANCELLED(ws > 0),则表明该线程的前驱节点已经等待超时或者被中断了,则需要从CLH队列中将该前驱节点删除掉,直到回溯到前驱节点状态 <= 0 ,返回false
  3. 如果前驱节点非SINNAL,非CANCELLED,则通过CAS的方式将其前驱节点设置为SINNAL,返回false
    如果 shouldParkAfterFailedAcquire(Node pred, Node node) 方法返回true,则调用parkAndCheckInterrupt()方法阻塞当前线程:
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

parkAndCheckInterrupt() 方法主要是把当前线程挂起,从而阻塞住线程的调用栈,同时返回当前线程的中断状态。其内部则是调用LockSupport工具类的park()方法来阻塞该方法。 当线程释放同步状态后,则需要唤醒该线程的后继节点:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

调用unparkSuccessor(Node node)唤醒后继节点:

private void unparkSuccessor(Node node) {
    //当前节点状态
    int ws = node.waitStatus;
    //当前状态 < 0 则设置为 0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //当前节点的后继节点
    Node s = node.next;
    //后继节点为null或者其状态 > 0 (超时或者被中断了)
    if (s == null || s.waitStatus > 0) {
        s = null;
        //从tail节点来找可用节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //唤醒后继节点
    if (s != null)
        LockSupport.unpark(s.thread);
}

可能会存在当前线程的后继节点为null,超时、被中断的情况,如果遇到这种情况了,则需要跳过该节点,但是为何是从tail尾节点开始,而不是从node.next开始呢?原因在于node.next仍然可能会存在null或者取消了,所以采用tail回溯办法找第一个可用的线程。最后调用LockSupport的unpark(Thread thread)方法唤醒该线程。

LockSupport

从上面我可以看到,当需要阻塞或者唤醒一个线程的时候,AQS都是使用LockSupport这个工具类来完成的。

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语

每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。但是注意许可不可重入,也就是说只能调用一次park()方法,否则会一直阻塞。 LockSupport定义了一系列以park开头的方法来阻塞当前线程,unpark(Thread thread)方法来唤醒一个被阻塞的线程。如下:

202202131353292211.png

park(Object blocker)方法的blocker参数,主要是用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。 park方法和unpark(Thread thread)都是成对出现的,同时unpark必须要在park执行之后执行,当然并不是说没有不调用unpark线程就会一直阻塞,park有一个方法,它带了时间戳(parkNanos(long nanos):为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用)。 park()方法的源码如下:

                public static void park() {
                    UNSAFE.park(false, 0L);
                }
unpark(Thread thread)方法源码如下:

                public static void unpark(Thread thread) {
                    if (thread != null)
                        UNSAFE.unpark(thread);
                }

从上面可以看出,其内部的实现都是通过UNSAFE(sun.misc.Unsafe UNSAFE)来实现的,其定义如下:

  public native void park(boolean var1, long var2);
  public native void unpark(Object var1);

两个都是native本地方法。Unsafe 是一个比较危险的类,主要是用于执行低级别、不安全的方法集合。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码才能获得该类的实例。

ReentrantLock 重入锁

ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。 API介绍如下:

一个可重入的互斥锁定 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
在这里插入图片描述

ReentrantLock和synchronized的相同点

ReentrantLock是独占锁且可重入的

例子

public class ReentrantLockTest {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {
            lock.lock();
        }

        for(int i=1;i<=3;i++){
            try {

            } finally {
                lock.unlock();
            }
        }
    }
}  

上面的代码通过lock()方法先获取锁三次,然后通过unlock()方法释放锁3次,程序可以正常退出。从上面的例子可以看出,ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。在加上ReentrantLock的的独占性,我们可以得出以下ReentrantLock和synchronized的相同点。

  1. ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
  2. ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
获取锁

我们一般都是这么使用ReentrantLock获取锁的:

            //非公平锁
            ReentrantLock lock = new ReentrantLock();
            lock.lock();

lock方法:


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

Sync为ReentrantLock里面的一个内部类,它继承AQS(AbstractQueuedSynchronizer),它有两个子类:公平锁FairSync和非公平锁NonfairSync。 ReentrantLock里面大部分的功能都是委托给Sync来实现的,同时Sync内部定义了lock()抽象方法由其子类去实现,默认实现了nonfairTryAcquire(int acquires)方法,可以看出它是非公平锁的默认实现方式。

释放锁

获取同步锁后,使用完毕则需要释放锁,ReentrantLock提供了unlock释放锁:

public void unlock() {
    sync.release(1);
}

unlock内部使用Sync的release(int arg)释放锁,release(int arg)是在AQS中定义的:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

与获取同步状态的acquire(int arg)方法相似,释放同步状态的tryRelease(int arg)同样是需要自定义同步组件自己实现:

protected final boolean tryRelease(int releases) {
    //减掉releases
    int c = getState() - releases;
    //如果释放的不是持有锁的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //state == 0 表示已经释放完全了,其他线程可以获取同步状态了
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

只有当同步状态彻底释放后该方法才会返回true。当state == 0 时,则将锁持有线程设置为null,free= true,表示释放成功。

ReentrantLock相比synchronized的额外功能
ReentrantLock可以实现公平锁。

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

ReentrantLock lock = new ReentrantLock(true);
继续跟进看下源码

/**  
* Creates an instance of {@code ReentrantLock} with the  
* given fairness policy.  
*  
* @param fair {@code true} if this lock should use a fair ordering policy  
*/
public ReentrantLock(boolean fair){   
    sync = fair ? new FairSync() : new NonfairSync(); 
}

可以看到公平锁和非公平锁的实现关键在于成员变量sync的实现不同,这是锁实现互斥同步的核心。
下面我们看非公平锁的lock()方法:

一个公平锁的例子

public class ReentrantLockTest {

      static Lock lock = new ReentrantLock(true);      

      public static void main(String[] args) throws InterruptedException {          

            for(int i=0;i<5;i++){        
                new Thread(new ThreadDemo(i)).start();    
            }    
       }      

       static class ThreadDemo implements Runnable {    
           Integer id;      

           public ThreadDemo(Integer id) {       
                this.id = id;       
           }      
      
           @Override    

        public void run() {         
               try {         
                   TimeUnit.MILLISECONDS.sleep(10);            
               } catch (InterruptedException e) {         
                    e.printStackTrace();            
               }             
               for(int i=0;i<2;i++){
                   lock.lock(); 
                   System.out.println("获得锁的线程:"+id);
                   lock.unlock();    
                }     
            }   
       } 
}

公平锁结果

获得锁的线程:4
获得锁的线程:1
获得锁的线程:3
获得锁的线程:0
获得锁的线程:2
获得锁的线程:4
获得锁的线程:1
获得锁的线程:3
获得锁的线程:0
获得锁的线程:2

我们开启5个线程,让每个线程都获取释放锁两次。为了能更好的观察到结果,在每次获取锁前让线程休眠10毫秒。可以看到线程几乎是轮流的获取到了锁。如果我们改成非公平锁,再看下结果

非公平锁结果

获得锁的线程:1
获得锁的线程:1
获得锁的线程:2
获得锁的线程:2
获得锁的线程:3
获得锁的线程:3
获得锁的线程:4
获得锁的线程:4
获得锁的线程:0
获得锁的线程:0

线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。

下面我们看非公平锁的lock()方法:

final void lock() {
    //尝试获取锁
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //获取失败,调用AQS的acquire(int arg)方法
        acquire(1);
}

首先会第一次尝试快速获取锁,如果获取失败,则调用acquire(int arg)方法,该方法定义在AQS中,如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个方法首先调用tryAcquire(int arg)方法,在AQS中讲述过,tryAcquire(int arg)需要自定义同步组件提供实现,非公平锁实现如下:

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

final boolean nonfairTryAcquire(int acquires) {
    //当前线程
    final Thread current = Thread.currentThread();
    //获取同步状态
    int c = getState();
    //state == 0,表示没有该锁处于空闲状态
    if (c == 0) {
        //获取锁成功,设置为当前线程所有
        if (compareAndSetState(0, acquires)) {
            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;
}
 

该方法主要逻辑:首先判断同步状态state == 0 ?,如果是表示该锁还没有被线程持有,直接通过CAS获取同步状态,如果成功返回true。如果state != 0,则判断当前线程是否为获取锁的线程,如果是则获取锁,成功返回true。成功获取锁的线程再次获取锁,这是增加了同步状态state。

公平锁与非公平锁

公平锁与非公平锁的区别在于获取锁的时候是否按照FIFO的顺序来。释放锁不存在公平性和非公平性,上面以非公平锁为例,下面我们来看看公平锁的tryAcquire(int arg):

                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;
                        if (nextc < 0)
                            throw new Error("Maximum lock count exceeded");
                        setState(nextc);
                        return true;
                    }
                    return false;
                }

比较非公平锁和公平锁获取同步状态的过程,会发现两者唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors(),定义如下:

            public final boolean hasQueuedPredecessors() {
                Node t = tail;  //尾节点
                Node h = head;  //头节点
                Node s;
        
                //头节点 != 尾节点
                //同步队列第一个节点不为null
                //当前线程是同步队列第一个节点
                return h != t &&
                        ((s = h.next) == null || s.thread != Thread.currentThread());
            }

该方法主要做一件事情:主要是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回true,否则返回false。
公平锁和非公平锁该如何选择
大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

ReentrantLock可响应中断

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

响应中断的例子

public class ReentrantLockTest{
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

          Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
          Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
          thread.start();
          thread1.start();  
          thread.interrupt();//是第一个线程中断
     }

      static class ThreadDemo implements Runnable{
            Lock firstLock;
            Lock secondLock;
            public ThreadDemo(Lock firstLock, Lock secondLock){
                 this.firstLock = firstLock;
                 this.secondLock = secondLock;
            }
            @Override
            public void run(){
               try {
                   firstLock.lockInterruptibly();
                   TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
                   secondLock.lockInterruptibly();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             } finally {
                 firstLock.unlock();
                 secondLock.unlock();
                 System.out.println(Thread.currentThread().getName()+"正常结束!");
             }
         }
     }
}

结果

java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.casic.ai.externaltools.ReentrantLockTest$ThreadDemo.run(ReentrantLockTest.java:29)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.casic.ai.externaltools.ReentrantLockTest$ThreadDemo.run(ReentrantLockTest.java:35)
	at java.lang.Thread.run(Thread.java:748)
Thread-1正常结束!

构造死锁场景:创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

获取锁时限时等待

ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。

更好的解决死锁的例子

public class ReentrantLockTest{
     static Lock lock1 = new ReentrantLock();
     static Lock lock2 = new ReentrantLock();
     public static void main(String[] args) throws InterruptedException {

          Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
          Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
          thread.start();
          thread1.start();
     }

      static class ThreadDemo implements Runnable{
         Lock firstLock;
         Lock secondLock;
         public ThreadDemo(Lock firstLock, Lock secondLock){
              this.firstLock = firstLock;
              this.secondLock = secondLock;
         }
         @Override
         public void run() {
             try {
                 while(!lock1.tryLock()){
                     TimeUnit.MILLISECONDS.sleep(10);
                 }
                 while(!lock2.tryLock()){
                     lock1.unlock();
                     TimeUnit.MILLISECONDS.sleep(10);
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             } finally {
                 firstLock.unlock();
                 secondLock.unlock();
                 System.out.println(Thread.currentThread().getName()+"正常结束!");
             }
         }
     }
}

结果

Thread-1正常结束!
Thread-0正常结束!

线程通过调用tryLock()方法获取锁,第一次获取锁失败时会休眠10毫秒,然后重新获取,直到获取成功。第二次获取失败时,首先会释放第一把锁,再休眠10毫秒,然后重试直到成功为止。线程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,避免了两个线程分别持有一把锁然后相互请求另一把锁。

总结

ReentrantLock是可重入的独占锁。比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。

ReentrantReadWriteLock 读写锁

读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。

读写锁的主要特性:

  • 公平性:支持公平性和非公平性。
  • 重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
  • 锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁

ReadWriteLock 和 ReentrantReadWriteLock介绍

ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。

“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。

“写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。

注意:不能同时存在读取锁和写入锁!

很多并发情况下,之前介绍的锁机制例如ReentrantLock重入锁,只允许单个线程执行受保护的程序,而在大部分应用中都是读的操作次数远远大于写操作次数。如果使用ReentrantLock锁会严重影响整体的性能,如果使用ReentrantReadWriteLock可以实现并发访问的情况下,读可以多线程同时访问,而写只有一个线程可以执行。他允许多读,但是不允许读和写同时发生。

(1)读-读不互斥:读读之间不阻塞

(2)读-写互斥:读会阻塞写,写也会阻塞读

(3)写-写互斥:写写阻塞

应用举例

public class ReentrantReadWriteLockTest {
    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();
    private final Lock writeLock = readWriteLock.writeLock();

    public String read() {
        readLock.lock();
        try {
            //执行的代码快
            return "xxx";
        } finally {
            readLock.unlock();
        }
    }

    public void write() {
        writeLock.lock();
        try {
            //执行写操作代码快
            System.out.println("执行写操作代码快");
        } finally {
            writeLock.unlock();
        }
    }
}
  1. 实现原理解读

ReentrantReadWriteLock实现的原理依然是内部类Sync继承AQS类,实现方式也是对state及waitStatus值进行操作。实现读写锁需要分别记录读锁状态和写锁状态,并且等待列队中需要区别处理两种加锁操作。使用state值同时记录读写锁状态是将int类型的state变量分为高16位和低16位,读写状态的发生操作的就是state值高16位和低16位的改变,这样就类似于ReentrantLock,只是读的过程可以允许多个线程同时发生,不需要加入到等待列队中,而写则依然需要加入等待列队
在这里插入图片描述

ReadWriteLock是一个接口。ReentrantReadWriteLock是它的实现类,ReentrantReadWriteLock包括子类ReadLock和WriteLock。

ReadWriteLock 和 ReentrantReadWriteLock函数列表

ReadWriteLock函数列表

// 返回用于读取操作的锁。
Lock readLock()
// 返回用于写入操作的锁。
Lock writeLock()

ReentrantReadWriteLock函数列表

// 创建一个新的 ReentrantReadWriteLock,默认是采用“非公平策略”。
ReentrantReadWriteLock()
// 创建一个新的 ReentrantReadWriteLock,fair是“公平策略”。fair为true,意味着公平策略;否则,意味着非公平策略。
ReentrantReadWriteLock(boolean fair)
 
// 返回当前拥有写入锁的线程,如果没有这样的线程,则返回 null。
protected Thread getOwner()
// 返回一个 collection,它包含可能正在等待获取读取锁的线程。
protected Collection<Thread> getQueuedReaderThreads()
// 返回一个 collection,它包含可能正在等待获取读取或写入锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回一个 collection,它包含可能正在等待获取写入锁的线程。
protected Collection<Thread> getQueuedWriterThreads()
// 返回等待获取读取或写入锁的线程估计数目。
int getQueueLength()
// 查询当前线程在此锁上保持的重入读取锁数量。
int getReadHoldCount()
// 查询为此锁保持的读取锁数量。
int getReadLockCount()
// 返回一个 collection,它包含可能正在等待与写入锁相关的给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回正等待与写入锁相关的给定条件的线程估计数目。
int getWaitQueueLength(Condition condition)
// 查询当前线程在此锁上保持的重入写入锁数量。
int getWriteHoldCount()
// 查询是否给定线程正在等待获取读取或写入锁。
boolean hasQueuedThread(Thread thread)
// 查询是否所有的线程正在等待获取读取或写入锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与写入锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果此锁将公平性设置为 ture,则返回 true。
boolean isFair()
// 查询是否某个线程保持了写入锁。
boolean isWriteLocked()
// 查询当前线程是否保持了写入锁。
boolean isWriteLockedByCurrentThread()
// 返回用于读取操作的锁。
ReentrantReadWriteLock.ReadLock readLock()
// 返回用于写入操作的锁。
ReentrantReadWriteLock.WriteLock writeLock()
ReentrantReadWriteLock数据结构

ReentrantReadWriteLock的UML类图如下:
在这里插入图片描述

从中可以看出:

(01) ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了"获取读锁的readLock()函数" 和 “获取写锁的writeLock()函数”。

(02) ReentrantReadWriteLock中包含:sync对象,读锁readerLock和写锁writerLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。读锁ReadLock和写锁WriteLock中也都分别包含了"Sync对象",它们的Sync对象和ReentrantReadWriteLock的Sync对象 是一样的,就是通过sync,读锁和写锁实现了对同一个对象的访问。

(03) 和"ReentrantLock"一样,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类。Sync也包括"公平锁"FairSync和"非公平锁"NonfairSync。sync对象是"FairSync"和"NonfairSync"中的一个,默认是"NonfairSync"。

所以ReentrantReadWriteLock实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样而已,它的读写锁其实就是两个类:ReadLock、writeLock,这两个类都是lock实现。 在ReentrantLock中使用一个int类型的state来表示同步状态,该值表示锁被一个线程重复获取的次数。但是读写锁ReentrantReadWriteLock内部维护着两个一对锁,需要用一个变量维护多种状态。所以读写锁采用“按位切割使用”的方式来维护这个变量,将其切分为两部分,高16为表示读,低16为表示写。分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过为运算。假如当前同步状态为S,那么写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S >>> 16(无符号补0右移16位)。代码如下:

                    static final int SHARED_SHIFT   = 16;
                    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
                    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
                    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
            
                    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
                    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

其中,共享锁源码相关的代码如下:

public static class ReadLock implements Lock, java.io.Serializable {
  private static final long serialVersionUID = -5992448646407690164L;
  // ReentrantReadWriteLock的AQS对象
  private final Sync sync;
  protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
  }
  // 获取“共享锁”
  public void lock() {
    sync.acquireShared(1);
  }
  // 如果线程是中断状态,则抛出一场,否则尝试获取共享锁。
  public void lockInterruptibly() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
  }
  // 尝试获取“共享锁”
  public boolean tryLock() {
    return sync.tryReadLock();
  }
 
  // 在指定时间内,尝试获取“共享锁”
  public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
  }
  // 释放“共享锁”
  public void unlock() {
    sync.releaseShared(1);
  }
  // 新建条件
  public Condition newCondition() {
    throw new UnsupportedOperationException();
  }
  public String toString() {
    int r = sync.getReadLockCount();
    return super.toString() +
      "[Read locks = " + r + "]";
  }
}

说明:

ReadLock中的sync是一个Sync对象,Sync继承于AQS类,即Sync就是一个锁。ReentrantReadWriteLock中也有一个Sync对象,而且ReadLock中的sync和ReentrantReadWriteLock中的sync是对应关系。即ReentrantReadWriteLock和ReadLock共享同一个AQS对象,共享同一把锁。

ReentrantReadWriteLock中Sync的定义如下:

final Sync sync;

下面,分别从“获取共享锁”和“释放共享锁”两个方面对共享锁进行说明。

获取共享锁

获取共享锁的思想(即lock函数的步骤),是先通过tryAcquireShared()尝试获取共享锁。尝试成功的话,则直接返回;尝试失败的话,则通过doAcquireShared()不断的循环并尝试获取锁,若有需要,则阻塞等待。doAcquireShared()在循环中每次尝试获取锁时,都是通过tryAcquireShared()来进行尝试的。下面看看“获取共享锁”的详细流程。

lock()

lock()在ReadLock中,源码如下:

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

Sync继承于AQS,acquireShared()定义在AQS中。源码如下:

public final void acquireShared(int arg) {
  if (tryAcquireShared(arg) < 0)
    doAcquireShared(arg);
}

说明:acquireShared()首先会通过tryAcquireShared()来尝试获取锁。

尝试成功的话,则不再做任何动作(因为已经成功获取到锁了)。

尝试失败的话,则通过doAcquireShared()来获取锁。doAcquireShared()会获取到锁了才返回。

tryAcquireShared()

tryAcquireShared()定义在ReentrantReadWriteLock.java的Sync中,源码如下:

protected final int tryAcquireShared(int unused) {
  Thread current = Thread.currentThread();
  // 获取“锁”的状态
  int c = getState();
  // 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。
  if (exclusiveCount(c) != 0 &&
    getExclusiveOwnerThread() != current)
    return -1;
  // 获取“读取锁”的共享计数
  int r = sharedCount(c);
  // 如果“不需要阻塞等待”,并且“读取锁”的共享计数小于MAX_COUNT;
  // 则通过CAS函数更新“锁的状态”,将“读取锁”的共享计数+1。
  if (!readerShouldBlock() &&
    r < MAX_COUNT &&
    compareAndSetState(c, c + SHARED_UNIT)) {
    // 第1次获取“读取锁”。
    if (r == 0) { 
      firstReader = current;
      firstReaderHoldCount = 1;
    // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程
    } else if (firstReader == current) { 
      firstReaderHoldCount++;
    } else {
      // HoldCounter是用来统计该线程获取“读取锁”的次数。
      HoldCounter rh = cachedHoldCounter;
      if (rh == null || rh.tid != current.getId())
        cachedHoldCounter = rh = readHolds.get();
      else if (rh.count == 0)
        readHolds.set(rh);
      // 将该线程获取“读取锁”的次数+1。
      rh.count++;
    }
    return 1;
  }
  return fullTryAcquireShared(current);
}

说明:tryAcquireShared()的作用是尝试获取“共享锁”。

如果在尝试获取锁时,“不需要阻塞等待”并且“读取锁的共享计数小于MAX_COUNT”,则直接通过CAS函数更新“读取锁的共享计数”,以及将“当前线程获取读取锁的次数+1”。否则,通过fullTryAcquireShared()获取读取锁。

fullTryAcquireShared()

fullTryAcquireShared()在ReentrantReadWriteLock中定义,源码如下:

final int fullTryAcquireShared(Thread current) {
  HoldCounter rh = null;
  for (;;) {
    // 获取“锁”的状态
    int c = getState();
    // 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。
    if (exclusiveCount(c) != 0) {
      if (getExclusiveOwnerThread() != current)
        return -1;
    // 如果“需要阻塞等待”。
    // (01) 当“需要阻塞等待”的线程是第1个获取锁的线程的话,则继续往下执行。
    // (02) 当“需要阻塞等待”的线程获取锁的次数=0时,则返回-1。
    } else if (readerShouldBlock()) {
      // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程
      if (firstReader == current) {
      } else {
        if (rh == null) {
          rh = cachedHoldCounter;
          if (rh == null || rh.tid != current.getId()) {
            rh = readHolds.get();
            if (rh.count == 0)
              readHolds.remove();
          }
        }
        // 如果当前线程获取锁的计数=0,则返回-1。
        if (rh.count == 0)
          return -1;
      }
    }
    // 如果“不需要阻塞等待”,则获取“读取锁”的共享统计数;
    // 如果共享统计数超过MAX_COUNT,则抛出异常。
    if (sharedCount(c) == MAX_COUNT)
      throw new Error("Maximum lock count exceeded");
    // 将线程获取“读取锁”的次数+1。
    if (compareAndSetState(c, c + SHARED_UNIT)) {
      // 如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount。
      if (sharedCount(c) == 0) {
        firstReader = current;
        firstReaderHoldCount = 1;
      // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,
      // 则将firstReaderHoldCount+1。
      } else if (firstReader == current) {
        firstReaderHoldCount++;
      } else {
        if (rh == null)
          rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
          rh = readHolds.get();
        else if (rh.count == 0)
          readHolds.set(rh);
        // 更新线程的获取“读取锁”的共享计数
        rh.count++;
        cachedHoldCounter = rh; // cache for release
      }
      return 1;
    }
  }
}

说明:fullTryAcquireShared()会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1。

doAcquireShared()

doAcquireShared()定义在AQS函数中,源码如下:

private void doAcquireShared(int arg) {
  // addWaiter(Node.SHARED)的作用是,创建“当前线程”对应的节点,并将该线程添加到CLH队列中。
  final Node node = addWaiter(Node.SHARED);
  boolean failed = true;
  try {
    boolean interrupted = false;
    for (;;) {
      // 获取“node”的前一节点
      final Node p = node.predecessor();
      // 如果“当前线程”是CLH队列的表头,则尝试获取共享锁。
      if (p == head) {
        int r = tryAcquireShared(arg);
        if (r >= 0) {
          setHeadAndPropagate(node, r);
          p.next = null; // help GC
          if (interrupted)
            selfInterrupt();
          failed = false;
          return;
        }
      }
      // 如果“当前线程”不是CLH队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待,
      // 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。
      if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
        interrupted = true;
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}

说明:doAcquireShared()的作用是获取共享锁。

它会首先创建线程对应的CLH队列的节点,然后将该节点添加到CLH队列中。CLH队列是管理获取锁的等待线程的队列。
如果“当前线程”是CLH队列的表头,则尝试获取共享锁;否则,则需要通过shouldParkAfterFailedAcquire()判断是否阻塞等待,需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。

doAcquireShared()会通过for循环,不断的进行上面的操作;目的就是获取共享锁。需要注意的是:doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!

释放共享锁

释放共享锁的思想,是先通过tryReleaseShared()尝试释放共享锁。尝试成功的话,则通过doReleaseShared()唤醒“其他等待获取共享锁的线程”,并返回true;否则的话,返回flase。

unlock()
public void unlock() {
  sync.releaseShared(1);
}

说明:该函数实际上调用releaseShared(1)释放共享锁。

  1. releaseShared()
releaseShared()AQS中实现,源码如下:

public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) {
    doReleaseShared();
    return true;
  }
  return false;
}

说明:releaseShared()的目的是让当前线程释放它所持有的共享锁。

它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放共享锁。

tryReleaseShared()

tryReleaseShared()定义在ReentrantReadWriteLock中,源码如下:

protected final boolean tryReleaseShared(int unused) {
  // 获取当前线程,即释放共享锁的线程。
  Thread current = Thread.currentThread();
  // 如果想要释放锁的线程(current)是第1个获取锁(firstReader)的线程,
  // 并且“第1个获取锁的线程获取锁的次数”=1,则设置firstReader为null;
  // 否则,将“第1个获取锁的线程的获取次数”-1。
  if (firstReader == current) {
    // assert firstReaderHoldCount > 0;
    if (firstReaderHoldCount == 1)
      firstReader = null;
    else
      firstReaderHoldCount--;
  // 获取rh对象,并更新“当前线程获取锁的信息”。
  } else {
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != current.getId())
      rh = readHolds.get();
    int count = rh.count;
    if (count <= 1) {
      readHolds.remove();
      if (count <= 0)
        throw unmatchedUnlockException();
    }
    --rh.count;
  }
  for (;;) {
    // 获取锁的状态
    int c = getState();
    // 将锁的获取次数-1。
    int nextc = c - SHARED_UNIT;
    // 通过CAS更新锁的状态。
    if (compareAndSetState(c, nextc))
      return nextc == 0;
  }
}

说明:tryReleaseShared()的作用是尝试释放共享锁。

doReleaseShared()

doReleaseShared()定义在AQS中,源码如下:

private void doReleaseShared() {
  for (;;) {
    // 获取CLH队列的头节点
    Node h = head;
    // 如果头节点不为null,并且头节点不等于tail节点。
    if (h != null && h != tail) {
      // 获取头节点对应的线程的状态
      int ws = h.waitStatus;
      // 如果头节点对应的线程是SIGNAL状态,则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。
      if (ws == Node.SIGNAL) {
        // 设置“头节点对应的线程状态”为空状态。失败的话,则继续循环。
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
          continue;
        // 唤醒“头节点的下一个节点所对应的线程”。
        unparkSuccessor(h);
      }
      // 如果头节点对应的线程是空状态,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态。
      else if (ws == 0 &&
           !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;        // loop on failed CAS
    }
    // 如果头节点发生变化,则继续循环。否则,退出循环。
    if (h == head)          // loop if head changed
      break;
  }
}

说明:doReleaseShared()会释放“共享锁”。它会从前往后的遍历CLH队列,依次“唤醒”然后“执行”队列中每个节点对应的线程;最终的目的是让这些线程释放它们所持有的锁。

HoldCounter

在读锁获取锁和释放锁的过程中,我们一直都可以看到一个变量rh (HoldCounter ),该变量在读锁中扮演着非常重要的作用。 我们了解读锁的内在机制其实就是一个共享锁,为了更好理解HoldCounter ,我们暂且认为它不是一个锁的概率,而相当于一个计数器。一次共享锁的操作就相当于在该计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以HoldCounter的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。我们先看HoldCounter的定义:

                    static final class HoldCounter {
                        int count = 0;
                        final long tid = getThreadId(Thread.currentThread());
                    }

HoldCounter 定义非常简单,就是一个计数器count 和线程 id tid 两个变量。按照这个意思我们看到HoldCounter 是需要和某给线程进行绑定了,我们知道如果要将一个对象和线程绑定仅仅有tid是不够的,而且从上面的代码我们可以看到HoldCounter 仅仅只是记录了tid,根本起不到绑定线程的作用。那么怎么实现呢?答案是ThreadLocal,定义如下:

                    static final class ThreadLocalHoldCounter
                        extends ThreadLocal<HoldCounter> {
                        public HoldCounter initialValue() {
                            return new HoldCounter();
                        }
                    }

通过上面代码HoldCounter就可以与线程进行绑定了。故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。 看到这里我们明白了HoldCounter作用了,我们在看一个获取读锁的代码段:

                            else if (firstReader == current) {
                                firstReaderHoldCount++;
                            } else {
                                if (rh == null)
                                    rh = cachedHoldCounter;
                                if (rh == null || rh.tid != getThreadId(current))
                                    rh = readHolds.get();
                                else if (rh.count == 0)
                                    readHolds.set(rh);
                                rh.count++;
                                cachedHoldCounter = rh; // cache for release
                            }

这段代码涉及了几个变量:firstReader 、firstReaderHoldCount、cachedHoldCounter 。我们先理清楚这几个变量:

            private transient Thread firstReader = null;
            private transient int firstReaderHoldCount;
            private transient HoldCounter cachedHoldCounter;

firstReader 看名字就明白了为第一个获取读锁的线程,firstReaderHoldCount为第一个获取读锁的重入数,cachedHoldCounter为HoldCounter的缓存。 理清楚上面所有的变量了,HoldCounter也明白了,我们就来给上面那段代码标明注释,如下:

                //如果获取读锁的线程为第一次获取读锁的线程,则firstReaderHoldCount重入数 + 1
                else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    //非firstReader计数
                    if (rh == null)
                        rh = cachedHoldCounter;
                    //rh == null 或者 rh.tid != current.getId(),需要获取rh
                    if (rh == null || rh.tid != getThreadId(current))
                        rh = readHolds.get();
                        //加入到readHolds中
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    //计数+1
                    rh.count++;
                    cachedHoldCounter = rh; // cache for release
                }

这里解释下为何要引入firstRead、firstReaderHoldCount。这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。

锁降级

上开篇是LZ就阐述了读写锁有一个特性就是锁降级,锁降级就意味着写锁是可以降级为读锁的,但是需要遵循先获取写锁、获取读锁在释放写锁的次序。注意如果当前线程先获取写锁,然后释放写锁,再获取读锁这个过程不能称之为锁降级,锁降级一定要遵循那个次序。 在获取读锁的方法tryAcquireShared(int unused)中,有一段代码就是来判读锁降级的:

                    int c = getState();
                    //exclusiveCount(c)计算写锁
                    //如果存在写锁,且锁的持有者不是当前线程,直接返回-1
                    //存在锁降级问题,后续阐述
                    if (exclusiveCount(c) != 0 &&
                            getExclusiveOwnerThread() != current)
                        return -1;
                    //读锁
                    int r = sharedCount(c);

锁降级中读锁的获取释放为必要?肯定是必要的。试想,假如当前线程A不获取读锁而是直接释放了写锁,这个时候另外一个线程B获取了写锁,那么这个线程B对数据的修改是不会对当前线程A可见的。如果获取了读锁,则线程B在获取写锁过程中判断如果有读锁还没有释放则会被阻塞,只有当前线程A释放读锁后,线程B才会获取写锁成功。

公平共享锁和非公平共享锁

和互斥锁ReentrantLock一样,ReadLock也分为公平锁和非公平锁。

公平锁和非公平锁的区别,体现在判断是否需要阻塞的函数readerShouldBlock()是不同的。

公平锁的readerShouldBlock()的源码如下:

final boolean readerShouldBlock() {
  return hasQueuedPredecessors();
}

在公平共享锁中,如果在当前线程的前面有其他线程在等待获取共享锁,则返回true;否则,返回false。

非公平锁的readerShouldBlock()的源码如下:

final boolean readerShouldBlock() {
  return apparentlyFirstQueuedIsExclusive();
}

在非公平共享锁中,它会无视当前线程的前面是否有其他线程在等待获取共享锁。只要该非公平共享锁对应的线程不为null,则返回true。

ReentrantReadWriteLock示例

import java.util.concurrent.locks.ReadWriteLock; 
import java.util.concurrent.locks.ReentrantReadWriteLock; 
public class ReadWriteLockTest1 { 
  public static void main(String[] args) { 
    // 创建账户
    MyCount myCount = new MyCount("4238920615242830", 10000); 
    // 创建用户,并指定账户
    User user = new User("Tommy", myCount); 
    // 分别启动3个“读取账户金钱”的线程 和 3个“设置账户金钱”的线程
    for (int i=0; i<3; i++) {
      user.getCash();
      user.setCash((i+1)*1000);
    }
  } 
} 
class User {
  private String name;      //用户名 
  private MyCount myCount;    //所要操作的账户 
  private ReadWriteLock myLock;  //执行操作所需的锁对象 
  User(String name, MyCount myCount) {
    this.name = name; 
    this.myCount = myCount; 
    this.myLock = new ReentrantReadWriteLock();
  }
  public void getCash() {
    new Thread() {
      public void run() {
        myLock.readLock().lock(); 
        try {
          System.out.println(Thread.currentThread().getName() +" getCash start"); 
          myCount.getCash();
          Thread.sleep(1);
          System.out.println(Thread.currentThread().getName() +" getCash end"); 
        } catch (InterruptedException e) {
        } finally {
          myLock.readLock().unlock(); 
        }
      }
    }.start();
  }
  public void setCash(final int cash) {
    new Thread() {
      public void run() {
        myLock.writeLock().lock(); 
        try {
          System.out.println(Thread.currentThread().getName() +" setCash start"); 
          myCount.setCash(cash);
          Thread.sleep(1);
          System.out.println(Thread.currentThread().getName() +" setCash end"); 
        } catch (InterruptedException e) {
        } finally {
          myLock.writeLock().unlock(); 
        }
      }
    }.start();
  }
}
class MyCount {
  private String id;     //账号 
  private int  cash;    //账户余额 
  MyCount(String id, int cash) { 
    this.id = id; 
    this.cash = cash; 
  } 
  public String getId() { 
    return id; 
  } 
  public void setId(String id) { 
    this.id = id; 
  } 
  public int getCash() { 
    System.out.println(Thread.currentThread().getName() +" getCash cash="+ cash); 
    return cash; 
  } 
  public void setCash(int cash) { 
    System.out.println(Thread.currentThread().getName() +" setCash cash="+ cash); 
    this.cash = cash; 
  } 
}

运行结果:

Thread-0 getCash start
Thread-2 getCash start
Thread-0 getCash cash=10000
Thread-2 getCash cash=10000
Thread-0 getCash end
Thread-2 getCash end
Thread-1 setCash start
Thread-1 setCash cash=1000
Thread-1 setCash end
Thread-3 setCash start
Thread-3 setCash cash=2000
Thread-3 setCash end
Thread-4 getCash start
Thread-4 getCash cash=2000
Thread-4 getCash end
Thread-5 setCash start
Thread-5 setCash cash=3000
Thread-5 setCash end

结果说明:

(01) 观察Thread0和Thread-2的运行结果,我们发现,Thread-0启动并获取到“读取锁”,在它还没运行完毕的时候,Thread-2也启动了并且也成功获取到“读取锁”。

因此,“读取锁”支持被多个线程同时获取。

(02) 观察Thread-1,Thread-3,Thread-5这三个“写入锁”的线程。只要“写入锁”被某线程获取,则该线程运行完毕了,才释放该锁。

因此,“写入锁”不支持被多个线程同时获取。

【死磕Java并发】—–J.U.C之读写锁:ReentrantReadWriteLock

Condition

在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE5后,Java提供了Lock接口,相对于Synchronized而言,Lock提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活。下图是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):

202202131353311611.png

Condition提供了一系列的方法来对阻塞和唤醒线程:

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = 4. nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
  6. signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  7. signal()All :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

Condition使用简介

Condition由ReentrantLock对象创建,并且可以同时创建多个

static Condition notEmpty = lock.newCondition();  

static Condition notFull = lock.newCondition();

Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify类似。

一个使用condition的简单例子

public class ConditionTest {

     static ReentrantLock lock = new ReentrantLock();
     static Condition condition = lock.newCondition();
     public static void main(String[] args) throws InterruptedException {

          lock.lock();
          new Thread(new SignalThread()).start();
          System.out.println("主线程等待通知");
         try {
             condition.await();
         } finally {
             lock.unlock();
         }
         System.out.println("主线程恢复运行");
     }
     static class Signal Thread implements Runnable {

          @Override
          publicvoidrun() {
             lock.lock();
             try {
                 condition.signal();
                 System.out.println("子线程通知");
             } finally {
                 lock.unlock();
             }
         }
     }
}

运行结果

主线程等待通知
子线程通知
主线程恢复运行
使用Condition实现简单的阻塞队列

阻塞队列是一种特殊的先进先出队列,它有以下几个特点
1.入队和出队线程安全
2.当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。

阻塞队列的简单实现

public class MyBlockingQueue<E> {

    int size;//阻塞队列最大容量

    ReentrantLock lock = new ReentrantLock();

    LinkedList<E> list=new LinkedList<>();//队列底层实现

    Condition notFull = lock.newCondition();//队列满时的等待条件
    Condition notEmpty = lock.newCondition();//队列空时的等待条件

    public MyBlockingQueue(int size){
         this.size = size;
     }

      public void enqueue(E e) throws InterruptedException {
         lock.lock();
         try {
             while (list.size() ==size)//队列已满,在notFull条件上等待
                 notFull.await();
             list.add(e);//入队:加入链表末尾
             System.out.println("入队:" +e);
             notEmpty.signal(); //通知在notEmpty条件上等待的线程
         } finally {
             lock.unlock();
         }
     }

      public E dequeue() throws InterruptedException {
         E e;
         lock.lock();
         try {
             while (list.size() == 0)//队列为空,在notEmpty条件上等待
                 notEmpty.await();
             e = list.removeFirst();//出队:移除链表首元素
             System.out.println("出队:"+e);
             notFull.signal();//通知在notFull条件上等待的线程
             return e;
         } finally {
             lock.unlock();
         }
     }
}

测试代码

public static void main(String[] args) throws InterruptedException {

    MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
    for (int i = 0; i < 10; i++) {
        int data = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.enqueue(data);
                } catch (InterruptedException e) {

                }
            }
        }).start();

    }
    for(int i=0;i<10;i++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer data = queue.dequeue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

运行结果

入队:0
入队:1
出队:0
出队:1
入队:4
出队:4
入队:5
出队:5
入队:8
出队:8
入队:9
出队:9
入队:2
出队:2
入队:3
出队:3
入队:6
出队:6
入队:7
出队:7

Process finished with exit code 0

用Condition实现的生产者消费者问题
            public class ConditionTest {
                private LinkedList<String> buffer;    //容器
                private int maxSize ;           //容器最大
                private Lock lock;
                private Condition fullCondition;
                private Condition notFullCondition;
            
                ConditionTest(int maxSize){
                    this.maxSize = maxSize;
                    buffer = new LinkedList<String>();
                    lock = new ReentrantLock();
                    fullCondition = lock.newCondition();
                    notFullCondition = lock.newCondition();
                }
            
                public void set(String string) throws InterruptedException {
                    lock.lock();    //获取锁
                    try {
                        while (maxSize == buffer.size()){
                            notFullCondition.await();       //满了,添加的线程进入等待状态
                        }
            
                        buffer.add(string);
                        fullCondition.signal();
                    } finally {
                        lock.unlock();      //记得释放锁
                    }
                }
            
                public String get() throws InterruptedException {
                    String string;
                    lock.lock();
                    try {
                        while (buffer.size() == 0){
                            fullCondition.await();
                        }
                        string = buffer.poll();
                        notFullCondition.signal();
                    } finally {
                        lock.unlock();
                    }
                    return string;
                }
            }

Condtion的实现

获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。定义如下:

            public class ConditionObject implements Condition, java.io.Serializable {
            }
等待队列

每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。我们看Condition的定义就明白了:

            public class ConditionObject implements Condition, java.io.Serializable {
                private static final long serialVersionUID = 1173984872572414699L;
            
                //头节点
                private transient Node firstWaiter;
                //尾节点
                private transient Node lastWaiter;
            
                public ConditionObject() {
                }
            
                /** 省略方法 **/
            }

从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:

202202131353319682.png

Node里面包含了当前线程的引用。Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。

Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。

等待

调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。

               public final void await() throws InterruptedException {
                    // 当前线程中断
                    if (Thread.interrupted())
                        throw new InterruptedException();
                    //当前线程加入等待队列
                    Node node = addConditionWaiter();
                    //释放锁
                    long savedState = fullyRelease(node);
                    int interruptMode = 0;
                    /**
                     * 检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
                     * 直到检测到此节点在同步队列上
                     */
                    while (!isOnSyncQueue(node)) {
                        //线程挂起
                        LockSupport.park(this);
                        //如果已经中断了,则退出
                        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                            break;
                    }
                    //竞争同步状态
                    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                        interruptMode = REINTERRUPT;
                    //清理下条件队列中的不是在等待条件的节点
                    if (node.nextWaiter != null) // clean up if cancelled
                        unlinkCancelledWaiters();
                    if (interruptMode != 0)
                        reportInterruptAfterWait(interruptMode);
                }

此段代码的逻辑是:首先将当前线程新建一个节点同时加入到条件队列中,然后释放当前线程持有的同步状态。然后则是不断检测该节点代表的线程释放出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。

加入条件队列(addConditionWaiter())源码如下:

                private Node addConditionWaiter() {
                    Node t = lastWaiter;    //尾节点
                    //Node的节点状态如果不为CONDITION,则表示该节点不处于等待状态,需要清除节点
                    if (t != null && t.waitStatus != Node.CONDITION) {
                        //清除条件队列中所有状态不为Condition的节点
                        unlinkCancelledWaiters();
                        t = lastWaiter;
                    }
                    //当前线程新建节点,状态CONDITION
                    Node node = new Node(Thread.currentThread(), Node.CONDITION);
                    /**
                     * 将该节点加入到条件队列中最后一个位置
                     */
                    if (t == null)
                        firstWaiter = node;
                    else
                        t.nextWaiter = node;
                    lastWaiter = node;
                    return node;
                }

该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清楚所有状态不为Condition的节点。

fullyRelease(Node node),负责释放该线程持有的锁。

                final long fullyRelease(Node node) {
                    boolean failed = true;
                    try {
                        //节点状态--其实就是持有锁的数量
                        long savedState = getState();
                        //释放锁
                        if (release(savedState)) {
                            failed = false;
                            return savedState;
                        } else {
                            throw new IllegalMonitorStateException();
                        }
                    } finally {
                        if (failed)
                            node.waitStatus = Node.CANCELLED;
                    }
                }

isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true

                final boolean isOnSyncQueue(Node node) {
                    //状态为Condition,获取前驱节点为null,返回false
                    if (node.waitStatus == Node.CONDITION || node.prev == null)
                        return false;
                    //后继节点不为null,肯定在CLH同步队列中
                    if (node.next != null)
                        return true;
            
                    return findNodeFromTail(node);
                }

unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除

                    private void unlinkCancelledWaiters() {
                        Node t = firstWaiter;
                        Node trail = null;
                        while (t != null) {
                            Node next = t.nextWaiter;
                            if (t.waitStatus != Node.CONDITION) {
                                t.nextWaiter = null;
                                if (trail == null)
                                    firstWaiter = next;
                                else
                                    trail.nextWaiter = next;
                                if (next == null)
                                    lastWaiter = trail;
                            }
                            else
                                trail = t;
                            t = next;
                        }
                    }
通知

调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。

                public final void signal() {
                    //检测当前线程是否为拥有锁的独
                    if (!isHeldExclusively())
                        throw new IllegalMonitorStateException();
                    //头节点,唤醒条件队列中的第一个节点
                    Node first = firstWaiter;
                    if (first != null)
                        doSignal(first);    //唤醒
                }

该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。

doSignal(Node first):唤醒头节点

                private void doSignal(Node first) {
                    do {
                        //修改头结点,完成旧头结点的移出工作
                        if ( (firstWaiter = first.nextWaiter) == null)
                            lastWaiter = null;
                        first.nextWaiter = null;
                    } while (!transferForSignal(first) &&
                            (first = firstWaiter) != null);
                }

doSignal(Node first)主要是做两件事:1.修改头节点,2.调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。transferForSignal(Node first)源码如下:

                 final boolean transferForSignal(Node node) {
                    //将该节点从状态CONDITION改变为初始状态0,
                    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
                        return false;
            
                    //将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点
                    Node p = enq(node);
                    int ws = p.waitStatus;
                    //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
                    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
                        LockSupport.unpark(node.thread);
                    return true;
                }

整个通知的流程如下:

  1. 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
  2. 如果线程已经获取了锁,则将唤醒条件队列的首节点
  3. 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中
  4. 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。

总结

一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒CLH同步队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。

CAS

CAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用 CAS 技术鬼斧神工地实现了Java 多线程的并发操作。整个 AQS 同步组件、Atomic 原子类操作等等都是以 CAS 实现的。可以说CAS是整个JUC的基石。
CAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用CAS技术鬼斧神工地实现了Java多线程的并发操作。整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。

202202131353326371.png

CAS分析

在CAS中有三个参数:内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干。其伪代码如下:

            if(this.value == A){
                this.value = B
                return true;
            }else{
                return false;
            }

JUC下的atomic类都是通过CAS来实现的,下面就以AtomicInteger为例来阐述CAS的实现。如下:

                private static final Unsafe unsafe = Unsafe.getUnsafe();
                private static final long valueOffset;
            
                static {
                    try {
                        valueOffset = unsafe.objectFieldOffset
                            (AtomicInteger.class.getDeclaredField("value"));
                    } catch (Exception ex) { throw new Error(ex); }
                }
            
                private volatile int value;

Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门:Unsafe,它提供了硬件级别的原子操作。

valueOffset为变量值在内存中的偏移地址,unsafe就是通过偏移地址来得到数据的原值的。

value当前值,使用volatile修饰,保证多线程环境下看见的是同一个。

我们就以AtomicInteger的addAndGet()方法来做说明,先看源代码:

                public final int addAndGet(int delta) {
                    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
                }
            
                public final int getAndAddInt(Object var1, long var2, int var4) {
                    int var5;
                    do {
                        var5 = this.getIntVolatile(var1, var2);
                    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
            
                    return var5;
                }

内部调用unsafe的getAndAddInt方法,在getAndAddInt方法中主要是看compareAndSwapInt方法:

                public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

该方法为本地方法,有四个参数,分别代表:对象、对象的地址、预期值、修改值(有位伙伴告诉我他面试的时候就问到这四个变量是啥意思…)。该方法的实现这里就不做详细介绍了,有兴趣的伙伴可以看看openjdk的源码。

CAS可以保证一次的读-改-写操作是原子操作,在单处理器上该操作容易实现,但是在多处理器上实现就有点儿复杂了。

CPU提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁。

  • 总线加锁 :总线加锁就是就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。所以就有了缓存加锁。
  • 缓存加锁 :其实针对于上面那种情况我们只需要保证在同一时刻对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不在输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

CAS缺陷

CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。

循环时间太长

如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。

只能保证一个共享变量原子操作

看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。例如读写锁中state的高地位

ABA问题

CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。

用一个例子来阐述ABA问题所带来的影响。

有如下链表

202202131353331072.png

假如我们想要把B替换为A,也就是compareAndSet(this,A,B)。线程1执行B替换A操作,线程2主要执行如下动作,A 、B出栈,然后C、A入栈,最终该链表如下:

202202131353336073.png

完成后线程1发现仍然是A,那么compareAndSet(this,A,B)成功,但是这时会存在一个问题就是B.next = null,compareAndSet(this,A,B)后,会导致C丢失,改栈仅有一个B元素,平白无故把C给丢失了。

CAS的ABA隐患问题,解决方案则是版本号,Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。对于上面的案例应该线程1会失败。

AtomicStampedReference的compareAndSet()方法定义如下:

                public boolean compareAndSet(V   expectedReference,
                                             V   newReference,
                                             int expectedStamp,
                                             int newStamp) {
                    Pair<V> current = pair;
                    return
                        expectedReference == current.reference &&
                        expectedStamp == current.stamp &&
                        ((newReference == current.reference &&
                          newStamp == current.stamp) ||
                         casPair(current, Pair.of(newReference, newStamp)));
                }
                

compareAndSet有四个参数,分别表示:预期引用、更新后的引用、预期标志、更新后的标志。源码部门很好理解预期的引用 == 当前引用,预期的标识 == 当前标识,如果更新后的引用和标志和当前的引用和标志相等则直接返回true,否则通过Pair生成一个新的pair对象与当前pair CAS替换。Pair为AtomicStampedReference的内部类,主要用于记录引用和版本戳信息(标识),定义如下:

                private static class Pair<T> {
                    final T reference;
                    final int stamp;
                    private Pair(T reference, int stamp) {
                        this.reference = reference;
                        this.stamp = stamp;
                    }
                    static <T> Pair<T> of(T reference, int stamp) {
                        return new Pair<T>(reference, stamp);
                    }
                }

                private volatile Pair<V> pair;
                            

Pair记录着对象的引用和版本戳,版本戳为int型,保持自增。同时Pair是一个不可变对象,其所有属性全部定义为final,对外提供一个of方法,该方法返回一个新建的Pari对象。pair对象定义为volatile,保证多线程环境下的可见性。在AtomicStampedReference中,大多方法都是通过调用Pair的of方法来产生一个新的Pair对象,然后赋值给变量pair。如set方法:

                public void set(V newReference, int newStamp) {
                    Pair<V> current = pair;
                    if (newReference != current.reference || newStamp != current.stamp)
                        this.pair = Pair.of(newReference, newStamp);
                }

下面我们将通过一个例子可以可以看到AtomicStampedReference和AtomicInteger的区别。我们定义两个线程,线程1负责将100 —> 110 —> 100,线程2执行 100 —>120,看两者之间的区别。

            public class Test {
                private static AtomicInteger atomicInteger = new AtomicInteger(100);
                private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
            
                public static void main(String[] args) throws InterruptedException {
            
                    //AtomicInteger
                    Thread at1 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            atomicInteger.compareAndSet(100,110);
                            atomicInteger.compareAndSet(110,100);
                        }
                    });
            
                    Thread at2 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                TimeUnit.SECONDS.sleep(2);      // at1,执行完
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100,120));
                        }
                    });
            
                    at1.start();
                    at2.start();
            
                    at1.join();
                    at2.join();
            
                    //AtomicStampedReference
            
                    Thread tsf1 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                //让 tsf2先获取stamp,导致预期时间戳不一致
                                TimeUnit.SECONDS.sleep(2);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            // 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1
                            atomicStampedReference.compareAndSet(100,110,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
                            atomicStampedReference.compareAndSet(110,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
                        }
                    });
            
                    Thread tsf2 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            int stamp = atomicStampedReference.getStamp();
            
                            try {
                                TimeUnit.SECONDS.sleep(2);      //线程tsf1执行完
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.out.println("AtomicStampedReference:" +atomicStampedReference.compareAndSet(100,120,stamp,stamp + 1));
                        }
                    });
            
                    tsf1.start();
                    tsf2.start();
                }
            
            }

运行结果:

AtomicInteger:true
AtomicStampedReference:false

运行结果充分展示了AtomicInteger的ABA问题和AtomicStampedReference解决ABA问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/62205.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

室外定位:高精度北斗RTK定位技术

北斗RTK定位技术&#xff0c;也称北斗差分定位技术&#xff0c;利用我国自主研发的北斗卫星定位系统实现精确定位功能。定位精度可根据需要&#xff0c;通过选择不同精度的人员定位终端来实现。 在科技强国的战略驱动下&#xff0c;北斗RTK定位技术迎来了广阔的发展机遇&#x…

【无锁队列】无锁CAS_无锁队列

1 引言 锁是解决并发问题的万能钥匙&#xff0c;可是并发问题只有锁能解决吗&#xff1f; 2 什么是CAS&#xff1f; ⽐较并交换(compare and swap, CAS)&#xff0c;是原⼦操作的⼀种&#xff0c;可⽤于在多线程编程中实现不被打断的数据交换操作&#xff0c;从⽽避免多线程…

[附源码]计算机毕业设计基于Springboot的专业技能认证系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Thread类的常用方法

目录 1.Thread类常用的构造方法 2.Thread类的几个常见属性 2.1 什么是守护线程?isDaemon 2.2线程是否存活 isAliye() 3.终止线程的方法 3.1使用共享标志位通知中断线程 3.2使用Thread自带的标志位通知 4.等待线程 join 5.获取当前线程的引用 6.休眠当前线程 Thread …

Java安全--篇2-类的动态加载

类的动态加载 首先我们来了解一下构造代码块和静态代码块&#xff1a;Java中静态代码块、构造代码块、构造函数、普通代码块 - YSOcean - 博客园 // 静态代码块 static {System.out.println("静态代码块"); }// 构造代码块 {System.out.println("构造代码块&q…

使用Jetpack组件Navigation实现Android开发中页面跳转

使用Jetpack组件Navigation实现Android开发中页面跳转 ​目录 一、前言 1.概述 2.导航图的创建&#xff08;官网&#xff09; 二、基本使用 1.依赖配置 2.具体实例&#xff1a;使用Navigation实现页面的跳转。 2.1.class的创建 2.2 、页面布局文件的创建 2.3 向 Acti…

Abaqus血管支架仿真攻略之几何创建与网格划分

作者&#xff1a;江丙云&#xff0c;仿真秀平台优秀讲师 前不久&#xff0c;笔者推送的冠脉支架的参数化建模和优化、Abaqus网格卷曲WrapMesh&#xff0c;冠脉支架的参数化建模和优化&#xff0c;以及Abaqus疲劳分析|Goodman插件等原创文章后&#xff0c;后台留言的读者众多&a…

GDP-海藻糖,5‘-鸟苷二磷酸岩藻糖,GDP-fucose ,CAS:15839-70-0

产品名称&#xff1a;GDP-海藻糖&#xff0c;5-鸟苷二磷酸岩藻糖&#xff0c;二磷酸鸟苷岩藻糖&#xff0c;GDP-L-岩藻糖 英文名称&#xff1a;GDP-fucose&#xff0c;Guanosine 5-diphosphate-L-fucose disodium salt CAS&#xff1a;15839-70-0 Mol. Formula C16H23N5O…

基于模糊神经网络算法预测电价(Matlab代码实现)

&#x1f4cb;&#x1f4cb;&#x1f4cb;本文目录如下&#xff1a;⛳️⛳️⛳️ ​ 目录 1 概述 2 模糊神经网络简介 3 运行结果 4 参考文献 5 Matlab代码实现 1 概述 近年来,随着能源短缺和环境问题的日益凸显,太阳能、风能等各种形式的清洁能源得到广泛应用,微网作为分布式…

【云原生 | 46】高可用的开源键值数据库Etcd的安装与使用

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 &#x1f3c5;阿里云ACE认证高级工程师 &#x1f3c5;阿里云开发者社区专…

第二证券|超300家机构关注两大赛道龙头,透露市场增长及发展方向

中科创达、奥普特成为本周调研组织数量最多的两家公司。 智能操作体系龙头获365家组织调研 证券时报数据宝计算&#xff0c;11月27日至12月3日&#xff0c;组织算计调研上市公司291家。被调研方多属于电子、机械设备、医药生物、电力设备、计算机、国防军工等板块&#xff0c;…

安卓属性动画

​ 一&#xff0e;三种安卓动画 Tween Animation(补间动画、视图动画)&#xff1a;通过对场景里的对象不断做图像变换&#xff08;平移、缩放、旋转&#xff09;产生的动画效果&#xff0c;即是一种渐变动画。 Frame Animation(帧动画)&#xff1a;顺序播放事先做好的图像&…

基于PLC的工业晾晒架系统

目录 前言 6 第一章 工业晾晒架的发展现状及趋势 7 1.1自动工业晾晒架的基本介绍 7 1.2自动工业晾晒架发展史和发展现状 8 第二章 自动晾晒架的智能模块 9 2.1自动晾晒架的结构框图 9 2.2自动晾晒架的机械理论 10 第三章 自动晾晒架的硬件设计 11 3.1电源设计部分 12 3.2 PLC的…

组合模式

文章目录思考组合模式1.组合模式的本质2.何时选用组合模式3.优缺点4.实现思考组合模式 组合模式实际上就是让客户端不再区分操作的是组合对象还是叶子对象&#xff0c;而是以一个统一的方式来操作。 1.组合模式的本质 组合模式的本质:统一叶子对象和组合对象。 组合模式通过把叶…

No.179 念念随风上九霄

引言有人跟老梁说&#xff0c;你挺忙的吧&#xff0c;更新变少了。害&#xff0c;可不是嘛&#xff1f;时间不太够&#xff0c;凑不出来了。凑不出来就不凑它了&#xff0c;扯扯其他的吧。在大城市生活节奏是快&#xff0c;个人也被裹挟着运转&#xff0c;无处可逃。从早到晚&a…

[附源码]计算机毕业设计基于SpringBoot动漫电影网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

如何用 7 分钟击破 Serverless 落地难点?

当前&#xff0c;Serverless 覆盖的技术场景正在不断变广。Serverless 已在微服务、在线应用、事件驱动、任务处理等众多场景被验证且广泛应用 。当你想要部署一个网站时&#xff0c;需要自己购买服务器并花费时间去维护&#xff0c;造成资源浪费不说&#xff0c;还要耗费精力。…

【NR 物理资源】

NR中与物理资源相关的概念主要包括天线端口、资源网格、资源单元、资源块RB和BWP等。 文章目录天线端口&#xff08;Antenna Ports&#xff09;资源网格Resource grid和资源单元RE资源块Resource Block部分带宽BWP天线端口&#xff08;Antenna Ports&#xff09; 天线端口定义 …

qmake source code 解读

qmake的主要功能执行入口在main.cpp中的runQMake(int argc, char **argv)中。其主要框架如下: runQMake(int argc, char **argv){QMakeVfs vfs; //初始化qmake的文件系统。virtual file system。vfs会为每个文件赋予一个id,并提供根据id进行操作的函数。Option::vfs &v…

交通流的微观模型(Matlab代码实现)

&#x1f352;&#x1f352;&#x1f352;欢迎关注&#x1f308;&#x1f308;&#x1f308; &#x1f4dd;个人主页&#xff1a;我爱Matlab &#x1f44d;点赞➕评论➕收藏 养成习惯&#xff08;一键三连&#xff09;&#x1f33b;&#x1f33b;&#x1f33b; &#x1f34c;希…