锁类型
悲观锁和乐观锁
悲观锁:认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。
java中的悲观锁就是Synchronized,> AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
乐观锁:认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
公平锁和非公平锁
- 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;
公平锁可以使用new ReentrantLock(true)实现 - 非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
自旋锁
- 自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会拜拜浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。
- 自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK6中已经变为默认开启,并且引入了自适应的自旋锁。自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 自旋是在轻量级锁中使用的,在重量级锁中,线程不使用自旋。
锁消除
锁消除是虚拟机JIT在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而能被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。
public String concatString(String s1, String s2, String s3)
{
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
也就是说在concatString()方法中涉及了同步操作。但是可以观察到sb对象它的作用域被限制在方法的内部,也就是sb对象不会“逃逸”出去,其他线程无法访问。因此,虽然这里有锁,但是可以被安全的消除,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。
锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制的尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁禁止,那等待的线程也能尽快拿到锁。大部分情况下,这些都是正确的。但是,如果一些列的联系操作都是同一个对象反复加上和解锁,甚至加锁操作是出现在循环体中的,那么即使没有线程竞争,频繁地进行互斥同步操作也导致不必要的性能损耗。
举个案例,类似锁消除的concatString()方法。如果StringBuffer sb = new StringBuffer();定义在方法体之外,那么就会有线程竞争,但是每个append()操作都对同一个对象反复加锁解锁,那么虚拟机探测到有这样的情况的话,会把加锁同步的范围扩展到整个操作序列的外部,即扩展到第一个append()操作之前和最后一个append()操作之后,这样的一个锁范围扩展的操作就称之为锁粗化。
可重入锁
可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。
类锁和对象锁
类锁:在方法上加上static synchronized的锁,或者synchronized(xxx.class)的锁。如下代码中的method1和method2:
对象锁:参考method4, method5,method6.
public class LockStrategy
{
public Object object1 = new Object();
public static synchronized void method1(){}
public void method2(){
synchronized(LockStrategy.class){}
}
public synchronized void method4(){}
public void method5()
{
synchronized(this){}
}
public void method6()
{
synchronized(object1){}
}
}
偏向锁、轻量级锁和重量级锁
- synchronized的偏向锁、轻量级锁以及重量级锁是通过Java对象头实现的。
- Java对象的内存布局分为:对象头、实例数据和对齐填充,而对象头又可以分为”Mark Word”和类型指针klass。”Mark Word”是关键,默认情况下,其存储对象的HashCode、分代年龄和锁标记位。
- 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
整个synchronized锁流程如下:
- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
- 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级状态。
- 如果自旋失败,则升级为重量级锁。
共享锁和排它锁
共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
排它锁:如果事务T对数据A加上排它锁后,则其他事务不能再对A加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。
读写锁
读写锁是一个资源能够被多个读线程访问,或者被一个写线程访问但不能同时存在读线程。Java当中的读写锁通过ReentrantReadWriteLock实现。
互斥锁
所谓互斥锁就是指一次最多只能有一个线程持有的锁。在JDK中synchronized和JUC的Lock就是互斥锁。
闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动指导其他活动都完成后才继续执行。CountDownLatch就是一种灵活的闭锁实现。
分段锁
ConcurrentHashMap中采用了分段锁
死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下4个条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序做操作来避免死锁。
活锁
LiveLock是一种形式活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,而且总会失败。活锁通常发送在处理事务消息的应用程序中:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的开头。如果消息处理器在处理某种特定类型的消息时存在错误并导致它失败,那么每当这个消息从队列中取出并传递到存在错误的处理器时,都会发生事务回滚。由于这条消息又被放回到队列开头,因此处理器将被反复调用,并返回相同的结果。
并发编程的3个基本概念
- 原子性 定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
Java中的原子性操作包括:
(1) 基本类型的读取和赋值操作, 且赋值必须是值赋给变量, 变量之间的相互赋值不是原子性操作。
(2) 所有引用reference的赋值操作
(3)java.concurrent.Atomic.* 包中所有类的一切操作 - 可见性 定义: 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
synchronize和Lock都可以保证可见性。 - 有序性 定义:即程序执行的顺序按照代码的先后顺序执行。
Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性
java线程阻塞的代价
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。
明确java线程切换的代价,是理解java中各种锁的优缺点的基础之一。
markword
synchronized与Lock的区别
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 sync是JVM层面的,底层通过 monitorenter 和 monitorexit 来实现的。 | Lock是JDK API层面的。是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断(sync悲观锁, 可重入的锁, 会一直等待对象头锁信息, 阻塞的, 不可中断,除非抛出异常或者正常运行完成。) 非公平 | 可重入 可判断 可中断(Lock是可中断的,通过调用interrupt()方法。) 可公平(Lock既能是公平锁,又能是非公平锁) |
性能 | 少量同步 | 大量同步 |
绑定多个条件 | sync不能,只能随机唤醒。synchronized是重量级锁,不适合大量代码; | 而Lock可以通过Condition来绑定多个条件,精确唤醒。Lock可以,lock锁的粒度更细,分为读锁写锁等 |
区别:
- 用法不一样。synchronized既可以加在方法上,也可以加载特定的代码块上,括号中表示需要锁的对象。而Lock需要显示地指定起始位置和终止位置。synchronzied是托管给jvm执行的,Lock锁定是通过代码实现的。
- 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
- 锁的机制不一样。synchronized获得锁和释放的方式都是在块结构中,而且是自动释放锁。而Lock则需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题的发生。
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;
而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在 finally 块中释放锁; - Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。Lock可以提高多个线程进行读操作的效率。
Lock
Lock:Lock实现和synchronized不一样,后者是一种悲观锁。而Lock底层其实是 CAS乐观锁 。 底层主要靠 volatile和 CAS操作实现的。
Synchronized
- synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁。
- Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
- synchronized关键字最主要有以下3种应用方式
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized底层语义原理
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。
同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。
下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。
理解Java对象头与 Monitor
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
虚拟机位数 头对象结构 说明
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象HashCode 对象分代年龄 0 01
Java虚拟机对synchronized的优化
偏向锁是Java 6之后加入的新锁
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
轻量级锁 轻量级锁的优化手段(1.6之后加入的) (对绝大部分的锁,在整个同步周期内都不存在竞争)
轻量级锁能够提升程序性能的依据是“ 对绝大部分的锁,在整个同步周期内都不存在竞争 ”,注意这是经验数据。
需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
自旋锁 (大多数情况下,线程持有锁的时间都不会太长)
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,
这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,
通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,
因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
synchronized的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
等待唤醒机制
- notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说
- notify/notifyAll和wait方法依赖于 monitor 对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么
- notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。
同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
题目
单例模式的双重锁为什么要加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
// 需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码
a. memory = allocate() // 分配内存
b. ctorInstanc(memory) // 初始化对象
c. instance = memory // 设置instance指向刚分配的地址
instance = new TestInstance(); //5
}
}
}
return instance; //6
}
}
volatile原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。 在JVM底层volatile是采用内存屏障来实现的。 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个 内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile变量的特性
- 保证可见性,不保证原子性
- 禁止指令重排 (在JVM底层volatile是采用“内存屏障”来实现的。 )
AQS (Abstract Queued Synchronizer)
AQS: AbstractQuenedSynchronizer 抽象的队列式同步器 。是除了java自带的synchronized关键字之外的锁机制。
AQS就是基于CLH双向队列,用volatile修饰共享变量state,线程通过CAS去改变状态符 state,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
AQS作为基础组件,实现的锁存在两种模式:
- Exclusive:独占,只有一个线程能执行,如 ReentrantLock (Lock lock = new ReentrantLock(); 支持重入锁 加锁多少次,就必须解锁多少次)
- Share:共享,多个线程可以同时执行,如 Semaphore、 CountDownLatch、ReadWriteLock,CyclicBarrier
无论是共享模式还是独占模式,内部都是基于同步队列器AQS实现的,也都维持了一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node节点并将线程当前的内容存储到Node节点中,然后加入同步队列中等待获取锁,无论是Semaphore还是ReentrantLock,其内部大多数方法是间接调用AQS完成的。
**总结:**abstractqueuesysnchronizer中的head和tail分别指向同步队列的头部和尾部,不存储信息,采用双向链表的表示结构可以方便节点增加和删除操作,abstractqueuesysnchronizer中的state变量表示同步的状态。lock实例调用lock()方法进行加锁的时候,如果此时lock实例内部state的值为0,则说明当前线程可以获取到锁
AbstractQueuedSynchronizer源码:
public abstract calss AbstractQueuedSynchronizer extends AbstranctOwnableSynchronizer{
private transient volatile Node head;// 指向同步队列的队列头
private transient volatile Node tail;// 指向同步队列的对列尾
private volatile int state;// 同步状态,0未被占用1被占用
}
AbstractQueueSynchronizer的整体结构:
- AbstractQueuedSynchronizer:内部以内部类node构成的同步队列、等待队列和state变量管理线程的锁的获取和释放,其中acquire() release()方法提供了实现,tryAcquire()方法和tryRelease()方法没有提供默认实现,需要子类重写这两个方法,开发者可以自己定义获取锁和释放锁的实现方式。
- Sync继承与AbstractQueuedSynchronizer,实现了tryRelease())释放锁的操作,在内部声明了lock()方法没有实现
- NonfairSync:继承与abstractSync,在内部实现了lock和tryAcquire()方法,是公平锁的实现类
- fairSync:继承与abstractSync,在内部实现lock和tryAcquire()方法,是非公平锁的实现类
并发工具类:
ReentantLock
ReentantReadWriteLock
Semaphore
CountDownLatch
Worker(ThredPoolExecutor类中)
Node节点:AbstractQueuedSynchronizer的内部类
static final class Node {
//注释翻译中有写AQS支持独占和共享两种模式,先不讨论怎么实现的,从注释可知这两个字段代表如下:
//注意:这两个字段并不会指向新的引用(随着Node初始化进行初始化),所以一定是其他指针指向这两个节点
static final Node SHARED = new Node();//表明当前Node代表的线程以共享模式等待
static final Node EXCLUSIVE = null;//表明当前Node代表的线程以阻塞模式等待
static final int CANCELLED = 1;//标识当前线程已经取消等待或者叫阻塞
static final int SIGNAL = -1;//表明当前节点的下一个节点所代表的线程需要被唤醒
static final int CONDITION = -2;//表明当前线程在等待某个条件而阻塞
// 值为-3,共享模式相关,在共享模式中,该状态标识的线程处于可运行状态 0状态:值为0,代表初始化状态
static final int PROPAGATE = -3;
volatile int waitStatus;
//由此可以看出AQS维护的是一个双向队列,那么为什么其要维护一个双向队列呢,请看后文分析
volatile Node prev;
volatile Node next;
//AQS是一个并发同步框架,那么队列存储的数据必然是阻塞线程
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
ReetrantLock
-
ReetrantLock是lock接口的实现类,与synchronized的monitor中的count一样,reentrantlock也是一种可重入锁,支持线程对资源进行重复加锁,加锁多少次就要解锁锁少次,reentrantlock也支持公平锁和非公平锁,synchronized是隐式锁不能干预不能控制,reentrantlock实现的锁可以控制加锁和释放锁的过程。
-
Reentrantlock类中的方法有很多都和线程相关,Reentrantlock内部是通过AQS并发框架实现的,就是同步队列器AbstractQueuedSynchronized实现的
-
ReentrantLock lock=new ReentrantLock(ture); // 参数true实现公平锁 不传参数默认实现非公平锁
-
Reentrantlock有三个内部类,这三个内部类支撑着reentrantlock实现的方法,这三个方法分别继承了abstractqueuesynchronizer,ReentrantLock中的内部类有Sync FairSync NofairSync,在创建的时候根据fair参数决定创建NonfairSync还是FairSync,其中lock方法是Sync提出的,由nofairsync和fairsync实现的,unlock方法是abstractqueuesynchronizer实现的,同时tryRelease是有sync实现的,tryAcquire是由nofairsync和fairsync实现的
Reentrantlock就是AbstractQueueSynchronizer的独占模式实现的,AbstractQueueSynchronizer的独占模式是怎么实现的?
- 首先abstractQueuesynchronizer中的独占模式提供的模板方法是tryRelease tryAcquire
release acquire,这些方法是需要自己实现,Reentrantlock内部有三个内部类Sync实现了tryRelease noFairSync和FairSync实现了tryAcquire方法 - ReentrantLock中提供了公平锁和非公平锁两种,两个内部类来实现,在构造方法中通过指定true表示指定是实现公平锁还是非公平锁