高并发技术---锁

文章目录

1. 锁的概念及作用

锁的本质是一种同步机制,用于协调并发访问共享资源的行为。在多线程或多进程环境中,多个线程或进程可能同时访问共享资源,导致数据不一致、竞态条件和死锁等问题。为了解决这些问题,锁被引入以保护共享资源的访问和修改。

锁的基本思想是在竞争共享资源时,只有一个线程或进程能够获得锁,从而获得对共享资源的独占访问权。其他线程或进程必须等待锁的释放才能访问共享资源。锁的使用可以确保共享资源的一致性和可靠性,同时避免并发冲突和数据损坏。

锁可以基于不同的实现方式,如互斥锁、读写锁、自旋锁等,但它们都具有同步的特点,用于协调多个线程或进程的行为,从而确保程序正确地运行。

2. 锁分类

按共享方式

共享锁(又叫读锁、S锁)

若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
Semaphore、CountDownLatch是共享锁。

互斥锁(又叫写锁、独占锁、排它锁、X锁) mutex

如果事务T对数据A加上互斥锁后,则其他事务不能再对A加任任何类型的锁,直到在事务的末尾将资源上的锁释放为止。获准排他锁的事务既能读数据,又能修改数据。
互斥锁是一种二进制锁,用于控制对共享资源的访问。只有获得了锁的线程才能访问资源,其他线程必须等待锁的释放。互斥锁通常用于保护关键代码段和临界区。
ReentrantLock是互斥锁。

附:共享锁与互斥锁区别

共享锁与排它锁区别

  1. 共享锁保证了其他事务不能写,排他锁保证了其他事物不能读。
  2. 加了共享锁的对象,可以继续加共享锁,不能再加排他锁。加了排他锁后,不能再加任何锁。
附: 共享锁与互斥锁应用场景举例

例如数据库操作,共享锁只用于表级,排他锁用于行级。比如一个DML操作,就要对受影响的行加排他锁,这样就不允许再加别的锁,也就是说别的会话不能修改这些行。同时为了避免在做这个DML操作的时候,有别的会话执行DDL,修改表的定义,所以要在表上加共享锁,这样就阻止了DDL的操作。当执行DDL操作时,就需要在全表上加排他锁

读写锁 rwlock (RWMutex)

特点

读写锁点:读读共享,读写互斥,写写互斥,写入操作优先级大于读取操作。

  • 读写锁维护一个读锁和一个写锁,读锁是共享锁,写锁是排他锁。
  • 读读共享: 它允许任意个读操作的同时进行
  • 读写互斥、写写互斥:写线程会阻塞其它读取和写入线程。在同一时刻,只能有一个线程可以获得写锁。当获得写锁的线程在执行写入操作时,读锁和写锁都不能被其它线程获取,即其它读取和写入操作会进入阻塞状态。
  • 写入操作优先级大于读取操作:当获得读锁或写锁的线程在操作完成释放锁后,被阻塞的写线程优先于被阻塞的读线程被唤醒。
适用场景

读写锁适用于读取数据的频率远远大于写数据的频率的场景。比如缓存场景。

附:互斥锁与读写锁区别

互斥锁不区分读者和写者,一视同仁。互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。

按是否可重入

可重入锁

定义

参见:

  • 可重入锁和不可重入锁详解
    Reentrant = Re + entrant,Re是重复、又、再的意思,entrant是enter的名词或者形容词形式,翻译为进入者或者可进入的,所以Reentrant翻译为可重复进入的、可再次进入的,因此ReentrantLock翻译为重入锁或者再入锁。

