*在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
*只有形如x = 10这种不可再分的赋值操作才有原子性;而形如x++、y = x这种赋值操作没有原子性,例子见下
int count =0;//1count++;//2int a = count;//3
*上面展示语句中,除了语句1是原子操作,其它两个语句都不是原子性操作
*其实语句2在执行的时候,包含三个指令操作
·指令 1:首先,需要把变量 count 从内存加载到 CPU的寄存器
·指令 2:之后,在寄存器中执行 +1 操作;
·指令 3:最后,将结果写入内存
可见性*Java提供了volatile关键字来保证可见性。(详见volatile关键字)
*当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
*普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
*Lock和synchronized也可以通过互斥访问保证可见性
有序性 简介*现代CPU运算速度远大于内存访存速度,CPU自身不同级别cache访问速度也有差异,为了减少等待时间,CPU会采用指令级并行重排序来提供执行效率,也可以叫做CPU乱序执行。
*同理,JVM有时也会进行指令重排序,即对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
· 进行重排序时是会考虑指令之间的数据依赖性,所以不会出现初始化被重排在变量使用之前等离谱情况
·指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
*synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
happens-before原则①程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
②锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
③volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
④传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
⑤线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
⑥线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
⑦线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等检测到线程已经终止执行
⑧对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
Java的中断和阻塞*首先注意区分阻塞和在等待队列中的就绪态,阻塞中的线程是无法竞争访问资源的
*Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。
*Java中的中断作用于线程对象,不会立刻阻塞对象,而是根据Thread对象当前的活动状态来产生不同的效果
·中断阻塞(等待)状态的线程将会抛出中断异常
·只是通过wait()、sleep()等方法被阻塞的线程用interrupted()判断时会抛出异常,而用更底层的LockSupport.park()方法阻塞的线程即使用interrupted()方法判断也不会抛出异常
·抛出InterruptedException时中断状态也会被清掉
·中断运行时的线程会改变线程对象中的一个中断状态值,不会影响该线程继续执行
*中断相关方法
*通常实现接口,因为Java不支持多继承
*要重写/实现run方法
*调用start方法开启线程的执行
*初始化:用实例作为参数初始化Thread类新实例(推荐,因为可以使用Thread类的方法)
Java线程模型 简介*JVM线程与OS线程间存在某种映射关系(JVM线程的创建与回收会调用OS相关接口)
*JVM线程对OS线程进行了抽象,使得开发更容易
*Linux中没有为线程设计专门的数据结构,其实现为轻量级进程(没有独立的地址空间)
*分为一对一、多对一、多对多
一对一*用户线程和内核线程一对一
*由于用户态线程的阻塞和唤醒会映射到内核态线程,会有频繁的用户态和内核态转换
·一些语言引入CAS操作(比如Java的AQS)减少CPU状态的切换
*OS内核支持线程数量有限,一对一限制了并发量
*JVM的常用模型
多对一*多个用户线程映射到一个内核线程上
*用户线程调度由用户态的应用完成
*有效提升并发上限
*一个线程阻塞,映射到同一内核线程的线程也阻塞
多对多*实现难度很高
*go语言GMP线程模型
JVM线程状态*NEW(初始化状态) :线程通过new初始化完成到调用start方法前都处于等待状态。
*RUNNABLE(可执行状态):线程执行start方法后就处于可以行状态。
·这也就是为什么线程的执行要调用start()方法而不是直接调用run()
*BLOCKED(阻塞状态):notify方法被调用后线程被唤醒,但是这时notify的synchronized代码段并没有执行完,同步锁没有被释放,所以线程处于BLOCKED状态。直到notify的synchronized代码段执行完毕锁被释放,线程才回到wait所在的synchronized代码段继续执行。
*WAITING(等待状态) :调用sleep或是wait方法后线程处于WAITING状态,等待被唤醒。
*TIMED_WAITING(等待超时状态) :调用有参的sleep或是wait方法后线程处于TIMED_WAITING状态,等待被唤醒或时间超时自动唤醒。
*TERMINATED(终止状态) :在run方法结束后线程处于结束状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suzRRRO3-1644036846816)(imgimage12344.jpeg)]
Java锁机制基础 什么是锁&JVM运行时的内存结构[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wpov4CUI-1644036846816)(imgimage-20220118172035158.png)]
*多个线程对共享资源进行争抢访问,可能造成数据不一致è锁机制进行解决
*JVM内存结构中,红色区域是各个线程独立的,蓝色区域为各个线程共享的
·Java堆中存放着所有对象
·方法区中存放着类信息、常量、静态变量等数据
* Java中每个对象都拥有一把锁,存放在对象头中
JVM对线程的内存分配过程*JVM有一个内存区域是JVM虚拟机栈,每一个线程运行时都有一个线程栈
*线程栈保存了线程运行时候变量值信息。
·当线程访问某一个对象时候值的时候,首先通过对象的引用找到在堆内存的变量的值 ·然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本
·之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值
*在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYkqfAcV-1644036846817)(imgimage-20220118172357892.png)]
volatile关键字*使用volatile关键字会强制将修改的值立即写入主存;
*使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU缓存中对应的缓存行无效);
·由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
*但是可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
Java的对象和对象头* Java对象包括三部分:对象头、实例数据和字节对齐填充部分
·Java要求8比特对齐,通常无实际意义
·实例数据包括初始化对象时设定的属性和状态等内容
·对象头存放了对象本身的运行时信息
*对象头
·对象头包含了Mark Word和Class Pointer(数组对象还有长度)
·Class Pointer指向当前类型在方法区的数据
·Mark Word记录者对象的很多运行时状态信息,详见下图(32位JVM Mark Word)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-io7yfVVB-1644036846817)(imgimage-20220118221505029.png)]
·不同的锁标志位对应着Mark Word的不同结构,也代表着不同种类的锁
Java中锁的4种状态 无锁*对象不会出现在多线程环境下
*不会产生针对该对象的竞争访问
*存在对该对象的竞争访问,但是通过非锁方式对其进行保护(如CAS)
偏向锁*对象被加锁,但是只有一个线程会访问对象(aka预期时间内不会竞争访问)
*对象“识别”线程,该线程访问时直接交出锁,不通过OS的mutex或者JVM的CAS机制
·偏向锁的Mark Word的前23比特就是该锁偏向的线程的ID
*假如由别的线程试图访问被偏向锁保护的对象时,偏向锁会升级为轻量级锁
轻量级锁*获取方式为虚拟机栈中开辟一块被称为Lock Record的空间(虚拟机栈不会竞争访问),存放锁住的对象的Mark Word的副本和Owner指针
*用CAS方式竞争获取锁,一旦获得,复制Mark Word后到锁记录空间后再将Owner指针指向该对象
*同时对象的Mark Word的前30个比特也编程指向锁记录的指针,实现了线程和对象的锁的绑定
*其他想要获得该对象的线程自旋等待(忙等),不需要进入内核态阻塞内核级线程
*但是长时间忙等也会造成资源浪费,于是优化为适应性自旋
·适应性自旋:自旋时间不固定,由上一次在同一个锁上的自旋时间以及锁的状态决定
*一旦自旋等待的进程超过一个,轻量级锁将被升级为重量级锁
重量级锁*通过Monitor机制锁住资源
*对线程的管控最为严格
Java锁的一些性质 公平vs非公平*广义地说,在有限的等待时间内,能保证所有线程都拿到锁,那这个锁就是公平的
*公平锁的狭义理解是:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
*不公平的锁就是像抛绣球那样把锁扔出去让线程们各凭本事去抢,可能造成饥饿
可重入*可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
*synchronized 和 ReentrantLock 都是可重入锁。
*可重入锁的作用之一在于防止死锁。
*实现原理:
·每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1。
·如果同一个线程再次请求这个锁,计数器将递增;
·每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
*关于父类和子类的锁的重入:子类覆写了父类的synchronized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。
乐观vs悲观*乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用
*对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
*Java的乐观锁指的就是CAS机制
应用场景*悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
*乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
JVM Monitor“管程”[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AYm9g4ds-1644036846817)(imgimage-20220118221521962.png)]
*monitor用OS的mutex lock实现线程同步
*性能低
*由于每次取得、释放锁都要进行OS用户态和内核态的转移,所以操作是“重量级”的
synchronized关键字 简介*悲观锁
*由于本质是调用OS的底层mutex原语,所以是非公平的
*底层实现为编译后生成monitorenter和monitorexit两个字节码指令进行线程同步(见[Java管程](#JVM Monitor“管程”))
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oNc3uffw-1644036846818)(imgimage-20220118221549824.png)]
*实际应用不多,需要记住无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问,所以最好不要把变量用synchronized修饰
synchronized等待唤醒机制*主要包括notify()、notifyAll()和wait()方法
*使用上术三个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常。
*sleep与wait方法的比较
·wait方法调用完成后,线程不仅会被阻塞,还会释放当前持有的监视器锁(monitor),直到有线程调用notify()/ notifyAll()方法后才能继续执行。
·sleep方法只让线程休眠并不释放锁
*notify()/ notifyAll()方法只是解除了等待线程的阻塞,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
Lock接口 官方注释说明*Lock接口的意义在于提供了区别于synchronized的另一种具有更广泛操作的同步方式,它能支持更多灵活的结构,可以关联多个Condition对象(Doug Lea)
·如果一个线程获取了多个不同的synchronized锁,那它们必须以相反的顺序被释放,并且所有的锁都必须在与它们相同的代码块内被释放。
*Lock允许线程在不同的代码块中获取、释放锁,并且支持以任意的顺序释放锁
*大部分情况下应当使用以下语法来进行锁的获取与释放
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
·当在不同块内发生锁定和解锁时,必须注意确保在获取锁后执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁。
*请注意, Lock实例只是普通对象,它们本身可以用作synchronized语句中的目标。
·获取Lock实例的监视器锁与调用该实例的任何lock()方法没有特定关系。 建议为避免混淆,不以这种方式使用Lock实例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MDc4n5Jm-1644036846818)(img202184103338457.png)]
API 语义:非阻塞尝试来获取锁( tryLock()),尝试获取锁,且可以在获取过程中响应外部中断( lockInterruptibly()),以及尝试获取锁,但是可以超时( tryLock(long, TimeUnit) )。
boolean tryLock()*尝试获取锁,成功则立即返回true,失败则立即返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException*如果锁在给定的等待时间内可用,并且当前线程未被中断,则获取锁。
*如果锁可用,则此方法将立即返回值 true。
*如果锁不可用,将禁用当前线程,并进行线程调度,在发生以下两种情况之一前,该线程将一直处于休眠状态:
·锁由当前线程获得;
·其他某个线程中断当前线程,并且方法的实现支持在获取锁的过程中中断(会发生什么见下)
*线程在以下情况下情况下会抛出InterruptedException,并清除当前线程的已中断状态
·在进入此方法时已经设置了该线程的中断状态
·在获取锁时被中断,并且方法的实现支持在获取锁的过程中中断
*一些情况下可能无法中断锁获取,即使可能,该操作的开销也很大。程序员应该知道可能会发生这种情况,虽然会对整体性能造成影响,但是最好按照方法的语义对这些情况做出响应,也应该对此进行记录。
·但是实际应用中还是能用就行,毕竟自己的屎山很少用自己再刨开了,而且留有改进空间老板也好留活不是吗
*Lock 实现可能可以检测锁的错误用法,例如,某个调用可能导致死锁,在特定的环境中可能抛出(未经检查的)异常。该 Lock 实现必须对环境和异常类型进行记录。
·死锁检测算法:银行家算法
·当然了,在可预期的未来,不会真的有注重并发性能的锁实现去做死锁检测,因为开销太大
*参数:
·time - 等待锁的最长时间
·unit - time参数的时间单位
void lock()*获得锁,如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。
*原则上线程在等待、获取锁的过程中无法对外部中断做出响应,且不保证操作的原子性
·所以设计时应保证lock操作本身是线程安全的
·也应当在实现中保证线程在获取锁后响应外部的中断(否则可能会出现运行时异常)
*Lock实现可能能够检测锁的错误使用,例如将导致死锁的调用,并且可能在这种情况下抛出(未检查)异常。 情况和异常类型必须由Lock实现记录。
void lockInterruptibly() throws InterruptedException*获取锁,除非当前线程被中断
*如果锁被其他线程占有,当前线程将被禁用以进行线程调度,并且处于休眠状态,直到发生下面两件事情之一:
·锁被当前线程获取
·当前线程被其他线程interrupt,并且方法的实现支持中断锁的获取(发生什么见下)
*线程在以下情况下情况下会抛出InterruptedException,并清除当前线程的中断状态
·在进入此方法时该线程的中断状态为真
·在获取锁时被中断,并且方法的实现支持在获取锁的过程中中断
***(其实就是tryLock()方法的无限时长版本)
void unlock()*释放锁
*需要注意检查释放锁的线程必须是得到锁的线程,并在错误地释放操作被调用时作出限制(比如抛出异常)。任何限制和异常类型必须由Lock实现记录。
Condition newCondition()*返回一个新Condition绑定到该实例Lock实例。
*开始条件等待之前,锁必须由当前线程保持。 调用Condition.await()将在等待之前将原子地释放锁,并在等待返回(结束)之前重新获取锁。
*关于Condition接口详细说明见下
Condition接口 介绍*在Java 1.5中引入,用来替代传统Object的Monitor机制实现线程间的协作
*一个Condition实例可以看作是一个条件等待的线程组成的队列,但是其与等待的条件不绑定,即等待条件的判断需要在使用Condition实例的类的方法中实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N74dNEP3-1644036846818)(img1731440-20190926221041940-1292273259.png)]
*条件等待(可中断,不可中断和定时)的三种形式在一些平台上的易用性和性能特征可能不同。 特别地,可能难以提供一些特征并保持特定的语义,例如排序保证。此外,中断线程实际挂起的能力可能并不总是在所有平台上实现。
API void await() throws InterruptedException*当调用await()方法后,当前线程会原子地释放锁并进入Condition实例的等待队列等待,直到发生以下四种情况之一
①其他线程调用了该Condition实例的signal()方法且当前线程被选中唤醒
②其他线程调用了该Condition实例的signalall()方法
③其他线程中断(interrupt)该线程,使得等待状态被中断
④发生“虚假唤醒”【需要进一步解释】
·虚假唤醒是对底层平台语义的让步。 这对大多数应用程序几乎没有实际的影响,因为Condition应该始终在循环中等待,测试正在等待的状态谓词。
*线程被唤醒后需要重新尝试获得与该Condition实例相关联的锁,而且在await()方法的调用返回时,方法必须保证线程持有锁
*发生以下两种情况之一,方法抛出InterruptedException并清除当前线程的中断状态
①进入该方法时,线程的中断状态就是true
②线程在等待过程中被中断且并且方法的实现支持在等待条件的过程中中断
注意:在第一种情况下,没有规定检查中断是不是发生在锁释放之前
void awaitUninterruptibly()*造成当前线程在接到信号之前一直处于等待状态。(与上一个方法不同之处在于该方法对中断不敏感)
*方法被调用后,与该Condition关联的锁被原子地释放,调用该方法的线程也进入该Condition的等待队列进行等待,直到下列几件事发生
①其他线程调用该Condition的signial()方法,且这个线程抢占到了锁
②其他线程调用该Condition的signakAll()方法,如果该线程没获得锁将回到等待状态
③发生“虚假唤醒”
·其实signakAll()方法能实现的先决条件是自旋地尝试得到锁,这样子没得到锁的线程才能重新回到等待状态
long awaitNanos(long nanosTimeout) throws InterruptedException*造成当前线程在接到信号或者被中断之前一直处于等待状态,直到等待时间过去,方法退出。
*与该Condition相关的锁被原子地释放,并且该线程进入等待状态,直到以下几种情况之一发生
①其他线程调用该Condition的signial()方法,且这个线程抢占到了锁
②其他线程调用该Condition的signakAll()方法,如果该线程没获得锁将回到等待状态
③其他线程将该线程中断,并且方法的实现支持对等待过程中的中断做出响应
④指定的等待时间过去
⑤发生“虚假唤醒”
*线程被唤醒后需要重新尝试获得与该Condition实例相关联的锁,而且在awaitnanos()方法的调用返回时,方法必须保证线程持有锁
*发生以下两种情况之一,方法抛出InterruptedException并清除当前线程的中断状态
①进入该方法时,线程的中断状态就是true
②线程在等待过程中被中断且并且方法的实现支持在等待条件的过程中中断
注意:在第一种情况下,没有规定检查中断是不是发生在锁释放之前
*返回值为等待时间较提供的nanosTimeout值所剩余的纳秒数的估计;如果超时,则返回小于等于0的值。这个值可以用来当作等待方法返回但是等待条件没有满足时再次等待的时间的参考,例子见下
boolean aMethod(long timeout, TimeUnit unit) { long nanos = unit.toNanos(timeout); lock.lock(); try { while (!conditionBeingWaitedFor()) { if (nanos <= 0L) return false; nanos = theCondition.awaitNanos(nanos); } // ... } finally { lock.unlock(); } }
·设计笔记(Lea神的官方注释):本方法的参数一定要是纳秒,以避免返回剩余时间时发生截断错误(truncation errors)。一旦发生截断错误,程序员将无法确保重新等待时等待的总时间不长于预期的参数时间
*调用此方法时,假定当前线程持有与此Condition关联的锁。方法的实现需要验证该假定是否成立,如果不成立,通常会引发异常(例如IllegalMonitorStateException),实现必须记录该情况。
*方法的实现可以优先响应中断而非正常的方法返回或等待超时,在任何情况下,实现必须确保信号被重定向到另一个等待线程(如果存在)
boolean await(long time, TimeUnit unit) throws InterruptedException*使当前线程等待直到收到信号或被中断,或指定的等待时间过去。 这种方法在行为上等同于:
awaitNanos(unit.tonanos(time)) > 0
*参数
time - 等待的最长时间
unit - time参数的时间单位
*返回值:如果在线程得到锁之前时间耗尽,返回false,否则返回true
boolean awaitUntil(Date deadline) throws InterruptedException*使当前线程等待直到收到信号或被中断,或指定的最后期限过去。
*与该Condition相关的锁被原子地释放,并且该线程进入等待状态,直到以下几种情况之一发生
①其他线程调用该Condition的signial()方法,且这个线程抢占到了锁
②其他线程调用该Condition的signakAll()方法,如果该线程没获得锁将回到等待状态
③其他线程将该线程中断,并且方法的实现支持对等待过程中的中断做出响应
④指定的等待期限过去
⑤发生“虚假唤醒”
*发生以下两种情况之一,方法抛出InterruptedException并清除当前线程的中断状态
①进入该方法时,线程的中断状态就是true
②线程在等待过程中被中断且并且方法的实现支持在等待条件的过程中中断
注意:在第一种情况下,没有规定检查中断是不是发生在锁释放之前
*返回值表示是否在等待期限内得到锁并返回,可以进行如下应用
boolean aMethod(Date deadline) { boolean stillWaiting = true; lock.lock(); try { while (!conditionBeingWaitedFor()) { if (!stillWaiting) return false; stillWaiting = theCondition.awaitUntil(deadline); } // ... } finally { lock.unlock(); } }
*调用此方法时,假定当前线程持有与此Condition关联的锁。方法的实现需要验证该假定是否成立,如果不成立,通常会引发异常(例如IllegalMonitorStateException),实现必须记录该情况。
*方法的实现可以优先响应中断而非正常的方法返回或等待超时,在任何情况下,实现必须确保信号被重定向到另一个等待线程(如果存在)
void signal()*唤醒一个等待线程
*挑选一个自身等待队列的线程进行唤醒。在线程从await()方法返回之前,必须要得到锁
*当调用此方法时,实现通常被要求当前线程持有与该Condition关联的锁。实现必须记录这一前提条件以及在未持有锁的情况下采取的任何行动。在未持有锁的情况下行动通常会引抛出异常,例如IllegalMonitorStateException。
void signalAll()*唤醒该Condition等待队列的所有等待线程
*线程必须重新获取锁,才能从await()方法返回。
*当调用此方法时,实现通常被要求当前线程持有与该Condition关联的锁。实现必须记录这一前提条件以及在未持有锁的情况下采取的任何行动。在未持有锁的情况下行动通常会引抛出异常,例如IllegalMonitorStateException。
使用Condition的例子*实现的是生产者消费者问题的Buffer和put()与take()方法
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } }
无锁同步机制—CAS*Compare and Swap,实际上是无锁
*目标是实现修改一个对象的操作的同步
*每个对象存储一个Old Value代表之前读到的对象的状态值
*当想更新时,New Value代表期待把对象更新为的值
*线程获取到时间片后,用Old Value和读取到对象的真正当前值比较(Compare),如果一致,则将对象更新为新值,并将新值更改为Old Value(Swap)
·理论上,当Compare发现真实值与Old Value不一致时只需放弃操作且更新Old Value即可
·应用中在不一致时会让线程自旋一定时间,看看对象的值能否被改回—CAS自旋
*实际上CAS机制不包括任何线程同步操作,所以还存在着线程不安全的情况
·比如多条线程同时占用资源对象
*解决方法是将CAS聚合为一个原子操作
·大部分CPU都提供指令级别的CAS操作
*Java的CAS底层实现用到了Unsafe类,不是重点
JUC开发包:AQS机制*Java.util.concurrent开发包的abstract queued synchronizor,抽象式队列同步器
*很重要,建议配合@寒食君的视频食用
AQS设计思路—如何设计一个同步管理框架1、通用性:下层实现透明的同步机制,同时与上层业务解耦
2、利用CAS,原子地修改共享标记位
3、等待队列:尽量避免轮询操作,因为忙等会造成CPU资源的浪费,而且轮询的实现要求上层编写的支持,与透明的需求相悖
AQS成员属性1、private volatile int state
*用于判断共享资源是否正在被占用的标记位
*volatile保证了线程之间的可见性
*使用int:由互斥访问拓展到资源的共享访问(比如读者写者问题中的读锁)
2、private transient volatile Node head, tail
*等待队列的头节点/尾节点
*FIFO的双向链表
3、Node的具体结构
*Thread类对象thread:等待的线程本体
*节点的等待状态:有4个状态,具体见后节点状态部分
*前、后指针
AQS节点状态①CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
②SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
③ConDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
④PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
⑤0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。*
AQS框架*它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义见上。state的访问方式有三种:
getState()
setState()
compareAndSetState()
*AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
*不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
*自定义同步器在实现时**按照需求实现(不提供)**独占资源模式和/或共享资源模式的基础获取与释放方法即可:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
独占模式源码分析 1、tryAcquire(int)protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();}
*参数代表对state的修改,返回值代表是否成功修改state(成功得到锁)
*实际上并没有实现,需要程序员按照需求override,否则抛出不支持操作的异常
*AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。
·这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
2、acquire(int)public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE),arg)) {selfInterrupt();}}
*此方法是独占模式下线程获取共享资源的顶层入口,参数表示该线程期望的访问模式。否则进入等待队列,直到获取到资源为止,且整个过程不受中断的影响。
*被关键字public和final修饰,说明所有实现都调用这个方法,而且不允许继承类覆写
*!tryAcquire(arg)说明一旦成功获得锁,直接跳出,不用执行后续判断条件
*流程为:
①tryAcquire()首先尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
②addWaiter()将该线程加入等待队列的尾部,并标记为独占模式
③acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
④**如果线程在等待过程中被中断过,它是不响应的。**只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。(解释见下)
3、addWaiter(Node)private Node addWaiter(Node mode) { //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享) Node node = new Node(Thread.currentThread(), mode); //尝试快速方式直接放到队尾。 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //上一步失败则通过enq入队。 enq(node); return node;}
*此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。
*流程为:
·首先用当前等待的线程初始化一个新Node对象
·其次获取尾节点的指针,将其作为当前节点的前置节点
·如果尾节点不为空,则尝试快速入队,通过CAS操作将尾节点更新为当前节点,并将之前的尾节点(当前节点的前置节点)的next指针指向当前节点
·当尾节点为空或者快速入队CAS操作失匹配,则调用enq方法进行常规入队
*CAS判断的if代码块内部的线程安全问题
·CAS操作只能保证本身的原子性,不能保证if代码块内的原子性
·但是在本代码块中,由于只是实现了新节点插入其读取到的尾节点之后,而且如果进入,则说明CAS操作已经保证了尾节点已经被更新为新节点,所以不会出现线程安全问题。
*尝试快速入队可能是因为想节省一次判空的时间
*其中enq(Node node)方法用于将node加入队尾,分析见下
4、enq(Node node)private Node enq(final Node node) { //CAS"自旋",直到成功加入队尾 for (;;) { Node t = tail; if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。 if (compareAndSetHead(new Node())) tail = head; } else {//正常流程,放入队尾 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }}
*代码执行的模式为:
·如果队列为空,CAS初始化队列头并将头赋值给队尾并自旋到队列不为空的情况
·如果队列不为空,自旋地通过CAS将当前节点插入,直到入队成功为止
*这段代码的精华是CAS自旋volatile变量,是一种很经典的用法,说明见上CAS部分
5、acquireQueued(Node, int)final boolean acquireQueued(final Node node, int arg) { boolean failed = true;//标记是否成功拿到资源 try { boolean interrupted = false;//标记等待过程中是否被中断过 //又是一个“自旋”! for (;;) { final Node p = node.predecessor();//拿到前驱 //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。 if (p == head && tryAcquire(arg)) { setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了! failed = false; // 成功获取资源 return interrupted;//返回等待过程中是否被中断过 } //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true } } finally { if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。 cancelAcquire(node); }}
*通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。
*acquireQueued()实现的就是线程下一步的需求:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。
*代码运行模式分析
·首先该方法内部定义了一个局部变量failed,初始值为true,正常执行时,failed会在返回前被更新为false,证明方法正常执行完毕,只有方法内部出现异常导致无法正常执行时,才会进入finally块中的if块,调用cancelAcquire(Node)方法,将异常node的status置为1并进行清理
·方法try代码块中定义了一个interrupted变量,初始化为false,只有在当前线程需要被阻塞且成功被阻塞时才会被更新成true,否则会返回其的false值(之后细讲)
·在循环体中,如果当前节点的前置节点是头节点且当前线程尝试获取锁成功,直接返回
·在AQS的FIFO队列中,头节点是虚节点,储存已经获得资源的线程,不参与竞争锁
·其调用的shouldParkAfterAcquire通过前置节点的waitStatus(ws)返回当前节点是否需要阻塞,说明见下
6、shouldParkAfterFailedAcquire(Node, Node)private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus;//拿到前驱的状态 if (ws == Node.SIGNAL) //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了 return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢! compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}
*此方法主要用于检查状态,看看自己是否真的可以去休息了(进入waiting状态)
*代码说明:
·当ws为Signal(-1)时,说明前置节点也在等待拿锁,由于FIFO,当前节点可以阻塞休息
·当ws大于0,状态只可能是CANCEL,将该节点以及其前置所有CANCLE节点从队列中删除并返回false进行下一轮的判断(通过acquireQueued()方法的自旋)
·当ws为小于0的其他情况时,前置节点就应当准备唤醒该节点当前节点,所以用CAS操作将前置节点的等待状态改为SIGNAL,返回false进行下一轮的判断Java中锁的分类
7、parkAndCheckInterrupt()private final boolean parkAndCheckInterrupt() {LockSupport.park(this);//调用park()使线程进入waiting状态return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。}
*如果线程找好安全休息点后,那就可以安心去休息了。此方法就是阻塞线程,让线程真正进入等待状态。
*阻塞线程需要调用OS的相关原语
*interrupted()方法返回其中断标志位并将该标志位复位为false
8、acquire方法总结public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE),arg)) {selfInterrupt();}}
*头节点后一个节点持续CAS自旋尝试获取锁(状态为SIGNAL)
*其他节点都被挂起
*除了头节点及其后继以及尾节点外其他节点的状态全部置为SIGNAL
·acquireQueued()方法通过自旋保存了传入的状态参数,并在获取共享资源时作为模式参数传入
9、release(int)public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head;//找到头结点 if (h != null && h.waitStatus != 0) unparkSuccessor(h);//唤醒等待队列里的下一个线程 return true; } return false;}
*此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
*它调用tryRelease()来释放资源,需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!
10、tryRelease(int)protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();}
*跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的
*正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。
***由于release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!**所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false
11、unparkSuccessor(Node)private void unparkSuccessor(Node node) { //这里,node一般为当前线程所在的结点。 int ws = node.waitStatus; if (ws < 0)//置零当前线程所在的结点状态,允许失败。 compareAndSetWaitStatus(node, ws, 0); Node s = node.next;//找到下一个需要唤醒的结点s if (s == null || s.waitStatus > 0) {//如果为空或已取消 s = null; for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。 if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。 s = t; } if (s != null) LockSupport.unpark(s.thread);//唤醒}
*首先,由于释放资源代表该线程已经运行完毕,所以需要先把运行状态CAS地更改为默认的0,避免干扰其他函数的判断
*用unpark()唤醒等待队列中最前边的那个未放弃线程(ws<=0的节点)s
* s被唤醒后,进入acquireQueued()的if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head后继结点,表示自己已经获取到资源了,acquire()也返回了!!
12、release()小结*release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
*如果获取锁的线程在release时异常了,没有unpark队列中的其他结点,这时队列中的其他结点会怎么办?是不是没法再被唤醒了?
·答案是YES!这时,队列中等待锁的线程将永远处于park状态,无法再被唤醒!!!但是我们再回头想想,获取锁的线程在什么情形下会release抛出异常呢?
①线程突然死掉了?可以通过thread.stop来停止线程的执行,但该函数的执行条件要严苛的多,而且函数注明是非线程安全的,已经标明Deprecated;
②线程被interupt了?线程在运行态是不响应中断的,所以也不会抛出异常;
③release代码有bug,抛出异常了?目前来看,Doug Lea的release方法还是比较健壮的,没有看出能引发异常的情形(如果有,恐怕早被用户吐槽了)。除非自己写的tryRelease()有bug,那就没啥说的,自己写的bug只能自己含着泪去承受了。
独占模式小结*头节点后一个节点持续CAS自旋尝试获取锁(状态为SIGNAL)
*其他节点都被挂起
*除了头节点及其后继以及尾节点外其他节点的状态全部置为SIGNAL
·acquireQueued()方法通过自旋保存了传入的状态参数,并在获取共享资源时作为模式参数传入
共享模式源码分析 1、acquireShared(int)public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg); }
*此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。
*通过调用的tryAcquireShared()方法的返回值确定共享资源是否还有剩余
*tryAcquireShared()依然需要自定义同步器去实现,但是其返回值语义已经被定义好
·负值代表获取失败;
·0代表获取成功,但没有剩余资源;
·正数表示获取成功,还有剩余资源,其他线程还可以去获取。
*该方法的流程为
·tryAcquireShared()尝试获取资源,成功则直接返回;
·失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。
2、doAcquireShared(int)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) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的 int r = tryAcquireShared(arg);//尝试获取资源 if (r >= 0) {//成功 setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程 p.next = null; // help GC if (interrupted)//如果等待过程中被打断过,此时将中断补上。 selfInterrupt(); failed = false; return; } } //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt() if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}
*与acquireQueued()流程并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外
*除了head.next可以获取资源外,如果还有剩余资源,还会通过setHeadAndPropagate()唤醒下一个节点,但是如果剩余资源不够下一个的话不会试图唤醒其余节点
·保证公平,降低并发
3、setHeadAndPropagate(Node, int)private void setHeadAndPropagate(Node node, int propagate) { Node h = head; setHead(node);//head指向自己 //如果还有剩余量,继续唤醒下一个邻居线程 if (propagate > 0 || h == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); }}
*propagate
*此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点
4、acquireShared()小结*它的流程为:
1、tryAcquireShared()尝试获取资源,成功则直接返回;
2、失败则通过doAcquireShared()进入等待队列park()开始等待,直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
*其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作
5、releaseShared()public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) {//尝试释放资源 doReleaseShared();//唤醒后继结点 return true; } return false;}
*此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放,它会通过doReleaseShared()方法唤醒等待队列里的其他线程来尝试获取资源。
*有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。
6、doReleaseShared()private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; unparkSuccessor(h);//唤醒后继 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } if (h == head)// head发生变化时继续循环 break; }}
*此方法逻辑是一个死循环,每次循环中重新读取一次head,然后保存在局部变量h中,再配合if(h == head) break;,这样,循环检测到head没有变化时就会退出循环。注意,head变化一定是因为:acquire thread被唤醒,之后它成功获取锁,然后setHead()设置了新head(见前[setHeadAndPropagate(Node, int)](#3、setHeadAndPropagate(Node, int)))。
·注意,只有通过if(h == head) break;即head不变才能退出循环,不然会执行多次循环。
*if (h != null && h != tail)判断队列是否至少有两个node,如果队列从来没有初始化过(head为null),或者head就是tail,那么中间逻辑直接不走,直接判断head是否变化了。
*如果队列中有两个或以上个node,那么检查局部变量h的状态:
·如果状态为SIGNAL,说明h的后继是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)和unparkSuccessor(h)绑定在了一起。说明了只要head成功得从SIGNAL修改为0,那么head的后继的线程肯定会被唤醒了。
·如果状态为0,说明h的后继所代表的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到。
·如果状态为PROPAGATE,直接判断head是否变化。
*两个continue保证了进入那两个分支后,只有当CAS操作成功后,才可能去执行if(h == head) break;,才可能退出循环。
*if(h == head) break;保证了,只要在某个循环的过程中有线程刚获取了锁且设置了新head,就会再次循环。目的当然是为了再次执行unparkSuccessor(h),即唤醒队列中第一个等待的线程。
*这个函数的难点在于,很可能有多个线程同时在同时运行它。
selfInterrupt()说明*当线程在等待队列中时,其无法响应来自外部的中断,但是外部中断可以更改其interrupt状态值
*所以在等待队列中可能出现中断状态值是true但是线程并没有响应的情况
*此时就需要讲中断状态值逐级读出并在线程回复响应后如果为true补上未响应且未被清除的中断
*未响应的中断状态的函数间传递见下图
基于AQS的Mutex的实现不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
*Mutex是一个不可重入的互斥锁实现。锁资源(AQS里的state)只有两种状态:0表示未锁定,1表示锁定。
class Mutex implements Lock, java.io.Serializable { // 自定义同步器 private static class Sync extends AbstractQueuedSynchronizer { // 判断是否锁定状态 protected boolean isHeldExclusively() { return getState() == 1; } // 尝试获取资源,立即返回。成功则返回true,否则false。 public boolean tryAcquire(int acquires) { assert acquires == 1; // 这里限定只能为1个量 if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入! setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源 return true; } return false; } // 尝试释放资源,立即返回。成功则为true,否则false。 protected boolean tryRelease(int releases) { assert releases == 1; // 限定为1个量 if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断! throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0);//释放资源,放弃占有状态 return true; } } // 真正同步类的实现都依赖继承于AQS的自定义同步器! private final Sync sync = new Sync(); //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。 public void lock() { sync.acquire(1); } //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。 public boolean tryLock() { return sync.tryAcquire(1); } //unlock<-->release。两者语文一样:释放资源。 public void unlock() { sync.release(1); } //锁是否占有状态 public boolean isLocked() { return sync.isHeldExclusively(); }}
*同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。
·同步类接口的实现直接依赖sync,它们在语义上也存在某种对应关系