可重入锁又叫递归锁,指的是可重复可递归调用的锁。当同一个线程在外层方法获取对象锁(见下例run方法的try{lock.lock()})之后,再进入该线程的内层方法递归获取相同的锁对象(见下例doBusiness()方法的try{lock.lock()})时,会自动获取锁(前提,锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞,否则会导致死锁。

举例:下例线程在执行run方法内部的lock.lock();获取到锁对象lock后,又调用了doBusiness方法,doBusiness内部再次(即递归)调用lock.lock()时,会自动获取该对象的锁,不会因为之前在test方法内已经获取过还没释放而阻塞。

由上可知,通过可重入锁可以解决同一个线程重复递归获取相同锁引起的死锁问题。

class MyThread extends Thread {
    private Lock lock = new Reentrantlock();
    public void run() {
		try {
			//获取锁之后才能进入业务
			lock.lock();

			//业务处理,业务处理代码中也包含获取锁和释放锁的逻辑,即递归获取锁
			doBusiness();
		} catch(Exception e) {
		} finally {
			lock.unlock();
		}
    }
	private void doBusiness() {
		try {
			//获取锁之后才能进入业务
			lock.lock();

			//业务处理,业务处理代码中也包含获取锁
		} catch(Exception e) {
		} finally {
			lock.unlock();
		}
	}
}

ReentrantLock和synchronized都是可重入锁。 ReentrantLock是显式的可重入锁,synchronized是隐式的可重入锁。

可重入锁的工作原理

可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。

不可重入锁

与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

自旋锁是不可重入锁。

不可重入锁也叫自旋锁(这句话对吗?)

按共享资源修改概率划分

悲观锁

悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁

乐观锁

相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很高,但是冲突的概率足够低的话,还是可以接受的。可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。

举个例子:

在线文档是可以同时多人编辑,如果使用了悲观锁,只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档,用户体验当然不好。

实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。

怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交改动,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。

服务端是如何解决这种冲突的?

由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号;当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号一致则修改成功,否则提交失败。

实际上,常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。

乐观锁虽然去除了加锁解锁的操作,但一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

按作用域

单机锁

自旋锁 spinlock

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

自旋锁是一种基于忙等待的锁类型,用于执行时间短暂的互斥区域和锁竞争较小的场景。自旋锁不会让线程进入睡眠状态,而是在循环中等待锁的释放。自旋锁通常用于多核处理器中的并发控制。

在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源

互斥锁与自旋锁的区别

最底层的两种就是「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现,可以认为它们是各种锁的地基,所以得清楚它俩之间的区别和应用。

加锁的目的是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。

当已经有一个线程加锁后,其他线程加锁则会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的。

互斥锁与自旋锁的开销成本

互斥锁加锁失败后,线程会释放 CPU ,给其他线程;旋锁加锁失败后,线程会忙等待,直到它拿到锁;互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。

对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,就可以继续执行。

RCU

即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。

条件变量(Condition)

条件变量是一种线程同步机制,用于在多线程之间传递信号和状态信息。条件变量通常与互斥锁一起使用,等待线程在获得锁之后才能进入等待状态,而唤醒线程会在释放锁之前发送信号。条件变量通常用于实现生产者-消费者模型和线程池等场景。

偏向锁
  • 自旋锁:自旋,jvm默认是10次吧,有jvm自己控制。for去争取锁
    自旋锁的其他种类
  • 阻塞锁:被阻塞的线程,不会争夺锁。
  • 可重入锁: 多次进入改锁的域
  • 读写锁
  • 互斥锁:锁本身就是互斥的
  • 悲观锁:不相信,这里会是安全的,必须全部上锁
  • 乐观锁:相信,这里是安全的。
  • 公平锁:有优先级的锁
  • 非公平锁:无优先级的锁
  • 偏向锁:无竞争不锁,有竞争挂起,转为轻量锁
  • 对象锁:锁住对象
  • 线程锁
  • 锁粗化:多锁变成一个,自己处理
  • 轻量级锁:CAS 实现
  • 锁消除:偏向锁就是锁消除的一种
  • 锁膨胀:jvm实现,锁粗化
  • 信号量:使用阻塞锁 实现的一种策略
第三方工具锁

分布式锁

第三方工具锁

互斥锁:互斥锁是一种二进制锁,用于控制对共享资源的访问。只有获得了锁的线程才能访问资源,其他线程必须等待锁的释放。互斥锁通常用于保护关键代码段和临界区。
读写锁:读写锁是一种优化的锁类型,用于在共享资源中支持并发读取和排他性写入。读写锁允许多个线程同时读取资源,但只有一个线程可以写入资源。读写锁通常用于读多写少的场景,以提高并发性能。
自旋锁:自旋锁是一种基于忙等待的锁类型,用于短暂的互斥区域和锁竞争较小的场景。自旋锁不会让线程进入睡眠状态,而是在循环中等待锁的释放。自旋锁通常用于多核处理器中的并发控制。
条件变量:条件变量是一种线程同步机制,用于在多线程之间传递信号和状态信息。条件变量通常与互斥锁一起使用,等待线程在获得锁之后才能进入等待状态,而唤醒线程会在释放锁之前发送信号。条件变量通常用于实现生产者-消费者模型和线程池等场景。

3. Java 内置锁相关组件

3.1 锁基础–volatile、CAS、Unsafe、VarHandle

在这里插入图片描述

volatile

参见:

volatile作用

参见:

总结:

  1. volatile可保证变量在多线程之间的可见性
    通过volatile修饰的变量,其地址会强制刷新到可对所有线程可见的主内存中,而不是只在各线程的本地内存可见。

  2. volatile可保证防止指令重排,保证有序性
    a. 在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。
    b. 重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果。
    下例中的1和2由于不存在数据依赖关系,则有可能会被重排序。线程 A先执行status=true再执行a=2,刚执行完statue=true还未执行a=2,此时线程B执行到4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。(若未发生指令重排,线程B执行到4处,得到的结果应该为4)

    public class TestVolatile{
    	int a = 1;
    	boolean status = false;//状态切换为true
    	public void changeStatus (){
    		a = 2;   //1
    		status = true;  //2
    	}
    	//若状态为true,则为running
    	public void run(){
    		if(status){   //3
    			int b = a + 1;  //4
    			System.out.println(b);
    		}
    	}
    }
    

    解决办法:使用volatile修饰变量a,可防止changeStatus内的两个指令重排。

    c. volatile可保证有序性原理:在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
    (1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    (2)它会强制将对缓存的修改操作立即写入主存;
    (3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

3. 要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行

4. volatile无法保证原子性
Java中的原子性操作包括:

(1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
(2)所有引用reference的赋值操作
(3)java.concurrent.Atomic.* 包中所有类的一切操作
(4)synchronize方法(块)或Lock保护的块。

volatile无法保证原子性案例及解决办法
案例一: i++操作非原子性操作

问题描述
例如: 例如,inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果并不能达到10000。
在这里插入图片描述
解决方法:用以上保证原子性的手段,来保证inc++操作的原子性

  1. 方法一:采用synchronized关键字保证inc++操作的原子性
    在这里插入图片描述
  2. 方法二:采用Lock保证inc++操作的原子性
    在这里插入图片描述
  3. 方法三:采用java并发包中的原子操作类,保证inc++操作的原子性
    java并发包中的原子操作类,是通过CAS循环的方式来保证其原子性的
    在这里插入图片描述
案例二: 单例模式的双重锁为什么要加volatile

public class TestInstance{
	private volatile static TestInstance instance;

	public static TestInstance getInstance(){ //1
		if(instance == null){ //2
			synchronized(TestInstance.class){ //3
					if(instance == null){ //4
						instance = new TestInstance(); //5
					}
			}
		return instance; //6
	}
}

单例类实例必须加volatile来修饰,原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码

a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。

原子操作–CAS

参见:什么是CAS机制?

原子操作基本概念和实现原理
什么是原子操作

原子操作指不会被线程调度机制打断的操作。

  • 原子操作是一个整体,不会被切割
  • 一旦开始,直到结束,不会被CPU切换
  • 顺序不会被打乱

CAS操作就是原子操作。

为什么学习原子操作
  • 实现轻量级同步
  • 在CPU指令级别提供支持
  • 场景:对少量资源进行快速数据更新操作,用于读多写少的场景。
原子操作实现原理

int i=0;
int x=i+1; //不是原子操作

CPU处理器保证基本内存操作的原子性:
总线锁
缓存锁

CAS原子操作定义

CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法。在 Java 平台上对这种操作进行了包装。在 Unsafe 类中,调用代码如下:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

它需要三个参数,分别是内存位置 V,旧的预期值 A 和新的值 B。

  • 操作时,先从内存位置读取到值,然后和预期值A比较。
  • 如果相等,则将此内存位置的值改为新值 B,返回 true。
  • 如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。
    这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。
java中 CAS 实现方式

1. JAVA基于Unsafe类的以下方法实现CAS操作

public final class Unsafe {
	// Usafe类
	//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
	//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
	public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
	public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
	public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
}

2. CAS操作的变量通常需要设置volatile关键字来保证数据的可见性和有序性

  • CAS操作只保证数据操作的原子性
  • volatile可保证所修饰变量的操作的可见性和有序性,但不保证操作的原则性
  • CAS+volatile配合,可保证数据的可见性、有序性和原子性,进而保证数据的一致性。

3. 通常结合自旋+CAS操作完成对数据的修改操作
CAS操作执行时间非常快,适合自旋(即无限循环)的用于执行时间短暂的互斥区域和锁竞争较小的场景特点,因此通常结合自旋+CAS操作完成对数据的修改操作。

AtomicInteger/AtomicLong等原子操作对象的内部实现都是基于自旋+CAS操作来实现的。从JDK1.8开始,自旋操作本身从原来的AtomicInteger/AtomicLong转到了jdk.internal.misc.Unsafe类中。

//AtomicInteger.incrementAndGet
//JDK 1.7的源码,由for的死循环实现,并且直接在AtomicLong中实现该方法,
//JDK1.8后,该方法实现已移动到jdk.internal.misc.Unsafe类中,直接调用getAndAddLong方法即可
public final int incrementAndGet() {
   //无限循环,即自旋,通常配合自旋机制实现CAS操作
   for (;;) {
       int current = get();
       int next = current + 1;
       if (compareAndSet(current, next))
           return next;
   }
}
// 这里的value须用volatile 修饰,volatile 保证数据读写操作的可见性和有序性,
// 但无法保证数据操作的原子性,原子性需要借助CAS操作来实现。即通过CAS+volatile来保证数据的一致性。
private volatile int value; 
public final int get() {
   return value;
}

这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:

  1. 获取当前值
  2. 当前值+1,计算出目标值
  3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤

这里需要注意的重点是get方法,这个方法的作用是获取变量的当前值。

如何保证获取的当前值是内存中的最新值?很简单,用volatile关键字来保证(保证线程间的可见性和有序性)。我们接下来看一下compareAndSet方法的实现:
在这里插入图片描述
compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。

什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。

我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。

CAS 缺点

1. 高并发情况下CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试(自旋)更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2. 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized或Lock了。

3. ABA问题
这是CAS机制最大的问题所在。

CAS的 ABA问题及解决办法

详见上方的链接文档。
解决办法:AtomicStampedReference类。
java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它可以 通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类比较"鸡肋",大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用 传统的互斥同步 可能会比原子类更加高效。

Unsafe

参见:

Unsafe 概述
  • 通过操作系统的相关知识我们知道,java无法直接访问底层的操作系统(中间还有虚拟机(JVM)),而是 通过本地(Native)方法来访问底层的。

  • 但是,虚拟机也有让java直接访问的方法,那就是JDK中的一个类Unsafe,Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

  • 该类中的方法都是native修饰的 , Unsafe提供了硬件级的原子操作。

  • 注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,也就是说Unsafe提供了硬件级别的操作,比如说获取某个属性在内存中的位置,修改对象的字段值,即使它是私有的,不过Java本身就是为了屏蔽底层的差异,对于一般的开发而言也很少会有这样的需求。

  • Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。

  • JDK1.8 之前Unsafe类存在于sun.misc包中,在JDK9之后,sun.misc.Unsafe被移动到jdk.unsupported模块中,同时在java.base模块克隆了一个jdk.internal.misc.Unsafe类,代替了JDK8以前的sun.misc.Unsafe(提供的许多方法委托给jdk.internal.misc.Unsafe实现)的功能,jdk.internal包不开放给开发者调用。Unsafe是用于在实质上扩展Java语言表达能力、便于在更高层(Java层)代码里实现原本要在更低层(C层)实现的核心库功能用的。这些功能包括裸内存的申请/释放/访问,低层硬件的atomic/volatile支持,创建未初始化对象等。为了让开发者有机会过渡到尽量不使用sun.misc.Unsafe,默认不允许Java应用代码访问sun.misc.Unsafe类,同时在java.base模块克隆了一个不能被外部访问的jdk.internal.misc.Unsafe类用于JDK内部API演进。

  • 这个类尽管里面的方法都是public的,但是并没有办法使用它们。Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

    package sun.misc;
    public final class Unsafe {
        @CallerSensitive
        public static Unsafe getUnsafe() {
            Class<?> caller = Reflection.getCallerClass();
            if (!VM.isSystemDomainLoader(caller.getClassLoader()))
                throw new SecurityException("Unsafe");
            return theUnsafe;
        }
    }
    
    
    package jdk.internal.misc;
    public class VM {
    
        /**
         * 判断是部署JDK内部类加载器:
         * 1. loader == null:表示类加载器是引导类加载器,即jre的lib目录
         * 2. PlatformClassLoader在jdk1.8及之前是扩展类加载器,即jre的ext目录
         */
        public static boolean isSystemDomainLoader(ClassLoader loader) {
            return loader == null || loader == ClassLoader.getPlatformClassLoader();
        }
    }
    

在这里插入图片描述

如何获取Unsafe实例?
  1. 从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
    在这里插入图片描述

  2. 通过反射获取单例对象theUnsafe,Spring提供了UnsafeUtils工具类,也可以获取Unsafe实例。

在这里插入图片描述

Unsafe实现的功能

Unsafe可实现以下功能:
在这里插入图片描述
在这里插入图片描述

CAS操作
内存管理

Unsafe的内存管理功能主要包括:普通读写、volatile读写、有序写入、直接操作内存等分配内存与释放内存的功能。

普通的读写无法保证对所有线程的可见性和有序性,而volatile读写就可以保证可见性和有序性,但是相对普通读写要更加昂贵。有序写只保证有序性,不保证可见性。

从执行效率上:普通写>有序写>volatile写

普通读写、volatile读写和有序写

Unsafe可以读写一个类的属性,即便这个属性是私有的,也可以对这个属性进行读写。

  • 普通读写-直接读写指定对象的属性:在对象 o 内,偏移量为 offset,取出当前值或者放入 x。

    • getInt(Long/Short/Char/Byte/Float/Double/Object/Boolean) (Object o, long offset)

    • putInt(Long/Short/Char/Byte/Float/Double/Object/Boolean) (Object o, long offset, int/long/short/char/byte/float/double/Object/boolean x)

      如: getInt (Object o, long offset); putInt((Object o, long offset, int x);
      其中偏移量,可以通过Unsafe.objectFieldOffset获得。参见AtomicInteger的偏移量获取:
      在这里插入图片描述

  • 普通读写-直接从内存中的一个地址读写:给地址 address,取出当前值或者放入 x。

    • getInt(Long/Short/Char/Byte/Float/Double/Object) (long address)
    • putInt(Long/Short/Char/Byte/Float/Double/Object) (long address, int/long/short/char/byte/float/double/Object x)
  • volatile读写:直接读写指定对象的属性,保证可见性和有序性,但是相对普通读写要更加昂贵。提供的方法除方法名外,其它(方法个数参数、)与普通读写-直接读写指定对象的属性完全相同。当原始对象的属性并未定义volatile,但需要其视作 volatile(也就是写入到主存或者从主存读)时,可以调用此方法。

    • getInt(Long/Short/Char/Byte/Float/Double/Object/Boolean)Volatile (Object o, long offset)
    • putInt(Long/Short/Char/Byte/Float/Double/Object/Boolean)Volatile (Object o, long offset, int/long/short/char/byte/float/double/Object/boolean x)
      如: getIntVolatile (Object o, long offset); putIntVolatile((Object o, long offset, int x);
  • 有序写:修改对象的属性,保证有序性

    • 有序写入只保证写入的有序性,不保证可见性,就是说一个线程的写入不保证其他线程立马可见。所谓有序性,就是保证指令不会重排序。
    • 而与volatile写入相比,putOrderedXX写入代价相对较低,putOrderedXX写入不保证可见性,但是保证有序性。

    Unsafe提供的有序写方法有:

    	public void putOrderedInt(Object o, long offset, int x) ;
    	public void putOrderedLong(Object o, long offset, long x);
    	// putObjectVolatile(Object, long, Object)的不保证可见性的版本。
    	// 通常,只有底层字段是Java volatile(或者数组单元格,否则只能使用
    	// volatile访问)时,这个方法才有用。
    	public void putOrderedObject(Object o, long offset, Object x) ;
    
直接堆外内存操作
  • 我们都知道Java不可以直接对内存进行操作,对象内存的分配和回收都是由JVM帮助我们实现的。但是Unsafe为我们在Java中提供了直接操作堆外du内存的能力。

    
     /*****************************1. 内存分配 ******************************/
    /**
    * 分配一块新的本地(堆外)内存,通过bytes指定内存块的大小(单位是byte),
    * 返回新开辟的内存的地址,并且其中的数据是未初始化的。
    * 如果内存块的内容不被初始化,那么它们一般会变成内存垃圾。
    * 生成的本机指针永远不会为零,并将对所有值类型进行对齐。
    * 可以通过freeMemory方法释放内存块,或者通过reallocateMemory方法调整内存块大小。
    * bytes值为负数或者过大会抛出IllegalArgumentException异常,
    * 如果系统拒绝分配内存会抛出OutOfMemoryError异常。
    */
    public native long allocateMemory(long bytes);
    
    /** 
    * 调整新本机内存块的大小,以字节为单位。
    * 新块的内容超过旧块的大小部分是未初始化的;
    * 当且仅当请求的大小为零时,生成的本机(native)指针将为零。
    * 生成的本机指针将对所有值类型进行对齐。通过调用freeMemory(long)来释
    * 放这个内存,或者使用reallocatemmemory (long, long)来调整它的大小。
    * 传递给该方法的地址可能为空,在这种情况下相当于执行allocateMemory操作。
     */
    public native long reallocateMemory(long address, long bytes);
    
    
     /***************************2. 内存赋值**********************************/
    /**
    * 
    * 从对象 o 的偏移量 offset 开始,一直向后 bytes 个字节都设置为 value。
    * 
    * 将给定内存块中的所有字节设置为固定值(通常是0),一般用于内存初始化。内存块的地址由对象引用o和偏移地址
    * 共同决定,如果对象引用o为null,offset就是绝对地址。
    * 第三个参数就是内存块的大小,如果使用allocateMemory进行内存开辟的话,
    * 这里的值应该和allocateMemory的参数一致。value就是设置的固定值,
    * 一般为0(这里可以参考netty的DirectByteBuffer)。一般而言,o为null,
    * 所以有个重载方法是public void setMemory(long offset, long bytes, byte value);,等效于setMemory(null, long offset, long bytes, byte value);
    */
    public native void setMemory(Object o, long offset, long bytes, byte value);
    public void setMemory(long address, long bytes, byte value);
    
    /**
     * copyMemory 从 srcBase 的 偏移量 srcOffset 开始,拷贝到 destBase 的 destOffset,拷贝 bytes 个字节。
     * 
     * 如果srcBase或者destBase为null,则srcOffset或者destOffset分别指代绝对地址。
     *
     */
    public native void copyMemory(Object srcBase, long srcOffset,
                           Object destBase, long destOffset,
                           long bytes);
    
    public void copyMemory(long srcAddress, long destAddress, long bytes);
    
    
    /**
     * 
     * 将本机指针存储到给定的内存地址中。如果地址为零,或者没有指向从
     * allocatemmemory (long)中获得的块,那么结果是未定义的。
     * 
     **/
    public native void putAddress(long address, long x);
    
    /** 
     * 从给定的内存地址获取本机指针。如果地址为0,
     * 或者没有指向从allocateMemory(long)中获得的块,那么结果是未定义的。
     * 
     **/
    public native long getAddress(long address);
    
    
    /**
     *
     *
     */
    // 清除内存
    public native void freeMemory(long var1);
    
  • Unsafe从堆外分配内存,因此用完后需要使用freeMemory释放内存,否则会导致内存泄露。

  • 也提供了一些获取内存信息的方法:getAddress、addressSize、pageSize

  • 值得注意的是:利用copyMemory方法可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,但只能做到对象浅拷贝。

线程调度
类加载
内存屏障
非常规对象实例化
数组操作
其他

VarHandle类

参见:

  • https://blog.csdn.net/sench_z/article/details/79793741
    Unsafe 是不建议开发者直接使用的,因为 Unsafe 所操作的并不属于Java标准,会容易带来一些安全性的问题。JDK9 之后,官方推荐使用 java.lang.invoke.Varhandle 来替代 Unsafe 大部分功能,对比 Unsafe ,Varhandle 有着相似的功能,但会更加安全,并且,在并发方面也提高了不少性能。

3.2 原子操作(Atomic*)相关组件

JUC中的原子操作类:

从JDK 1.5开始提供了java.util.concurrent.atomic包,在该包中提供了许多基于CAS实现的原子操作类,用法方便,性能高效,主要分以下4种类型:

  • 基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
  • 数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
  • 引用类型: AtomicReference, AtomicStampedRerence , AtomicMarkableReference ;
  • 对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。
    这些类存在的目的是对相应的数据进行原子操作。所谓原子操作,是指操作过程不会被中断,保证数据操作是以原子方式进行的。

//基本原子类型 实现对int、long、boolean、double类型的原子操作
AtomicBoolean atomicBoolean;
AtomicInteger atomicInteger;
AtomicLong atomicLong;
AtomicDouble atomicDouble;

//数组原子类型,实现对数组元素的原子操作。包括int 数组、long数组、double数组和引用数组
AtomicIntegerArray atomicIntegerArray;
AtomicLongArray atomicLongArray;
AtomicDoubleArray atomicDoubleArray;
AtomicReferenceArray atomicReferenceArray;

//引用原子类型
AtomicReference atomicReference;
AtomicStampedReference atomicStampedReference;
AtomicMarkableReference atomicMarkableReference;

//对象属性更改原子类型
AtomicIntegerFieldUpdater atomicIntegerFieldUpdater;
AtomicLongFieldUpdater atomicLongFieldUpdater;
AtomicReferenceFieldUpdater atomicReferenceFieldUpdater;

在这里插入图片描述

基本原子类型

很多情况下我们只是需要一个简单的、高效的、线程安全的递增递减方案。注意,这里有三个条件:简单,意味着程序员尽可能少的操作底层或者实现起来要比较容易;高效意味着耗用资源要少,程序处理速度要快;线程安全也非常重要,这个在多线程下能保证数据的正确性。这三个条件看起来比较简单,但是实现起来却难以令人满意。

通常情况下,在Java里面,++i或者–i不是线程安全的,这里面有三个独立的操作:或者变量当前值,为该值+1/-1,然后写回新的值。在没有额外资源可以利用的情况下,只能使用加锁才能保证读-改-写这三个操作时“原子性”的。

实现原理

底层调用Unsafe的相关方法实现。
AtomicInteger.getAndAdd(int delta)->Unsafe.getAndAddInt(this, VALUE, delta)
AtomicLong.getAndAdd(long delta)->Unsafe.getAndAddLong(this, VALUE, delta)

在这里插入图片描述
关于AtomicInteger 代码中出现的几个成员属性:

  • Unsafe是CAS的核心类
  • VALUE表示的是变量值在内存中的偏移地址,因为 Unsafe(U) 就是根据内存偏移地址获取数据的原值的
  • value是用volatile修饰的,这是非常关键的
//JDK11源码
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    /*
     * This class intended to be implemented using VarHandles(句柄), but there
     * are unresolved cyclic startup dependencies.
     * 注意!!!这个类打算使用VarHandles实现,但是存在无法解析的循环启动依赖项。
     */ 
    // 获取指针类Unsafe
    private static final Unsafe U = Unsafe.getUnsafe();
    
    //下述变量value在AtomicInteger实例对象内的内存偏移量,记为VALUE,也就是早期版本的valueoffset
    private static final long VALUE
        = U.objectFieldOffset(AtomicInteger.class, "value");
        
    //当前AtomicInteger封装的int变量value,注意用volatile 修饰符进行了修饰,确保该值对多线程修改的可见性
    private volatile int value;
 
    public AtomicInteger(int initialValue) { //含参构造器
        value = initialValue;
    }
    public AtomicInteger() {//空构造器
    }
   
     //获取当前最新值,因为value 用volatile修饰
    public final int get() {
        return value;
    }

     //设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。
    public final void set(int newValue) {
        value = newValue;
    }
    
     //最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
    public final void lazySet(int newValue) {
        U.putIntRelease(this, VALUE, newValue);
    }
    
     //获取旧值并设置新值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法
    public final int getAndSet(int newValue) {
        return U.getAndSetInt(this, VALUE, newValue);
    }
    
    //如果当前值为expect,则设置为update(当前值指的是value变量)
    public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
    }
	
	 //下面的所有操作的底层是weakCompareAndSetPlain然后再底层才是CAS操作,下面有源码图
    @Deprecated(since="9")
    public final boolean weakCompareAndSet(int expectedValue, int newValue) {
        return U.weakCompareAndSetIntPlain(this, VALUE, expectedValue, newValue);
    }

    /**
     * Possibly atomically sets the value to {@code newValue}
     * if the current value {@code == expectedValue},
     * with memory effects as specified by {@link VarHandle#weakCompareAndSetPlain}.
     *
     * @param expectedValue the expected value  期望值
     * @param newValue the new value 新值
     * @return {@code true} if successful 
     * @since 9
     */
     //下面的所有操作的底层是weakCompareAndSetPlain然后再底层才是CAS操作,下面有源码图
    public final boolean weakCompareAndSetPlain(int expectedValue, int newValue) {
        return U.weakCompareAndSetIntPlain(this, VALUE, expectedValue, newValue);
    }

  	// 返回旧值并当前值加1,底层CAS --》compareAndSetInt操作
    public final int getAndIncrement() {
        return U.getAndAddInt(this, VALUE, 1);
    }

	// 返回旧值并当前值减1(+(-1)),底层CAS --》compareAndSetInt操作
    public final int getAndDecrement() {
        return U.getAndAddInt(this, VALUE, -1);
    }

  	//当前值增加delta,返回旧值,底层CAS操作--》compareAndSetInt操作
    public final int getAndAdd(int delta) {
        return U.getAndAddInt(this, VALUE, delta);
    }

   //当前值加1,返回新值,底层CAS操作--》compareAndSetInt操作
    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

   //当前值减1,返回新值,底层CAS操作--》compareAndSetInt操作
    public final int decrementAndGet() {
        return U.getAndAddInt(this, VALUE, -1) - 1;
    }

   //当前值增加delta,返回新值,底层CAS操作--》compareAndSetInt操作
    public final int addAndGet(int delta) {
        return U.getAndAddInt(this, VALUE, delta) + delta;
    }
	//省略一些不常用的方法....

}



public final class Unsafe {
 	@HotSpotIntrinsicCandidate
    public native int getIntVolatile(Object o, long offset);

    @HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        //可看出通过一个do-while循环不断的重试更新要设置的值,直到成功为止,
        // 重试调用的是weakcompareAndSwapInt()方法,该方法调用的compareAndSetInt方法是一个CAS操作方法。
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }


    @HotSpotIntrinsicCandidate
    public final boolean weakCompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
        return compareAndSetInt(o, offset, expected, x);
    }
    
	//CAS操作方法
    @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);
                                                 
//CAS操作方法
   @HotSpotIntrinsicCandidate
    public native int     getIntVolatile(Object o, long offset);
}

以AtomicLong为例,AtomicLong.incrementAndGet()调用过程如下:
在这里插入图片描述

数组原子类型

引用原子类型

对象属性更改原子类型

3.3 synchronized+Object对象锁机制

利用synchronized+Object对象锁,可以实现线程等待/通知的机制。

public class Object {
	//Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or some other thread interrupts the current thread, or a specified amount of time has elapsed.
    public final native void wait(long timeout) throws InterruptedException;

	//Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object, or some other thread interrupts the current thread, or a certain amount of real time has elapsed.
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

	//this method behaves exactly as if it simply performs the call wait(0).
    public final void wait() throws InterruptedException {
        wait(0);
    }

	/**
	* Wakes up a single thread that is waiting on this object's 
	* monitor. If any threads are waiting on this object, 
	* one of them is chosen to be awakened. The choice is arbitrary 
	* and occurs at the discretion of the implementation. A thread 
	* waits on an object's monitor by calling one of the wait 
	* methods.
	* The awakened thread will not be able to proceed until the 
	* current thread relinquishes the lock on this object. The 
	* awakened thread will compete in the usual manner with any other
	*  threads that might be actively competing to synchronize on 
	* this object; for example, the awakened thread enjoys no 
	* reliable privilege or disadvantage in being the next thread 
	* to lock this object.
	* This method should only be called by a thread that is the owner 
	* of this object's monitor. A thread becomes the owner of the 
	* object's monitor in one of three ways:
	* 1. By executing a synchronized instance method of that object.
	* 2. By executing the body of a synchronized statement that 
	* synchronizes on the object.
	* 3. For objects of type Class, by executing a synchronized 
	* static method of that class.
	* 
	* Only one thread at a time can own an object's monitor.
	* 
	*/
    public final native void notify();

	/**
	* Wakes up all threads that are waiting on this object's monitor. 
	* A thread waits on an object's monitor by calling one of the 
	* wait methods.
	* The awakened threads will not be able to proceed until the
	*  current thread relinquishes the lock on this object. The 
	* awakened threads will compete in the usual manner with any 
	* other threads that might be actively competing to synchronize 
	* on this object; for example, the awakened threads enjoy no 
	* reliable privilege or disadvantage in being the next thread 
	* to lock this object.
	* This method should only be called by a thread that is the owner 
	* of this object's monitor. See the notify method for a 
	* description of the ways in which a thread can become the owner 
	* of a monitor. 
	*/
    public final native void notifyAll();

}

3.4 Lock+Condition锁框架组件

Lock+Condition锁框架简介

Lock+Condition锁框架作用

利用Lock+Condition接口,可以替代synchronized+Object对象锁,也可以实现线程等待/通知的机制。Lock对标synchronized,Condition对标Object对象锁的相关监视器方法。
但Lock+Condition接口的实现方式更加灵活强大。

Lock lock = new ReentrantLock();
Condition condition=lock.newCondition();

lock.lock();
try{ 
//业务逻辑;
condition.await();
condition.await(30, TimeUnit.SECONDS);
condition.notify();
condition.notify();
} finally {
	lock.unlock()
}


// 相当于 
Object lock = new Object();
synchronized(lock ){
	//业务逻辑
   lock.wait();
   lock.wait(30);
   lock.notify();
   lock.notifyAll();
}
Lock(ReenTrantLock)+Condition锁框架与synchronized+Object的区别

**相同点:**都是可重入锁,性能相差不大,两者都可用的情况下,推荐使用synchronized+Object。

可重入性

两者都是可重入的,在这方面区别不大。两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

锁的实现
  1. synchronized是java中的关键字,是jvm层面的,对开发者透明(开发者看不到,除非去看jvm的源码),ReentrantLock是jdk的类库,提供的服务是通过api访问得到的
  2. ReentrantLock需要手动释放,synchronized不需要,只要走出临界区,锁自动释放。
  3. ReentrantLock提供公平锁和非公平锁的实现,synchronized本身是非公平锁
  4. synchronized是通过在字节码加对象监视器实现的,Reentrntlock是依赖基于硬件支持的CAS操作实现的
性能的区别

在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

功能区别
  • 便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

  • 锁的细粒度和灵活度:ReentrantLock提供condition api工具,同一个lock下可创建多个condition,condition可以很方便的实现分组唤醒线程;synchronized只能配合一个Object锁对象,很明显ReenTrantLock优于synchronized

  • 加解锁的顺序控制:synchronized+Object 上锁顺序和解锁顺序必须相反;Condition无此要求,更加灵活。

Lock+Condition锁框架比synchronized+Object的强大之处(优势)

Lock+Condition实现方式功能更强大:

  1. synchronized+Object 只能有一个锁对象,要么随机唤醒一个线程要么唤醒全部线程。但通过Lock.newCondition()可以创建多个Condition对象,每个Condition对象可以负责维护同一**业务线程组(如put线程组)的等待/通知的机制。这样可以通过多个Condition分别维护不同业务线程组(如put线程组、take线程组)**的等待/通知的机制。锁的细粒度和灵活度方面, Lock+Condition比synchronized+Object强大许多。
  2. synchronized+Object 上锁顺序和解锁顺序必须相反;Condition无此要求,这种情况下,使用Condition更合适。
Lock+Condition锁框架比synchronized+Object的技术选型
  • 在两种方法都可用的情况下,建议使用synchronized,因为synchronized不用显式执行unlock操作,且最新版JDK其性能与Lock+Condition相当。
  • 在synchronized+Object无法满足需求的情况下,再考虑使用Lock+Condition。
Lock+Condition锁框架场景举例

As an example, suppose we have a bounded buffer which supports put and take methods. If a take is attempted on an empty buffer, then the thread will block until an item becomes available; if a put is attempted on a full buffer, then the thread will block until a space becomes available. We would like to keep waiting put threads and take threads in separate wait-sets so that we can use the optimization of only notifying a single thread at a time when items or spaces become available in the buffer. This can be achieved using two Condition instances.
举个例子,假设我们有一个支持put和take方法的有界缓冲区。如果尝试在空缓冲区上执行take,那么线程将阻塞,直到某个项目可用为止;如果尝试在已满的缓冲区上进行put,那么线程将阻塞,直到有空间可用为止。我们希望为put线程和take线程各自使用不同的监视器锁(putLock,takeLock),这样我们就可以使用优化,即当缓冲区中的项或空间变为可用时,一次只通知一个线程。这可以通过使用两个Condition实例来实现。
(java.util.concurrent.ArrayBlockingQueue类提供了此功能(以下代码),因此没有理由实现此示例用法类,这里只是举例说明Lock+Condition的用法)

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition putLock  = lock.newCondition(); 
   final Condition takeLock = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;
    
   /**
   * 被put线程调用 
   */
   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       //缓冲区满了,put线程被阻塞,直到收到take线程的putLock.signal()通知
       while (count == items.length)
         putLock.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       takeLock.signal();
     } finally {
       lock.unlock();
     }
   }

   /**
   * 被take线程调用 
   */
   public Object take() throws InterruptedException {
     lock.lock();
     try {
       //缓冲区为空(没有数据),take线程被阻塞,直到收到put线程的takeLock.signal()通知
       while (count == 0)
         takeLock.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       putLock.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }
 
Lock+Condition锁框架相关接口
Lock接口
package java.util.concurrent.locks;
/**
*  
* Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.
* 
* Lock实现提供了比synchronized 方法和statements所能获得的更广泛的锁操作。它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的Condition对象。
* 
* A lock is a tool for controlling access to a shared resource by multiple threads. Commonly, a lock provides exclusive access to a shared resource: only one thread at a time can acquire the lock and all access to the shared resource requires that the lock be acquired first. However, some locks may allow concurrent access to a shared resource, such as the read lock of a ReadWriteLock.
* 
* 锁是一种用于控制多个线程对共享资源的访问的工具。通常,锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,而对共享资源进行的所有访问都需要首先获取锁。但是,有些锁可能允许对共享资源进行并发访问,例如读写锁ReadWriteLock的读锁。
* 
* 
* 
* The use of synchronized methods or statements provides access to the implicit monitor lock associated with every object, but forces all lock acquisition and release to occur in a block-structured way: when multiple locks are acquired they must be released in the opposite order, and all locks must be released in the same lexical scope in which they were acquired.
* 
* synchronized 方法或statements 的使用提供了对与每个对象关联的隐式监视锁的访问,但强制所有锁的获取和释放都以块结构的方式进行:当获取多个锁时,必须以相反的顺序释放它们,并且所有锁都必须在获取它们的同一词法范围内释放。
* 
* 
* While the scoping mechanism for synchronized methods and statements makes it much easier to program with monitor locks, and helps avoid many common programming errors involving locks, there are occasions where you need to work with locks in a more flexible way. For example, some algorithms for traversing concurrently accessed data structures require the use of "hand-over-hand" or "chain locking": you acquire the lock of node A, then node B, then release A and acquire C, then release B and acquire D and so on. Implementations of the Lock interface enable the use of such techniques by allowing a lock to be acquired and released in different scopes, and allowing multiple locks to be acquired and released in any order.
* 
* 虽然synchronized 方法和statements 的作用域机制使使用监视锁进行编程变得更加容易,并有助于避免许多涉及锁的常见编程错误,但在某些情况下,您需要以更灵活的方式使用锁。例如,一些遍历并发访问的数据结构的算法需要使用“切换-切换”或“链式锁定”:您获取节点A的锁,然后节点B的锁,然后释放A并获取C,然后释放B并获取D,依此类推。lock接口的实现允许在不同范围内获取和释放锁,从而允许使用这些技术,并且允许以任何顺序获取和释放多个锁。
* 
* With this increased flexibility comes additional responsibility. The absence of block-structured locking removes the automatic release of locks that occurs with synchronized methods and statements. In most cases, the following idiom should be used:
* 随着这种灵活性的增加,带来了额外的责任。块结构锁的缺失导致了synchronized 方法和statements中发生的锁的自动释放逻辑的移除。因此,在使用Lock时,大多数情况下,应该使用以下语句:
* 
*  Lock l = ...;
*  l.lock();
*  try {
*    // access the resource protected by this lock
*  } finally {
*    l.unlock();
*  }
* When locking and unlocking occur in different scopes, care must be taken to ensure that all code that is executed while the lock is held is protected by try-finally or try-catch to ensure that the lock is released when necessary.
* 
* 当锁定和解锁发生在不同的作用域中时,必须小心确保在持有锁定时执行的所有代码都受到try finally或try catch的保护,以确保在必要时释放锁定。
* 
* 
* Lock implementations provide additional functionality over the use of synchronized methods and statements by providing a non-blocking attempt to acquire a lock (tryLock()), an attempt to acquire the lock that can be interrupted (lockInterruptibly() ), and an attempt to acquire the lock that can timeout (tryLock(long, TimeUnit)).
* 
* Lock的实现类提供了比synchronized 方法和statements更多的额外的功能,包括尝试获取非阻塞尝试(tryLock())、获取可以中断的锁定的尝试(lockInterruptibly() )和获取可以超时的锁定的试图(tryLock(long, TimeUnit))。
* 
* A Lock class can also provide behavior and semantics that is quite different from that of the implicit monitor lock, such as guaranteed ordering, non-reentrant usage, or deadlock detection. If an implementation provides such specialized semantics then the implementation must document those semantics.
* 
* Lock类还可以提供与隐式监视锁截然不同的行为和语义,例如有保证的排序、不可重入的使用或死锁检测。如果一个实现类提供了这种专门的语义,那么该实现类必须记录这些语义。
* 
* Note that Lock instances are just normal objects and can themselves be used as the target in a synchronized statement. Acquiring the monitor lock of a Lock instance has no specified relationship with invoking any of the lock methods of that instance. It is recommended that to avoid confusion you never use Lock instances in this way, except within their own implementation.
* 
* 请注意,Lock实例只是普通对象,它们本身可以用作synchronized 语句中的目标。获取lock实例的监视器锁与调用该实例的任何锁方法都没有指定的关系。为了避免混淆,建议您永远不要以这种方式使用Lock实例,除非在它们自己的实现中使用。
* 
* Except where noted, passing a null value for any parameter will result in a NullPointerException being thrown.
* 
* 除非另有说明,否则为任何参数传递null值都将导致引发NullPointerException。
* 
* 
* 
* Memory Synchronization
* 内存同步
* 
* All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock, as described in The Java Language Specification (17.4 Memory Model) :
* A successful lock operation has the same memory synchronization effects as a successful Lock action.
* A successful unlock operation has the same memory synchronization effects as a successful Unlock action.
* Unsuccessful locking and unlocking operations, and reentrant locking/unlocking operations, do not require any memory synchronization effects.
* 
* 如Java语言规范(17.4内存模型)中所述,所有Lock实现类都必须强制执行与内置监视器锁所提供的相同的内存同步语义:
* 1. 成功的lock操作与成功的lock动作具有相同的内存同步效果。
* 2. 成功unlock操作与成功解锁动作具有相同的内存同步效果。
* 3. 未成功的locking和unlocking操作以及可重入的locking/unlocking操作不需要任何内存同步效果。
* 
* Implementation Considerations
* 实现类注意事项
* 
* The three forms of lock acquisition (interruptible, non-interruptible, and timed) may differ in their performance characteristics, ordering guarantees, or other implementation qualities. Further, the ability to interrupt the ongoing acquisition of a lock may not be available in a given Lock class. 
* Consequently, an implementation is not required to define exactly the same guarantees or semantics for all three forms of lock acquisition, nor is it required to support interruption of an ongoing lock acquisition. An implementation is required to clearly document the semantics and guarantees provided by each of the locking methods. It must also obey the interruption semantics as defined in this interface, to the extent that interruption of lock acquisition is supported: which is either totally, or only on method entry.
* 锁获取的三种形式(可中断、不可中断和超时获取锁)可能在性能特征、排序保证或其他实现质量方面有所不同。此外,中断正在进行的锁的获取的能力在给定的锁类中可能不可用。
* 因此,实现类不需要为所有三种形式的锁获取定义完全相同的保证或语义,也不需要支持正在进行的锁获取的中断。需要一个实现来清楚地记录每个锁定方法所提供的语义和保证。它还必须遵守该接口中定义的中断语义,以支持锁获取的中断:这要么是完全的,要么只是在方法进入时。
* 
* 
* As interruption generally implies cancellation, and checks for interruption are often infrequent, an implementation can favor responding to an interrupt over normal method return. This is true even if it can be shown that the interrupt occurred after another action may have unblocked the thread. An implementation should document this behavior.
* 
* 由于中断通常意味着取消,并且对中断的检查通常不频繁,因此实现可能有利于响应中断,而不是正常的方法返回。即使可以表明中断发生在另一个操作可能已解除线程阻塞之后,这也是正确的。实现应该记录这种行为。
*/
public interface Lock {
	
	// 获取锁,若获取不到则阻塞等待,直到获取到锁为止,无法被中断
	void lock();
	
	/**
	 * 除非当前线程被中断,否则将获取锁。
	 * 
	 * 如果锁可用,则获取锁并立即返回。
	 * 
	 * 如果锁不可用,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下两种情况之一,才会退出休眠状态:
	 * 1. 当前线程获取到锁,或
	 * 2. 其他线程中断了当前线程,并且支持中断锁获取。
	 * 
	 * 当前线程出现以下情况时,抛出InterruptedException,并清除当前线程的中断状态:
	 * 1. 在进入该方法时设置其中断状态;或
	 * 2. 在获取锁时被中断,并且支持锁获取的中断
	 * 
	 * 实现类注意事项
	 * 
	 * 在一些实现中,中断锁定获取的能力可能是不可能的,如果可能的话,这可能是一种昂贵的操作。程序员应该意识到可能会出现这种情况。在这种情况下,实施应该记录下来。
	 * 
	 * 与正常的方法返回相比,一个实现可以更倾向于响应中断。(An implementation can favor responding to an interrupt over normal method return.)
	 * 
	 * Lock实现可能能够检测锁的错误使用,例如会导致死锁的调用,并且在这种情况下可能引发(未检查的)异常。情况和异常类型必须由该Lock实现进行记录。
	 * 
	 * throws:
	 * 
	 * InterruptedException–如果当前线程在获取锁时被中断(并且支持中断获取锁)
	*/
	void lockInterruptibly() throws InterruptedException;
	
	/**
	* 
	* 只有在调用时锁是空闲的情况下,才会获取锁。
	* 如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。
	* 这种方法的一个典型用法是:
	*  Lock lock = ...;
	*  if (lock.tryLock()) {
	*    try {
	*      // manipulate protected state
	*    } finally {
	*      lock.unlock();
	*    }
	*  } else {
	*    // perform alternative actions
	*  }
	*  
	*  这种用法可确保在获取锁时解锁,而在未获取锁时不会尝试解锁。
	*/
	boolean tryLock();


	// 尝试获取锁,如果没获取到锁,就等待一段时间,这段时间内还没获取到锁就返回false
   /**
	*  
	* 如果锁在给定的等待时间内是空闲的,并且当前线程没有被中断,则获取锁。
	* 如果锁可用,则此方法会立即返回值true。如果锁不可用,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下三种情况之一,才会退出休眠状态:
	* 1. 当前线程获取到锁,或
	* 2. 其他线程中断了当前线程,并且支持中断锁获取,或
	* 3. 指定的等待时间已过
	* 
	* 当前线程出现以下情况时,抛出InterruptedException,并清除当前线程的中断状态:
	* 1. 在进入该方法时设置其中断状态;或
	* 2. 在获取锁时被中断,并且支持锁获取的中断
	* 
	* 实现类注意事项
	* 
	* 在一些实现中,中断锁定获取的能力可能是不可能的,如果可能的话,这可能是一种昂贵的操作。程序员应该意识到可能会出现这种情况。在这种情况下,实施应该记录下来。
	* 与正常的方法返回或报告超时相比,实现更倾向于响应中断。
	* Lock实现可能能够检测锁的错误使用,例如会导致死锁的调用,并且在这种情况下可能引发(未检查的)异常。情况和异常类型必须由该Lock实现进行记录。
	*/
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
	/** 释放锁。
	* 锁的实现通常会对线程可以释放锁施加限制(通常只有锁的持有者才能释放锁),
	* 如果违反限制,则可能引发(未检查的)异常。
	* 任何限制和异常类型都必须由该Lock实现进行记录。
	*/
	void unlock();
	
	/**
	* Returns a new Condition instance that is bound to this Lock instance.
	* Before waiting on the condition the lock must be held by the current thread. 
	* A call to Condition.await() will atomically release the lock before waiting and re-acquire the lock before the wait returns.
	* 
	* Implementation Considerations
	* The exact operation of the Condition instance depends on the Lock implementation and must be documented by that implementation.
	* 	 
	*/
	Condition newCondition();
}

Condition 接口
  1. Condition接口必须与Lock配合使用,通过Lock.newCondition();工厂方法创建。

  2. 一个Condition对象相当于一个Object锁对象,包含了Object监视器方法的对标方法。

    Object对象Condition对象
    public final void wait() throws InterruptedExceptionvoid await() throws InterruptedException; void awaitUninterruptibly();
    native void wait(long timeoutInMilliseconds) throws InterruptedExceptionboolean await(long time, TimeUnit unit) throws InterruptedException; long awaitNanos(long nanosTimeout) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException;
    native void notify()void signal();
    native void notifyAll()void signalAll();
    public interface Condition {
    	// Causes the current thread to wait until it is signalled or interrupted.(可被中断)
    	void await() throws InterruptedException;
    	
    	// Causes the current thread to wait until it is signalled.(一直等,不可被中断)
        void awaitUninterruptibly();
        
        // Causes the current thread to wait until it is signalled or interrupted, or the specified waiting time elapses.
        long awaitNanos(long nanosTimeout) throws InterruptedException;
        
        // Causes the current thread to wait until it is signalled or interrupted, or the specified waiting time elapses.
        boolean await(long time, TimeUnit unit) throws InterruptedException;
    
    	// Causes the current thread to wait until it is signalled or interrupted, or the specified deadline elapses
        boolean awaitUntil(Date deadline) throws InterruptedException;
        
        // Wakes up one waiting thread.
    	// If any threads are waiting on this condition then one is selected for waking up. That thread must then re-acquire the lock before returning from await.
        void signal();
    
    	// Wakes up all waiting threads.
    	// If any threads are waiting on this condition then they are all woken up. Each thread must re-acquire the lock before it can return from await.
        void signalAll();
    
    }
    
  3. 一个Lock可以创建多个Condition对象。而synchronized只能使用一个Object锁对象。

Lock+Condition锁框架实现类组件-UML图
不带依赖

在这里插入图片描述

带依赖(show dependencies)

在这里插入图片描述

Lock+Condition锁框架实现基础组件–AQS(AbstractQueuedSynchronizer)
AQS原理
AQS实现类

在这里插入图片描述

上图实现类从左到右依次为:

  1. Semaphore.Sync、Semaphore.FairSync和Semaphore.NonfairSync。
  2. ThreadPoolExecutor.Worker
  3. ReentrantLock.Sync、ReentrantLock.FairSync和ReentrantLock.NonfairSync
  4. CountDownLatch.Sync
  5. ReentrantReadWriteLock.Sync、ReentrantReadWriteLock.FairSync和ReentrantReadWriteLock.NonfairSync。
  6. NonReentrantLock

Lock+Condition实现具体组件

ReenTrantLock

参见:可重入锁和不可重入锁详解

功能说明(来自源码类注释)

A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.
A ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it. A thread invoking lock will return, successfully acquiring the lock, when the lock is not owned by another thread. The method will return immediately if the current thread already owns the lock. This can be checked using methods isHeldByCurrentThread, and getHoldCount.
The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation. Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock. Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.
It is recommended practice to always immediately follow a call to lock with a try block, most typically in a before/after construction such as:

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

In addition to implementing the Lock interface, this class defines a number of public and protected methods for inspecting the state of the lock. Some of these methods are only useful for instrumentation and monitoring.
Serialization of this class behaves in the same way as built-in locks: a deserialized lock is in the unlocked state, regardless of its state when serialized.
This lock supports a maximum of 2147483647 recursive locks by the same thread. Attempts to exceed this limit result in Error throws from locking methods.

译文:

  1. 一个可重入互斥锁,其基本行为和语义与使用同步方法和语句访问的隐式监视器锁相同,但具有扩展功能

  2. ReentrantLock由上次成功锁定但尚未解锁的线程所拥有。当锁不为另一个线程所拥有时,调用lock方法的线程将返回并成功获取锁。如果当前线程已经拥有该锁,则该方法将立即返回。这可以使用方法isHeldByCurrentThread和getHoldCount进行检查。

  3. 这个类的构造函数接受一个可选的公平性参数。当设置为true时(公平锁),在争用下,锁定有利于授予对等待时间最长的线程。否则,此锁不能保证任何特定的访问顺序。使用由许多线程访问的公平锁的程序可能显示出比使用默认设置的程序更低的总体吞吐量(即,更慢;通常慢得多),但获得锁的时间差异较小,并保证不会出现饥饿。然而,请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程中的一个线程可以连续多次获得它,而其他活动线程没有进行并且当前没有持有该锁。还要注意,untimed tryLock()方法不尊重公平性设置。如果锁可用,即使其他线程正在等待,它也会成功。

  4. 建议始终在调用后立即使用try块进行锁定,最常见的是在构造之前/之后,例如:

     class X {
       private final ReentrantLock lock = new ReentrantLock();
       // ...
    
       public void m() {
         lock.lock();  // block until condition holds
         try {
           // ... method body
         } finally {
           lock.unlock()
         }
       }
     }
    
  5. 除了实现Lock接口外,这个类还定义了许多公共和受保护的方法,用于检查锁的状态。其中一些方法仅适用于仪器仪表和监测。

此类的序列化行为与内置锁相同:反序列化的锁处于未锁定状态,而与序列化时的状态无关。

此锁最多支持同一线程的2147483647个递归锁。试图超过此限制会导致锁定方法抛出错误。

ReenTrantLock独有的能力:
  1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
什么情况下使用ReenTrantLock

答案是,如果你需要实现ReenTrantLock的三个独有功能时。

ReenTrantLock实现的原理

在这里插入图片描述
在这里插入图片描述

  1. 上图Sync、NonfairSync、FairSync都是在ReentrantLock内部定义。

  2. 在ReentrantLock定义了成员变量private final Sync sync;,并提供了构造方法,根据faire决定创建公平锁还是非公平锁。

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

    附:ReentrantLock还提供了默认的无参构造方法,调用了this(false);即默认创建推荐的、效率更高非公平锁。

  3. Sync继承了AQS(AbstractQueuedSynchronizer),AQS中包含一个队列,当一个线程调用ReentrantLock.lock()方法时,会调用AQS.acquire(int args)调用方法,该方法的基本逻辑是:先调用AQS,tryAcquire(arg)(该方法为模板方法,需要子类提供实现),若返回false,说明此锁已经被别的线程获取,那么当前线程就会被封装为AQS中队列的节点,加入等待队列,之后调用Unsafe.park()挂起,直到别的线程调用unlock解锁(内部是unpark),当前线程会被唤醒然后获取锁。
    附:调用Unsafe.park()或Object.wait()都可以挂起当前线程,前者与Unsafe.unpark(Object thread )配合使用,可以精准地唤醒某个线程,后者与Object.notify()和Object.notifyAll()使用,不能精准唤醒。

  4. FairSync和NonfairSync均实现了AQS,tryAcquire(arg)方法,因此公平锁和非公平锁的实现逻辑均在子类的该方法实现类中。

    //ReentrantLock.lock()方法
    public void lock() {
    	//调用sync.acquire(1)即调用父类的AQS.acquire(int arg) 方法
    	sync.acquire(1);
    }
    
    //AQS.acquire(int arg) 
    /** 
    * AQS中包含一个等待队列,当一个线程调用ReentrantLock.lock()方法时,进入到此方法。
    * 该方法的逻辑为:先调用tryAcquire(arg) 尝试获取锁,若返回false,
    * 说明此锁已经被别的线程获取,那么当前线程就会被封装为AQS中队列的一个节点,
    * 并加入等待队列,之后调用Unsafe.park()挂起当前线程,
    * 直到别的线程调用unlock解锁(内部是unpark)重新唤醒当前线程。
    * 
    * tryAcquire方法需要子类实现,FairSync和NonfairSync均实现了
    * tryAcquire方法,因此公平锁和非公平锁的实现逻辑均在子类的该方法实现类中。
    */
    public final void acquire(int arg) {
     	// 调用tryAcquire(arg) 尝试获取锁
     	// 如果失败了,就调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))排队
    
        if (!tryAcquire(arg) &&
        	// 注意addWaiter()这里传入的节点模式为独占模式
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    //AQS.tryAcquire方法,模板方法,需要子类实现
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
  5. ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

ReentrantLock源码分析–公平锁获取过程
/**
* 一个可重入互斥锁,其基本行为和语义与使用synchronized方法和statements访问的隐式监视器锁相同,但具有扩展功能。
* ReentrantLock由上次成功锁定但尚未解锁的线程所拥有。当锁不为另一个线程所拥
* 有时,调用lock()方法的线程将返回并成功获取锁。如果当前线程已经拥有该锁,
* 则该方法将立即返回。这可以使用方法isHeldByCurrentThread和getHoldCount
* 进行检查。
* 这个类的构造函数接受一个可选的公平性参数。当设置为true时,在争用下,锁定
* 有利于授予对等待时间最长的线程的访问权限。否则,此锁不能保证任何特定的访问
* 顺序。使用由许多线程访问的公平锁的程序可能显示出比使用默认设置的程序更低的
* 总体吞吐量(即,更慢;通常慢得多),但获得锁的时间差异较小,并保证不会出现
* 饥饿。然而,请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁
* 的许多线程中的一个线程可以连续多次获得它,而其他活动线程没有进行并且当前没
* 有持有该锁。还要注意,untimed tryLock()方法不尊重公平性设置。如果锁可
* 用,即使其他线程正在等待,它也会成功。
* 
* ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。
* (1)抽象类Sync实现了AQS的部分方法;
* (2)NonfairSync实现了Sync,主要用于非公平锁的获取;
* (3)FairSync实现了Sync,主要用于公平锁的获取。
* 
*/
public class ReentrantLock implements Lock, java.io.Serializable {
	//主要属性,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁
    private final Sync sync;

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 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();
    }
    
    /**
    * ReentrantLock.lock()
    * 
    * ->sync.acquire(1),实际调用父类
    * AbstractQueuedSynchronizer.acquire(1),该方法实现见下方代码。其逻辑是先调用tryAcquire() 尝试获取锁,若尝试获取锁失败,线程将排队,并以自旋(无限循环)方式一直不断尝试调用tryAcquire,直到tryAcquire成功。
    * 
    * ->tryAcquire() :FairSync和NonfairSync均实现了tryAcquire() 实现逻辑详见各自的注释。
    *
    */
    public void lock() {
        sync.acquire(1);
    }

    /**
     * Attempts to release this lock.
     *
     * <p>If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then {@link IllegalMonitorStateException} is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() {
    	//实际调用AQS.release->Sync.tryRelease()
        sync.release(1);
    }
    
    public Condition newCondition() {
    	//sync.newCondition()会new ConditionObject(),而
    	// ConditionObject在父类AQS中定义
        return sync.newCondition();
    }
    
	/**
	* Base of synchronization control for this lock. Subclassed into 
	* fair and nonfair versions below. 
	* Uses AQS state to represent the number of holds on the lock.
	* 此锁的同步控制的基础。子类分为公平(FairSync)和非公平版本(NonfairSync)。
	* 使用AQS状态来表示锁上的保留数。
	*/
	abstract static class Sync extends AbstractQueuedSynchronizer {
        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            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;
        }
        
        @ReservedStackAccess
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
        
        final ConditionObject newCondition() {
        	//ConditionObject在父类AQS中定义
            return new ConditionObject();
        }
	}
	
 	static final class NonfairSync extends Sync {
        protected final boolean tryAcquire(int acquires) {
        	//Sync.nonfairTryAcquire(int acquires)
            return nonfairTryAcquire(acquires);
        }
    }

    static final class FairSync extends Sync {
        @ReservedStackAccess
        protected final boolean tryAcquire(int acquires) {
        	// 当前线程
            final Thread current = Thread.currentThread();
            // 查看当前状态变量的值, state表示当前线程hold着的锁的数量
            int c = getState();
            // 如果状态变量的值为0,说明暂时还没有人占有锁
            if (c == 0) {
            
        		// 如果没有其它比当前线程等待时间更久的线程在排队,那么当前线程尝试更新state的值为1
        		// 用CAS操作尝试上锁,如果成功了,则说明当前线程获取了锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    //当前线程获取了锁,把自己设置到exclusiveOwnerThread变量中
                    setExclusiveOwnerThread(current);
                    // 返回true说明成功获取了锁
                    return true;
                }
            }
            // 如果当前线程本身就占有着锁,现在又尝试获取锁
    		// 那么,直接让它获取锁并返回true
            else if (current == getExclusiveOwnerThread()) {
            	// 状态变量state的值加1
                int nextc = c + acquires;
                // 如果溢出了,则报错
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                    
        		// 设置到state中
        		// 这里不需要CAS更新state
        		// 因为当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的
        		// 所以不存在竞争,自然不需要使用CAS来更新
                setState(nextc);
                
                 // 当线程获取锁成功
                return true;
            }
            // 当前线程尝试获取锁失败
            return false;
        }
	}


}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

  /** 
   * 先调用tryAcquire() 尝试获取锁,若尝试获取锁失败,线程将排队
   * 
   * tryAcquire方法需要子类实现,FairSync和NonfairSync均实现了
   * tryAcquire方法
   */
   public final void acquire(int arg) {
	    // 尝试获取锁
	    // 如果失败了,就排队
        if (!tryAcquire(arg) &&
        	// 注意addWaiter()这里传入的节点模式为独占模式
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
	
	//FairSync和NonfairSync均实现了该方法
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
	    
	// 调用这个方法,说明上面尝试获取锁失败了
	private Node addWaiter(Node mode) {
	    // 新建一个节点
	    Node node = new Node(Thread.currentThread(), mode);
	    // 这里先尝试把新节点加到尾节点后面
	    // 如果成功了就返回新节点
	    // 如果没成功再调用enq()方法不断尝试
	    Node pred = tail;
	    // 如果尾节点不为空
	    if (pred != null) {
	        // 设置新节点的前置节点为现在的尾节点
	        node.prev = pred;
	        // CAS更新尾节点为新节点
	        if (compareAndSetTail(pred, node)) {
	            // 如果成功了,把旧尾节点的下一个节点指向新节点
	            pred.next = node;
	            // 并返回新节点
	            return node;
	        }
	    }
	    // 如果上面尝试入队新节点没成功,调用enq()处理
	    enq(node);
	    return node;
	}
	// AbstractQueuedSynchronizer.enq()
	private Node enq(final Node node) {
	    // 自旋,不断尝试
	    for (;;) {
	        Node t = tail;
	        // 如果尾节点为空,说明还未初始化
	        if (t == null) { // Must initialize
	            // 初始化头节点和尾节点
	            if (compareAndSetHead(new Node()))
	                tail = head;
	        } else {
	            // 如果尾节点不为空
	            // 设置新节点的前一个节点为现在的尾节点
	            node.prev = t;
	            // CAS更新尾节点为新节点
	            if (compareAndSetTail(t, node)) {
	                // 成功了,则设置旧尾节点的下一个节点为新节点
	                t.next = node;
	                // 并返回旧尾节点
	                return t;
	            }
	        }
	    }
	}
	
	// 调用上面的addWaiter()方法使得新节点已经成功入队了
	// 这个方法是尝试让当前节点来获取锁的
	final boolean acquireQueued(final Node node, int arg) {
	    // 失败标记
	    boolean failed = true;
	    try {
	        // 中断标记
	        boolean interrupted = false;
	        // 自旋
	        for (;;) {
	            // 当前节点的前一个节点
	            final Node p = node.predecessor();
	            // 如果当前节点的前一个节点为head节点,则说明轮到自己获取锁了
	            // 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁
	            if (p == head && tryAcquire(arg)) {
	                // 尝试获取锁成功
	                // 这里同时只会有一个线程在执行,所以不需要用CAS更新
	                // 把当前节点设置为新的头节点
	                setHead(node);
	                // 并把上一个节点从链表中删除
	                p.next = null; // help GC
	                // 未失败
	                failed = false;
	                return interrupted;
	            }
	            // 是否需要阻塞
	            if (shouldParkAfterFailedAcquire(p, node) &&
	                // 真正阻塞的方法
	                parkAndCheckInterrupt())
	                // 如果中断了
	                interrupted = true;
	        }
	    } finally {
	        // 如果失败了
	        if (failed)
	            // 取消获取锁
	            cancelAcquire(node);
	    }
	}
	
	// 这个方法是在上面的for()循环里面调用的
	// 第一次调用会把前一个节点的等待状态设置为SIGNAL,并返回false
	// 第二次调用才会返回true
	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	    // 上一个节点的等待状态
	    // 注意Node的waitStatus字段我们在上面创建Node的时候并没有指定
	    // 也就是说使用的是默认值0
	    // 这里把各种等待状态再贴出来
	    //static final int CANCELLED =  1;
	    //static final int SIGNAL    = -1;
	    //static final int CONDITION = -2;
	    //static final int PROPAGATE = -3;
	    int ws = pred.waitStatus;
	    // 如果等待状态为SIGNAL(等待唤醒),直接返回true
	    if (ws == Node.SIGNAL)
	        return true;
	    // 如果前一个节点的状态大于0,也就是已取消状态
	    if (ws > 0) {
	        // 把前面所有取消状态的节点都从链表中删除
	        do {
	            node.prev = pred = pred.prev;
	        } while (pred.waitStatus > 0);
	        pred.next = node;
	    } else {
	        // 如果前一个节点的状态小于等于0,则把其状态设置为等待唤醒
	        // 这里可以简单地理解为把初始状态0设置为SIGNAL
	        // CONDITION是条件锁的时候使用的
	        // PROPAGATE是共享锁使用的
	        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	    }
	    return false;
	}
	
	private final boolean parkAndCheckInterrupt() {
	    // 阻塞当前线程
	    // 底层调用的是Unsafe的park()方法
	    LockSupport.park(this);
	    // 返回是否已中断
	    return Thread.interrupted();
	}
	
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    public final boolean release(int arg) {
    	//tryRelease是模板方法,需要在子类实现,ReentrantLock.Sync实现了该方法
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    //模板方法,需要在子类实现(ReentrantLock.Sync实现了该方法)
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

下面我们看一下公平锁主要方法的调用关系:

ReentrantLock#lock()
->ReentrantLock.FairSync#lock() // 公平模式获取锁
  ->AbstractQueuedSynchronizer#acquire() // AQS的获取锁方法
    ->ReentrantLock.FairSync#tryAcquire() // 尝试获取锁
    ->AbstractQueuedSynchronizer#addWaiter()  // 添加到队列
	  ->AbstractQueuedSynchronizer#enq()  // 入队
    ->AbstractQueuedSynchronizer#acquireQueued() // 里面有个for()循环,唤醒后再次尝试获取锁
      ->AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire() // 检查是否要阻塞
      ->AbstractQueuedSynchronizer#parkAndCheckInterrupt()  // 真正阻塞的地方

公平锁获取锁的主要过程大致如下:

1. 尝试获取锁,如果获取到了就直接返回了;
2. 尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;
3. 然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;
4. 如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL)
5. 调用parkAndCheckInterrupt()阻塞当前线程;
6. 如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;
7. 如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。

以上就是整个公平锁获取锁的过程。

ReentrantLock源码分析–非公平锁获取过程

下面我们看看非公平锁是怎么获取锁的。

相对于公平锁,非公平锁加锁的过程主要有两点不同:

1. 一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;
2. 在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;

总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。

CountDownLatch

Latch /lætʃ/ :锁存器;闩锁;闭锁

概念及使用场景

参见:CountDownLatch的理解和使用

核心API
  1. 初始化:
    //n为计数器
    final CountDownLatch latch = new CountDownLatch(n);

  2. wait
    // 主线程被阻塞,直到n=0为止
    // 若由于某种原因计数器没有完全被释放,
    // 则n永远不会为0,此时将使得主线程永远阻塞而发生死锁
    //解决办法是用重载方法,设置一个等待超时时间
    latch.await();
    //主线程被阻塞,直到n=0或超时为止
    latch.await(6000, TimeUnit.MILLISECONDS);

  3. //执行计数器-1操作,一般由各子线程执行,执行完该方法后,线程继续执行,不会被阻塞
    latch.countDown();

使用方法
  1. 先初始化CountDownLatch实例: CountDownLatch startSignal = new CountDownLatch(n);
  2. 根据业务场景,可以多个await线程,一个countDown线程(应用场景一);也可以一个await线程,多个countDown线程(应用场景二,此时CountDownLatch 相当于一个加强版的join)
  3. 无论什么业务场景,都需要某个(些)线程先await,其它线程再countDown()
  4. 调用await方法的线程会发生阻塞;调用countDown的线程,调完该方法后可以继续执行,不会发生阻塞
  5. 调用await方法的线程需要其它线程通过调用countDown()来解除阻塞,且至少需要调用countDown()>=n次,才能解除await线程的阻塞(内部实现是由最后一次调用countDown时才解除await阻塞)
  6. 有可能由于程序异常,导致 调用countDown()次数<n,进而导致wait线程无法解除阻塞,解决办法:wait线程调用wait(timeout),设置超时阻塞
  7. 注意:CountDownLatch 对象在一次await/countDown使用完成后,不能再继续使用。要想继续使用需要创建新的CountDownLatch 对象。
应用场景
场景一:战士(工作线程)已万事俱备,什么也不干(阻塞),就等司令(调度线程)下令,司令下令后,便马上投入战斗
  • 初始化参数为1的CountDownLatch实例: CountDownLatch startSignal = new CountDownLatch(1);

  • 战士(工作线程)已万事俱备,都通过调用startSignal.await()阻塞,等待老总下令;

  • 司令(调度线程)通过调用startSignal.countDown(1) (可解除所有战士的startSignal.await()阻塞)下令作战开始

  • 战士(工作线程)的startSignal.await()阻塞被解除,于是立即投入战斗

     class Driver { // ...
       void main() throws InterruptedException {
       	 //必须是1把枪
         CountDownLatch startSignal = new CountDownLatch(1);
         CountDownLatch doneSignal = new CountDownLatch(N);
    
         for (int i = 0; i < N; ++i) // create and start threads
           new Thread(new Worker(startSignal, doneSignal)).start();
    
         doSomethingElse();            // don't let run yet,即先等各运动员准备就绪(调用了CountDownLatch .await()阻塞)之后,再鸣枪(执行下一行的startSignal.countDown();)
         startSignal.countDown();      // let all threads proceed
         doSomethingElse();
         doneSignal.await();           // wait for all to finish
       }
     }
    
     class Worker implements Runnable {
       private final CountDownLatch startSignal;
       private final CountDownLatch doneSignal;
       Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
         this.startSignal = startSignal;
         this.doneSignal = doneSignal;
       }
       public void run() {
         try {
           startSignal.await();
           doWork();
           doneSignal.countDown();
         } catch (InterruptedException ex) {} // return;
       }
    
       void doWork() { ... }
     }
    
场景二:领导(调度线程)忙完了其它活,之后什么也不干(阻塞),等所有员工(工作线程)干完活后,再去干别的
  • 初始化参数为n,的CountDownLatch实例: CountDownLatch doneSignal = new CountDownLatch(N); 其中n为员工数量。
  • 领导调用doneSignal.await()阻塞,等待员工干活完成后通知
  • 每个员工干完活后,通过调用CountDownLatch.countDown()来登记自己已干完该项工作,之后便去忙其它的事情(即调用CountDownLatch.countDown()后自己并不被阻塞)
  • 等所有员工干完活后(内部实现是最后一个员工干完活后进行登记时,发现自己是最后一个干完的,于是通知领导所有员工的活都已干完(解除领导的阻塞)),解除领导因调用CountDownLatch.await的阻塞,此时领导方可继续工作。

The second is a completion signal that allows the driver to wait until all workers have completed.

```java
 class Driver2 { // ...
   void main() throws InterruptedException {
     CountDownLatch doneSignal = new CountDownLatch(N);
     Executor e = ...

     for (int i = 0; i < N; ++i) // create and start threads
       e.execute(new WorkerRunnable(doneSignal, i));

     doneSignal.await();           // wait for all to finish
     doSomethingElse();
   }
 }

 class WorkerRunnable implements Runnable {
   private final CountDownLatch doneSignal;
   private final int i;
   WorkerRunnable(CountDownLatch doneSignal, int i) {
     this.doneSignal = doneSignal;
     this.i = i;
   }
   public void run() {
     try {
       doWork(i);
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
   }

   void doWork(int i) { ... }
 }
```
场景三:利用new CountDownLatch(1)实现阻塞/通知机制
  1. 创建一个n为1的CountDownLatch实例 CountDownLatch latch = new CountDownLatch(1)
  2. 线程A调用latch.await()阻塞,等待其它线程的通知
  3. 线程B调用latch.countDown(),通知线程A解除阻塞继续执行。
实现原理

利用AQS的共享锁实现。

  • 初始化时,设置AQS state数为new CountDownLatch(n)中的n,n>=0(实际n应>0,但为了遵循所有AQS的实现类在state>=0时均可以执行获取锁操作的约定,所以这里也要求n>=0。CountDownLatch内部实现n=0时,需要做到虽不会抛出异常,但在调用CountDownLatch.await()和CountDownLatch.countDown()时均不生效(既不需要阻塞,也无需唤醒))
  • 先调用 CountDownLatch.wait(),内部实现是调用AQS.acquireSharedInterruptibly(1),后者通过调用tryAcquireShared(1)(该方法由CountDownLatch.Sync重写)尝试获取共享锁,并由于state>0尝试失败而进入同步队列阻塞,等待被调用CountDownLatch.countDown()的线程唤醒。可以一个线程调用该方法、也可以多个线程调用该方法均阻塞。
  • 调用CountDownLatch.countDown(),每次调用该方法时,AQS state数减1,并且当最后一次调用(此时state=1)countDown()时,唤醒所有(一个或多个)因调用CountDownLatch.wait()而阻塞的线程。
CountDownLatch初始化

final CountDownLatch latch = new CountDownLatch(n);
最终执行AQS.setState(n);设置初始状态数为n,n>=0

实际n应>0,但为了遵循所有AQS的实现类在state>=0时均可以执行获取锁操作的约定,所以这里也要求n>=0。CountDownLatch内部实现n=0时,需要做到虽不会抛出异常,但在调用CountDownLatch.await()和CountDownLatch.countDown()时均不生效(既不需要阻塞,也无需唤醒)

//CountDownLatch构造方法,要求n>=0,否则抛出异常
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
//CountDownLatch.Sync 构造方法
Sync(int count) {
    setState(count);
}
调用CountDownLatch.await() 操作阻塞

**内部实现:**CountDownLatch.await() ->AQS.acquireSharedInterruptibly(1)->CountDownLatch.Sync.tryAcquireShared(1)。

  • 由AQS.acquireSharedInterruptibly(1)根据CountDownLatch.Sync.tryAcquireShared(1)的返回值判定是否阻塞:
    • 正常情况下,n!=0(实际n>=1),CountDownLatch.Sync.tryAcquireShared(1)返回-1,因而进入同- 队列阻塞
    • 当n=0时,CountDownLatch.Sync.tryAcquireShared(1)返回1,所以不会阻塞(不生效)
  • 一个或线程均可以调用await操作,只要调用时state>0,都会被阻塞。当其它线程执行CountDownLatch.countDown() 操作时,会唤醒所有处于阻塞状态的await线程。

	//CountDownLatch.await()
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
  
  	//AQS.acquireSharedInterruptibly(1)
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //正常情况下,n!=0(实际n>0),CountDownLatch.Sync.tryAcquireShared(1)返回-1,因而进入同步队列阻塞
        // 当n=0时,CountDownLatch.Sync.tryAcquireShared(1)返回1,所以不会阻塞(不生效)
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    
    //CountDownLatch.Sync.tryAcquireShared(1)
	protected int tryAcquireShared(int acquires) {
		return (getState() == 0) ? 1 : -1;
	}
  
调用CountDownLatch.countDown() 操作

内部实现:CountDownLatch.countDown() ->AQS.releaseShared(1)->CountDownLatch.Sync.tryReleaseShared()

实现逻辑:

  • 根据CountDownLatch初始化部分的分析,可以看到在初始化CountDownLatch时,将状态数设置为构造参数 n

  • 结合CountDownLatch.countDown() ->AQS.releaseShared(1)->CountDownLatch.Sync.tryReleaseShared()代码,可以看出

    • 每次调用CountDownLatch.countDown() 时,sate数减1,因而需要客户线程调用该方法至少n次。
    • 在最后一次调用CountDownLatch.countDown() 时,才调用AQS.doReleaseShared解除await线程的阻塞锁,前面的调用只做state计数-1,不会触发await线程的解锁操作。
    • state减为0后,客户线程再次调用(属于过度调用)CountDownLatch.countDown() 无效(即什么也不做)
    • 当CountDownLatch初始化参数n=0时,CountDownLatch.countDown()由于调用CountDownLatch.Sync.tryReleaseShared将state数减一,但返回返回false(nextc == 0),此时调用CountDownLatch.countDown(),state计数减少不会对业务造成影响,因为此时调用await也不会阻塞,无需调用CountDownLatch.countDown()唤醒。
    
    //CountDownLatch.countDown() 
    public void countDown() {
        sync.releaseShared(1);
    }
    //AQS.releaseShared
    public final boolean releaseShared(int arg) {
    	//调用tryReleaseShared:
    	//在最后一次调用CountDownLatch.countDown() 时(此时调用tryReleaseShared 会将state数降为0并返回true,其它情况返回false),才会调用doReleaseShared();唤醒await线程。
    	//当CountDownLatch初始化参数n=0时,
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    //CountDownLatch.Sync.tryReleaseShared
    protected boolean tryReleaseShared(int releases) {
        // 通过自旋方式,直到下方compareAndSetState执行成功。
    	for (;;) {
            int c = getState();
            //state减为0后,客户线程再次调用(属于过度调用)CountDownLatch.countDown() 无效(即什么也不做)
            if (c == 0)
                return false;
            //每次调用CountDownLatch.countDown() 时,sate数减1,因而需要客户线程调用该方法至少n次
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
            	//在最后一次调用CountDownLatch.countDown() 完成时,才调用AQS.doReleaseShared解除await线程的阻塞锁,前面的调用只做state计数-1,不会触发await线程的解锁操作。
                return nextc == 0;
        }
    }
    
问题:CountDownLatch为什么用共享锁,而不用排它锁实现

即CountDownLatch为什么不用排它锁(AQS.acquire和AQS.release方法)实现?
实际上,利用排它锁也能实现CountDownLatch的大部分da功能。以下是利用排它锁实现的CountDownLatch版本及测试代码。
但由于排它锁自身无法实现await(timeout)超时能力,而这在CountDownLatch非常重要,所以最终CountDownLatch采用了共享锁实现。

//CountDownLatch的排它锁实现版本(无法实现await(timeout)能力)
package database.java.util.concurrent;

        import java.util.concurrent.TimeUnit;
        import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CountDownLatchByExclusiveLock {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected boolean tryAcquire(int acquires) {
            return (getState() == 0) ? true : false;
        }

        protected boolean tryRelease(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    public CountDownLatchByExclusiveLock(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    public void await() throws InterruptedException {
        sync.acquire(1);
    }

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

    public long getCount() {
        return sync.getCount();
    }

    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}


//测试代码
import database.java.util.concurrent.CountDownLatchByExclusiveLock;

import java.util.concurrent.atomic.AtomicInteger;

public class CountDownLatchTest {

    static class WorkerRunnable implements Runnable {
        private final CountDownLatchByExclusiveLock doneSignal;
        private final int i;
        WorkerRunnable(CountDownLatchByExclusiveLock doneSignal, int i) {
            this.doneSignal = doneSignal;
            this.i = i;
        }
        private static AtomicInteger num =new AtomicInteger(0);
        public void run() {
            try {
                doWork(i);

                int current = num.incrementAndGet();
                System.out.println(current);
                doneSignal.countDown();
//                System.out.println("工作线程"+i+"执行完毕");
            } catch (Exception ex) {
                ex.printStackTrace();
            } // return;
        }

        void doWork(int i) {
//            System.out.println("工作线程"+i+"正在执行");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args)  {
        int n=10;
        CountDownLatchByExclusiveLock doneSignal = new CountDownLatchByExclusiveLock(n);
//        Executor e = Executors.newFixedThreadPool(n);

        //doSomethingElse();
        for (int i = 0; i < n; ++i) // create and start threads
            new Thread(new WorkerRunnable(doneSignal, i)).start();

        try {
            System.out.println("主线程开始阻塞");
            doneSignal.await();           // wait for all to finish
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("主线程阻塞解除,继续执行");

    }


}


CyclicBarrier
  • Cyclic /ˈsaɪklɪk/ :循环的;周期的;循环群;循环式;环形结构

  • barrier /ˈbæriər/ :n.障碍;屏障;阻力;关卡;分界线;隔阂;难以逾越的数量(或水平、数目)

参见:

概念及使用场景

对于CyclicBarrier,假设有一家公司要全体员工进行团建活动,活动内容为翻越三个障碍物,每一个人翻越障碍物所用的时间是不一样的。但是公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个,以此类推。类比地,每一个员工都是一个“其他线程”。当所有人都翻越的所有的障碍物之后,程序才结束。而主线程可能早就结束了,这里我们不用管主线程。
所谓循环屏障,指所有人翻越第一个障碍物之后,该CyclicBarrier对象会被自动重置,所有人可继续利用该对象翻越第二个障碍物,之后第三个。。。,同一个CyclicBarrier对象会被循环反复使用。

public class CyclicBarrierDemo {

    static class TaskThread extends Thread {
        
        CyclicBarrier barrier;
        
        public TaskThread(CyclicBarrier barrier) {
            this.barrier = barrier;
        }
        
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(getName() + " 到达栅栏 A");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 A");
                
                Thread.sleep(2000);
                System.out.println(getName() + " 到达栅栏 B");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 B");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        int threadNum = 5;
        CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {
            
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "全部到达栅栏,集合完毕");
            }
        });
        
        for(int i = 0; i < threadNum; i++) {
            new TaskThread(barrier).start();
        }
    }
    
}

执行结果

Thread-1 到达栅栏 A
Thread-3 到达栅栏 A
Thread-0 到达栅栏 A
Thread-4 到达栅栏 A
Thread-2 到达栅栏 A
Thread-2 全部到达栅栏,集合完毕 //由此可以看出,CyclicBarrier构造方法的第二个Runnable由最后一个到达栅栏的线程调用,且直接调用Runnable.run方法,而不是通过创建新的线程执行Runnable
Thread-2 冲破栅栏 A
Thread-1 冲破栅栏 A
Thread-3 冲破栅栏 A
Thread-4 冲破栅栏 A
Thread-0 冲破栅栏 A
Thread-4 到达栅栏 B
Thread-0 到达栅栏 B
Thread-3 到达栅栏 B
Thread-2 到达栅栏 B
Thread-1 到达栅栏 B
**Thread-1 全部到达栅栏,集合完毕 **
Thread-1 冲破栅栏 B
Thread-0 冲破栅栏 B
Thread-4 冲破栅栏 B
Thread-2 冲破栅栏 B
Thread-3 冲破栅栏 B

核心API
  • 构造方法:
    • public CyclicBarrier(int parties, Runnable barrierAction)
    • public CyclicBarrier(int parties)
  • 核心方法: public int await(),前n-1个线程调用该方法后会被阻塞,直到最后1个线程调用该方法后执行完毕(若定义了 barrierAction,最后一个线程还需要继续执行barrierAction)后,所有线程才能继续执行下一步逻辑。
使用方法
应用场景
实现原理
CyclicBarrier与CountDownLatch的区别

在这里插入图片描述

Semaphore

Semaphore /ˈseməfɔːr/:信号量;信号;信号灯

参见:

概念和作用

在这里插入图片描述

Semaphore 主要用于流量控制。
可以将Semaphore 理解成一个停车场,里面包含permits个车位,最多允许permits辆车(每一辆车是一个线程)停车。

  • 每一辆车进入停车场前,需要向管理员申请车位
  • 当还有空闲车位时,申请成功,空车位数减1
  • 没有空车位时,后续车辆无法申请车位,需要等待,直到有的车辆归还车位为止。
    在这里插入图片描述
    输出结果:
    在这里插入图片描述

举例二:

public static void main(String[] args) {
    // 表示有2个许可.
    Semaphore sem = new Semaphore(2);
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            try {
                // 默认使用一个许可.
                sem.acquire();
                System.out.println(Thread.currentThread() + " I get it.");
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread() + " I release it.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                sem.release();
            }
        }).start();
    }
}

输出结果:

Thread[Thread-0,5,main] I get it.
Thread[Thread-1,5,main] I get it.
Thread[Thread-1,5,main] I release it.
Thread[Thread-0,5,main] I release it.
Thread[Thread-2,5,main] I get it.
Thread[Thread-2,5,main] I release it.

核心API
  • 构造方法:定义一个信号量,确定许可个数

    • Semaphore sem = new Semaphore(2); // 表示有2个许可.
    • public Semaphore(int permits, boolean fair)
  • 其它方法

    • public void acquire() throws InterruptedException; // 默认申请一个许可
    • public void acquire(int permits) throws InterruptedException
    • public void acquireUninterruptibly()
    • public void acquireUninterruptibly(int permits)
    • public boolean tryAcquire()
    • public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException
    • public boolean tryAcquire(int permits)
    • public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException
    • public void release()
    • public void release(int permits)
    • public int availablePermits()
    • public int drainPermits()
    • public final boolean hasQueuedThreads()
    • public final int getQueueLength()
实现原理
  • 与ReentrantLock一样,Semaphore 内部基于AQS实现,并提供了公平锁和非公平锁两种机制。
  • 但Semaphore是共享锁。ReentrantLock是排它锁。
  • new Semaphore(permits)时,初始state数=permits数。
  • 当调用acquire方法时,state做减法操作,直到state<=0时,再次调用acquire方法会导致线程阻塞。
  • 当调用release方法时,state做加法操作,直到达到permits数为止,
注意事项
  1. 锁由调用acquire获取到许可的线程来释放,而不是车场(管理员)
  2. 线程可以一次申请和释放多个许可。
  3. 如果许可数量为1,则可以当做普通的互斥锁来使用。
Exchanger
概念和作用

在这里插入图片描述

参见:

demo:
在这里插入图片描述

使用场景
  1. 在两个线程之间交互数据
  2. 实现生产者-消费者模式:当一个线程只存数据,另外一个线程只取数据时,就变成了生产者-消费者模式
核心API
实现原理

Exchanger类底层并不是太复杂,关键的技术有:

(1)使用CAS自旋指令完成数据交换

(2)使用LockSupport的park方法使交换线程进入休眠等待, 使用LockSupport的unpark方法唤醒等待线程。

(3)此外还声明了一个Node对象用于存储交换数据。

关于使用锁方面的考量

尽量避免使用锁

读写分离

CopyOnWrite技术

如无法避免使用锁,应尽量减少锁竞争

分段锁

根据不同读写场景选择合适的锁

优先考虑使用轻量级锁

自旋锁

最后考虑使用重量级锁

根据单机场景还是分布式场景,选择单机锁还是分布式锁

ReadWriteLock

其它

ReadWriteLock 接口

UML图

在这里插入图片描述

重要的实现类

Java内部锁对象

参见:java 并发——内置锁

monitor

双检锁及问题

单例模式实现方式

其它

transient关键字

参见:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值