并发编程
1.多线程基础
1.1线程和进程的关系
现代操作系统在运行一个程序时(启动一个Java程序,操作系统就会创建一个Java进程。),会为其创建一个进程。
线程:操作系统调度的最小单元是线程;
关系:在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。
1.2操作系统是如何调用线程的?
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。
1.2.1线程优先级是如何体现的?
优先级高的线程分配时间片的数量要多于优先级低的线程。
1.3线程状态
线程创建之后,调用start()方法开始运行。
扩展:Thread.start 之后,他会进入一个就绪状态,还没有分配到 cpu的执行权。 当cpu的时间片切换到他的时候,他才会开始执行,进入running状态
当线程执行wait()方法之后,线程进入等待状态。
进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。
当线程调用同步方法(synchronized)时,在没有获取到锁的情况下,线程将会进入到阻塞状态。
线程在执行Runnable的run()方法之后将会进入到终止状态。
1.4构造线程(线程初始化init)需要有哪些属性?
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。
扩展:
一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识(sync)这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。
1.5线程中断是什么?
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。其他线程通过调用该线程的interrupt()方法对其进行中断操作。
方法isInterrupted():判断线程是否被中断;
静态方法Thread.interrupted():当前线程的中断标识位进行复位。
1.6Thread.sleep方法的特征?
不释放锁;对中断敏感;释放cpu;
1.7Object.wait方法特征?
释放锁;对中断敏感(throws InterruptedException);
释放cpu:让出 CPU 时间片。进入等待队列。
1.8Thread.join()的使用 //TODO
2.volatile
两大特征:保证可见性;禁止指令重排序;
2.1volatile是如何保证共享变量中的“可见性”的?
2.1.1什么是可见性?
当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
谁保证可见性?
Java线程内存模型保证(JMM)。
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,Lock前缀的指令;
Lock前缀有什么作用?
1)Lock前缀指令会引起处理器缓存回写到内存。
2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
2.1.2处理器缓存回写到内存时如何保证原子性?
使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
2.1.3如何保证多核处理器缓存的数据在总线的一致性?
处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。
2.1.4JSR-133使用什么保证两个操作之间的内存可见性。
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
happens-before要求什么?
happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,
且前一个操作按顺序排在第二个操作之前。
2.1.5volatile变量有什么特性?
1)可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
2)原子性:对任意单个volatile变量的读/写具有原子性,volatile++这种复合操作不具有原子性。
2.1.6volatile写-读的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
对volatile写和volatile读的内存语义做个总结。
1)线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。 2)线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。 3)线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
2.1.7线程A和线程B是如何进行消息的传递的?
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
2.1.8JMM控制哪些共享变量?哪些不控制?
所有实例域、静态域和数组元素都存储在堆内存中(统称共享变量),堆内存在线程之间共享;
局部变量(Local Variables),方法定义参数和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
2.2volatile是如何禁止指令重排序的?
2.2.1为什么要进行指令重排序?
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
2.2.2as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
2.2.3volatile内存语义的实现
volatile重排序规则表
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
-
在每个volatile写操作的前面插入一个StoreStore屏障。
-
在每个volatile写操作的后面插入一个StoreLoad屏障。
-
在每个volatile读操作的后面插入一个LoadLoad屏障。
-
在每个volatile读操作的后面插入一个LoadStore屏障。
2.3CAS为什么同时具有volatile读和volatile写的内存语义?
AQS使用一个整型的volatile变量(命名为state)来维护同步状态;
加锁方法首先读volatile变量state。
释放锁的最后写volatile变量state。
根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。
sun.misc.Unsafe类的compareAndSwapInt()方法的源代码调用的c++代码为:unsafe.cpp;处理器会根据情况加lock前缀;
2.4无volatile双重检查锁有什么缺陷?
双重检查锁
public class DoubleCheckedLocking { // 1 private static Instance instance; // 2 public static Instance getInstance() { // 3 if (instance == null) { // 4:第一次检查 synchronized (DoubleCheckedLocking.class) { // 5:加锁 if (instance == null) // 6:第二次检查 instance = new Instance(); // 7:问题的根源出在这里 } // 8 } // 9 return instance; // 10 } // 11 }
(instance=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。
memory = allocate(); // 1:分配对象的内存空间 ctorInstance(memory); // 2:初始化对象 instance = memory; // 3:设置instance指向刚分配的内存地址
2和3之间重排序之后的执行时序如下。
memory = allocate(); // 1:分配对象的内存空间 instance = memory; // 3:设置instance指向刚分配的内存地址 // 注意,此时对象还没有被初始化! ctorInstance(memory); // 2:初始化对象
如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!
2.4.1基于volatile解决双重检查锁缺陷
把instance声明为volatile型;就可以实现线程安全的延迟初始化。
当声明对象的引用为volatile后,代码中的2和3之间的重排序,在多线程环境中将会被禁止。
2.4.2基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。
2.5final域的内存语义?
对于final域,编译器和处理器要遵守两个重排序规则。
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
3.synchronized
Synchronized 的特性
有序性;
可见性;
原子性;
可重入性;
3.1synchronized修饰普通方法,修饰静态同步方法,修饰同步方法块分别锁的是什么?
Java中的每一个对象都可以作为锁。
-
·对于普通同步方法,锁是当前实例对象。
-
·对于静态同步方法,锁是当前类的Class对象。
-
·对于同步方法块,锁是Synchonized括号里配置的对象。
3.2JVM里的Synchonized实现原理
从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。
代码块同步是使用monitorenter和monitorexit指令实现的;
3.3monitorenter是什么时候开始出现的?
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。
3.4那么锁到底存在哪里呢?锁里面会存储什么信息呢?
synchronized用的锁是存在Java对象头里的。
3.4.1对象头还存什么信息?
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
3.5说说锁的状态
锁状态从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。
锁可以升级但不能降级。
3.6偏向锁
使用背景:为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁加锁:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁释放锁:
(全背)
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
使用前提:
1)至少JDK1.6 版本且开启了偏向锁配置。
2)被加锁的对象,没有真正、或者隐式的调用父类 Object 里边的hashcode方法。(如果一旦调用了object的hashcode方法,那么我们的对象头里边就有真正的hashcode值了,如果偏向锁来进行markword的替换,至少要提供一个保存hashcode的地方吧?可惜的是,偏向锁并没有地方进行markworduan的保存,只有轻量级锁才会有“displace mark word”)
举例:
-
A线程获取偏向锁,并且A线程死亡退出。B线程争抢偏向锁,会直接升级当前对象的锁为轻量级锁。这只是针对我们争抢了一次。
-
A线程获取偏向锁,并且A线程没有释放偏向锁(),还在syhnc的代码块里边。B线程此时过来争抢偏向锁,会直接升级为重量级锁。
-
A线程获取偏向锁,并且A线程释放了锁,但是A线程并没有死亡还在活跃状态。B线程过来争抢,会直接升级为轻量级锁。
综上所述,当我们尝试第一次竞争偏向锁时,如果A线程已经死亡,升级为轻量级锁;如果A线程未死亡,并且未释放锁,直接升级为重量级锁;如果A线程未死亡,并且已经释放了锁,直接升级为轻量级锁。
批量重偏向: 当我们的一个对象,Object 类,在经过默认 20次的争抢的情况下,会将后边的所有争抢从新偏向争抢的线程。1. 当B线程争抢第 18 次的时候,触发了批量重偏向的阈值;在第十八次以及以后的争抢里,jvm会将线程偏向线程b,因为jvm认为,这个对象更加适合线程B。
批量撤销:如果基于批量重偏向的基础上,还在继续进行争抢达到40次,并且有第三条线程C加入了,这个时候会触发批量撤销。JVM会标记该对象不能使用偏向锁,以后新创建的对象,直接以轻量级锁开始。 这个时候,才是真正的完成了锁升级。
偏向锁升级为轻量级锁:依赖于 class 的,而并不是依赖于 某一个 new出来的对象。
轻量级锁升级为重量级锁:依赖于 当前new出来的对象的;
3.7轻量级锁
(1)轻量级锁加锁:
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
轻量级锁---重量级锁:
释放锁(前四步)并唤醒等待线程
-
线程1 初始化monitor 对象;
-
将状态设置为膨胀中(inflating);
-
将monitor里边的header属性,set称为对象的markword;(将自己lock record里边的存放的mark word的hashcode,分代年龄,是否为偏向锁 set 到 objectmonitor对象的header属性里)
-
设置对象头为重量级锁状态(标记为改为00);然后将前30位指向第1不他初始化的monitor 对象;(真正的锁升级是由线程1操控的)
-
唤醒线程2;
线程2 开始争抢重量级锁。(线程2就干了一件事儿,就是弄了一个临时的重量级锁指针吧?还不是最后的重量级锁指针。因为最后的重量级锁指针是线程1初始化的并且是线程1修改的。 而且,线程2被唤醒之后,还不一定能够抢到这个重量级锁。Sync是非公平锁。 线程2费力不讨好,但是线程2做了一件伟大的事情:他是锁升级的奠基者。)
3.8以Markword的角度说说锁升级的过程?
创建一个对象,此时对象里边没有hashcode,所以该对象可以使用我们的偏向锁,偏向锁不会考虑hashcode,他会直接将自己的线程id放到我们的markword里边,不需要考虑后续的替换问题。 所以呢,一旦我们的对象主动调用了Object的hashcode方法,我们的偏向锁就自动不可用了。
如果我们的对象有了hashcode和分代年龄和是否为偏向锁(30位)。在轻量级锁的状态下,这30位会被复制到我们的轻量级锁线程持有者的栈帧里的lock record里边记录。
与此同时,我们的对象的markword里边存放的是我们的指向轻量级锁线程持有者的栈帧的lock recod里。如果一直存在轻量级锁竞争,在未发生锁膨胀的前提下,一直会保持轻量级锁,A线程释放的时候,会将markword替换回对象的markword里边,B线程下次再从新走一遍displace mark word;
一旦发生了轻量级膨胀为重量级锁。
前提,A线程持有锁;B线程争抢。
B线程将marikword里边A线程的指针替换成一个临时的(过度的)重量级锁指针,为了让A线程在cas往回替换markword的时候失败。
A线程替换回markword失败后,会发起:1.初始化monitor对象;2. 将状态设置为膨胀中;3 将替换失败的 markword放到objectmonitro的head属性里; 4。改变markword的锁标志为10;将markword里的 30 位设置为指向自己第一步初始化的那个monitor对象;5唤醒B线程; 6以后这个对象只能作为重量级锁;
Markword从未丢失。
4.LOCK锁
4.1Lock锁提供的Synchonized关键字不具备的主要的特性?
尝试非阻塞地获取锁:
boolean tryLock()
Boolean tryLock:尝试非阻塞的获取锁,调用该方法后立即返回,如果能获取则返回true;否则返回false;
能被中断的获取锁:
void lockInterruptibly() throws InterruptedException
lockInterruptibly():可中断的获取锁,响应中断,在获取锁的时候可以中断当前线程
超时获取锁:
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
tryLock(long timeout, TimeUnit unit):超时的获取锁,当前线程在3种情况会返回:
1)当前线程在超时时间内获取到锁;
2)当前线程在超时时间内被中断;
3)超时时间结束,返回false;
4.2lock锁中API有哪些?
4.2lock锁接口是怎么样完成对线程的访问控制?
lock通过同步器AbstractQueuedSynchronizer以及常用Lock接口的实现ReentrantLock。
Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。
4.3队列同步器(AQS)是什么?
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
4.4如何使用这个同步器?
主要的使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
4.5同步器依赖同步队列进行同步状态的管理,说说细节?
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步器的设计是基于模板方法模式;,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
-
getState():获取当前同步状态。
-
setState(int newState):设置当前同步状态。
-
compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
节点状态:
####
同步器可重写的方法:
//Attempts to acquire in exclusive mode.尝试在独占模式下获取。 //This method should query if the state of the object permits it(此方法应查询对象的状态是否允许) to be acquired in the exclusive mode(以独占模式获得), and if so to acquire it.(如果是这样,则获取它。) //This method is always invoked by the thread performing acquire. (此方法始终由执行获取的线程调用。) //If this method reports failure(如果此方法报告失败), the acquire method may queue the thread(获取方法可以将线程排队), if it is not already queued(如果还没有排队), until it is signalled by a release from some other thread(直到它通过从其他线程释放发出信号). This can be used to implement method Lock.tryLock(). protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } //独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); } //独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); } //共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); } //共享式释放同步状态
protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); } //当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
同步器提供的模板方法
4.6独占式同步状态获取与释放
流程图:
首先通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出;
/* Acquires in exclusive mode, ignoring interrupts.(在独占模式下获取,忽略中断。) Implemented by invoking at least once {@link #tryAcquire},returning on success. (通过调用至少一次 tryAcquire 来实现,成功返回。) Otherwise the thread is queued, possibly repeatedly blocking and unblocking, invoking {@link #tryAcquire} until success. This method can be used to implement method {@link Lock#lock}.(否则线程排队,可能重复阻塞和解除阻塞,调用tryAcquire 直到成功。) @param arg the acquire argument. This value is conveyed to {@link #tryAcquire} but is otherwise uninterpreted and can represent anything you like. */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
acquire(int arg)主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,
主要逻辑:
调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态);
并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部;如果当前节点为空或者cas添加失败调用enq()方法创建节点(初始化节点)或者cas死循环,添加到同步队列中;
/*Creates and enqueues node for current thread and given mode.(为当前线程和给定模式创建节点并排队。) 形参: mode – Node.EXCLUSIVE for exclusive, Node.SHARED for shared 返回值: the new node*/ private Node addWaiter(Node mode) { 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(node); return node; } /* Inserts node into queue, initializing if necessary. See picture above.(将节点插入队列,必要时进行初始化) @param node the node to insert @return node's predecessor */ 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; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) {//p == head:node的前一个节点为head节点,说明node节点马上被唤醒竞争锁;tryAcquire:竞争锁成功; setHead(node);//设置头节点 p.next = null; // help GC failed = false; return interrupted; }//获取锁成功 if (shouldParkAfterFailedAcquire(p, node) && //代码如下 parkAndCheckInterrupt()) interrupted = true;//代码如下 } } finally { if (failed) cancelAcquire(node);//代码如下 } }
selfInterrupt()方法;
/** * Convenience method to interrupt current thread. */ static void selfInterrupt() { Thread.currentThread().interrupt();//设置中断标记,并不会中断,不会影响线程等待; }
shouldParkAfterFailedAcquire(p, node):方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus;//拿到等待的状态 if (ws == Node.SIGNAL)//状态为被通知;如果pre节点是当前节点;那么当前节点就会进行park操作 /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true;//被挂起; if (ws > 0) { //>0状态为CANCELLED;Cancelled 状态:一个取消的线程状态。这个状态的线程会被移除 /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev;//移除节点 } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将前一个节点置为等待状态 } return false; }
parkAndCheckInterrupt()方法;
/** * Convenience method to park and then check if interrupted * * @return {@code true} if interrupted */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//中断该线程 return Thread.interrupted();//重置线程设置中断标志 } /** * Tests whether the current thread has been interrupted. The * <i>interrupted status</i> of the thread is cleared by this method. In * other words, if this method were to be called twice in succession, the * second call would return false (unless the current thread were * interrupted again, after the first call had cleared its interrupted * status and before the second call had examined it). * * <p>A thread interruption ignored because a thread was not alive * at the time of the interrupt will be reflected by this method * returning false. * * @return <code>true</code> if the current thread has been interrupted; * <code>false</code> otherwise. * @see #isInterrupted() * @revised 6.0 */ public static boolean interrupted() { return currentThread().isInterrupted(true);//清楚当前线程的中断标志 }
cancelAcquire(Node node)方法;
/** * Cancels an ongoing attempt to acquire. * * @param node the node */ private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // Skip cancelled predecessors Node pred = node.prev; while (pred.waitStatus > 0)//移除之前的节点 node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will // fail if not, in which case, we lost race vs another cancel // or signal, so no further action is necessary. Node predNext = pred.next; // Can use unconditional write instead of CAS here. // After this atomic step, other Nodes can skip past us. // Before, we are free of interference from other threads. node.waitStatus = Node.CANCELLED;//将自己状态设置为CANCELLED // If we are the tail, remove ourselves. if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { unparkSuccessor(node); } node.next = node; // help GC } }
如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
总结:
acquire 方法是 aqs提供的模板方法,是为了进行锁的获取;
tryAcquire 方法是aqs提供的可以复写的方法,主要是完成了加锁状态变化的逻辑(state);
addWaiter 将我们的获取失败的线程放到我们的同步队列里;
enq 如果addwaiter第一次没有成功,就进行死循环添加;
acquireQueued:这部分其实是通过循环的自我检查,如果当前节点的pred节点是头节点,那么就尝试获取锁; 如果不是头节点,就调用 shouldParkAfterFailedAcquire 方法,判断pred节点是否为 SIGNAL 状态,如果是signal状态,自己就好好的等着;如果是 cancell状态,就移除cancell的节点。
其他状态的节点,会通过cas操作替换为 SIGNAL状态。
4.7ReentrantLock()的tryLock() 方法
/** * Acquires the lock only if it is not held by another thread at the time * of invocation. * * <p>Acquires the lock if it is not held by another thread and * returns immediately with the value {@code true}, setting the * lock hold count to one. Even when this lock has been set to use a * fair ordering policy, a call to {@code tryLock()} <em>will</em> * immediately acquire the lock if it is available, whether or not * other threads are currently waiting for the lock. * This "barging" behavior can be useful in certain * circumstances, even though it breaks fairness. If you want to honor * the fairness setting for this lock, then use * {@link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) } * which is almost equivalent (it also detects interruption). * * <p>If the current thread already holds this lock then the hold * count is incremented by one and the method returns {@code true}. * * <p>If the lock is held by another thread then this method will return * immediately with the value {@code false}. * * @return {@code true} if the lock was free and was acquired by the * current thread, or the lock was already held by the current * thread; and {@code false} otherwise */ public boolean tryLock() { return sync.nonfairTryAcquire(1); }
sync.nonfairTryAcquire(1):
/** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ 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;//状态+1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
4.8ReentrantLock()的boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException 方法
/** * Acquires the lock if it is not held by another thread within the given * waiting time and the current thread has not been * {@linkplain Thread#interrupt interrupted}. * * <p>Acquires the lock if it is not held by another thread and returns * immediately with the value {@code true}, setting the lock hold count * to one. If this lock has been set to use a fair ordering policy then * an available lock <em>will not</em> be acquired if any other threads * are waiting for the lock. This is in contrast to the {@link #tryLock()} * method. If you want a timed {@code tryLock} that does permit barging on * a fair lock then combine the timed and un-timed forms together: * * <pre> {@code * if (lock.tryLock() || * lock.tryLock(timeout, unit)) { * ... * }}</pre> * * <p>If the current thread * already holds this lock then the hold count is incremented by one and * the method returns {@code true}. * * <p>If the lock is held by another thread then the * current thread becomes disabled for thread scheduling * purposes and lies dormant until one of three things happens: * * <ul> * * <li>The lock is acquired by the current thread; or * * <li>Some other thread {@linkplain Thread#interrupt interrupts} * the current thread; or * * <li>The specified waiting time elapses * * </ul> * * <p>If the lock is acquired then the value {@code true} is returned and * the lock hold count is set to one. * * <p>If the current thread: * * <ul> * * <li>has its interrupted status set on entry to this method; or * * <li>is {@linkplain Thread#interrupt interrupted} while * acquiring the lock, * * </ul> * then {@link InterruptedException} is thrown and the current thread's * interrupted status is cleared. * * <p>If the specified waiting time elapses then the value {@code false} * is returned. If the time is less than or equal to zero, the method * will not wait at all. * * <p>In this implementation, as this method is an explicit * interruption point, preference is given to responding to the * interrupt over normal or reentrant acquisition of the lock, and * over reporting the elapse of the waiting time. * * @param timeout the time to wait for the lock * @param unit the time unit of the timeout argument * @return {@code true} if the lock was free and was acquired by the * current thread, or the lock was already held by the current * thread; and {@code false} if the waiting time elapsed before * the lock could be acquired * @throws InterruptedException if the current thread is interrupted * @throws NullPointerException if the time unit is null */ public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
4.8.1sync.tryAcquireNanos(1, unit.toNanos(timeout))方法:
对中断敏感
/** * Attempts to acquire in exclusive mode, aborting if interrupted, * and failing if the given timeout elapses. Implemented by first * checking interrupt status, then invoking at least once {@link * #tryAcquire}, returning on success. Otherwise, the thread is * queued, possibly repeatedly blocking and unblocking, invoking * {@link #tryAcquire} until success or the thread is interrupted * or the timeout elapses. This method can be used to implement * method {@link Lock#tryLock(long, TimeUnit)}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. * @param nanosTimeout the maximum number of nanoseconds to wait * @return {@code true} if acquired; {@code false} if timed out * @throws InterruptedException if the current thread is interrupted */ public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || //进行加锁 doAcquireNanos(arg, nanosTimeout); //等待获取 } /** * Acquires in exclusive timed mode. * * @param arg the acquire argument * @param nanosTimeout max wait time * @return {@code true} if acquired */ private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { //可被中断 if (nanosTimeout <= 0L) //没有获取到 return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { //是head并且获取锁成功 setHead(node); p.next = null; // help GC failed = false; return true; } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) //超时了 return false; if (shouldParkAfterFailedAcquire(p, node) && //ws的3个判断 nanosTimeout > spinForTimeoutThreshold) //为了使超时时间不过期 LockSupport.parkNanos(this, nanosTimeout); //继续等待 if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
//tryLock() 为了进行一次性的获取锁,如果获取成功则成功,如果失败则失败 //tryLock(1,null) 在超时时间以内,循环获取锁、
lock.lockInterruptibly();方法
/** * Acquires the lock unless the current thread is * {@linkplain Thread#interrupt interrupted}. * * <p>Acquires the lock if it is not held by another thread and returns * immediately, setting the lock hold count to one. * * <p>If the current thread already holds this lock then the hold count * is incremented by one and the method returns immediately. * * <p>If the lock is held by another thread then the * current thread becomes disabled for thread scheduling * purposes and lies dormant until one of two things happens: * * <ul> * * <li>The lock is acquired by the current thread; or * * <li>Some other thread {@linkplain Thread#interrupt interrupts} the * current thread. * * </ul> * * <p>If the lock is acquired by the current thread then the lock hold * count is set to one. * * <p>If the current thread: * * <ul> * * <li>has its interrupted status set on entry to this method; or * * <li>is {@linkplain Thread#interrupt interrupted} while acquiring * the lock, * * </ul> * * then {@link InterruptedException} is thrown and the current thread's * interrupted status is cleared. * * <p>In this implementation, as this method is an explicit * interruption point, preference is given to responding to the * interrupt over normal or reentrant acquisition of the lock. * * @throws InterruptedException if the current thread is interrupted */ public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
sync.acquireInterruptibly(1);方法
/** * Acquires in exclusive mode, aborting if interrupted. * Implemented by first checking interrupt status, then invoking * at least once {@link #tryAcquire}, returning on * success. Otherwise the thread is queued, possibly repeatedly * blocking and unblocking, invoking {@link #tryAcquire} * until success or the thread is interrupted. This method can be * used to implement method {@link Lock#lockInterruptibly}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. * @throws InterruptedException if the current thread is interrupted */ public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } /** * Acquires in exclusive interruptible mode. * @param arg the acquire argument */ private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
4.8.2lockInterruptibly方法,是一个支持中断的加锁方式。他与 lock.tryLock(1,null) 这个有什么区别?
相同点:都支持中断
不同点: lockInterruptibly方法仅仅支持中断;不支持超时。lock.tryLock(1,null)即支持超时,也支持超时内的时间中断;
4.9lock.unlock();方法
调用tryRelease,直到释放掉所有的锁(state =0),因为考虑有重入的情况。然后唤醒后继(unparkSuccessor)线程让他进行锁竞争。
/** * 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() { sync.release(1); }
sync.release(1);方法释放同步状态
/** * Releases in exclusive mode. Implemented by unblocking one or * more threads if {@link #tryRelease} returns true. * This method can be used to implement method {@link Lock#unlock}. * * @param arg the release argument. This value is conveyed to * {@link #tryRelease} but is otherwise uninterpreted and * can represent anything you like. * @return the value returned from {@link #tryRelease} */ public final boolean release(int arg) { if (tryRelease(arg)) {//tryRelease自己复写的方法 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); //唤醒下一个线程 return true; } return false; }
unparkSuccessor(h);方法 //唤醒下一个线程
/** * Wakes up node's successor, if one exists. * * @param node the node */ private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //设置初始状态 /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; //s为后继节点 if (s == null || s.waitStatus > 0) { //后继节点为空或者为cancelled状态 s = null; for (Node t = tail; t != null && t != node; t = t.prev) //tail是尾节点 if (t.waitStatus <= 0) //找线程为SIGNAL状态的-1,目前在同步队列;cancelled状态-2在等待队列中;propagate-3在共享同步状态 s = t; //重新为s赋值 } if (s != null) LockSupport.unpark(s.thread); //唤醒S的线程; }
4.10共享式获取同步状态
调用同步器的acquireShared(int arg)方法可以共享式地获取同步状态;
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) //同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0。 doAcquireShared(arg); } private void doAcquireShared(int arg) { //在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。 final Node node = addWaiter(Node.SHARED); //加入尾部队列 boolean failed = true; try { boolean interrupted = false; for (;;) { //进入自旋 final Node p = node.predecessor();//拿到当前节点的前一个节点 if (p == head) {//是头节点 int r = tryAcquireShared(arg);//获取锁 if (r >= 0) {//获取锁成功 setHeadAndPropagate(node, r);//代码如下 p.next = null; if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && //将前驱节点的状态改成SIGNAL. parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
setHeadAndPropagate(node, r);
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); /* * propagate>0,说明还有资源 * h.waitStatus < 0 h的waitStatus要么是-1,要么是-3, * 满足以上2个条件之一,就能进入doReleaseShared(),释放后继节点了 */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared(); } }
doReleaseShared();
private void doReleaseShared() { /* * *会先判断head节点的waitStatus,doReleaseShared只被2个方法调用,1个是setHeadAndPropagate,1个是releaseShared,我只讨论setHeadAndPropagate方法调用的情况 *所以进来的head的waitStatus也只有3种可能,0,-1,-3. *如果是0的情况说明还有资源,但是后继节点为空,则会把waitStatus设置成-3,便结束这个方法.当节点释放锁的时候,会执行releaseShared,如果这个时候waitStatus还是0,说明后面还是没节点,如果后面有节点,则必定会更改waitStatus为-1 *如果是-1,则会更改成0并且唤醒后继节点 */ for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
4.11公平锁与非公平锁的区别?
-
synchronized 他是一个非公平锁。
-
Lock分为非公平锁(默认) 还有公平锁。非公平锁: 当我们的线程在同步队列里排队完成之后,获取锁的时候,这个时间点上如果有其他新的线程来竞争锁,那么我当前的排队的锁可能**会被插队(当前线程可能竞争不过新来的线程,导致自己竞争锁失败。)** 这是不公平的,我(当前线程) 已经在同步队列里排了好长时间了,你这新来的线程直接抢走了。 公平锁:获取锁的时候,这个时间点上如果有其他新的线程来竞争锁,那么新的线程会直接加入到同步队列里(源码),cas set tail。
-
性能比较(公平和非公平):肯定是非公平锁性能更高。(都要+同步队列里,都要进行我们源码中的一系列的操作;公平锁会有更多的上下文切换, 挂起,park())
-
非公平锁容易造成线程饥饿。(会被插队,极限情况考虑,如果一直被插队,同步队列里的其他线程就等着呗,饥饿。)
-
很多情况我们在进行实战开发的时候,如果要限定我们的线程的访问先后顺序,就要使用公平锁了。
nonfairTryAcquire(int acquires)方法,对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同,公平锁多了hasQueuedPredecessors()方法。
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && //这个方法!加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
4.12读写锁中的锁是如何维护的?
读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。
读写锁将变量切分成了两个部分,高16位表示读,低16位表示写
写锁是一个支持重进入的排它锁。
ReentrantReadWriteLock的tryAcquire方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。
4.12.1为什么存在读锁,写锁获取不到?
读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
4.12.2读锁的获取?
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。
获取不到读锁的情况:
如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
4.12.3每个线程各自获取读锁的次数保存在哪里?
保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。
如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。 读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
4.12.4读写锁的锁降级中读锁的获取是否必要呢?
读写锁的锁降级是什么?
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
为什么要这样释放锁?
主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
4.13LockSupport工具做什么?
当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。
LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。Park有停车的意思,假设线程为车辆,那么park方法代表着停车,而unpark方法则是指车辆启动离开。
在Java 6中,LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。
4.14Condition接口
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待
而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
获取一个Condition必须通过Lock的newCondition()方法。
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
5.并发容器
5.1ConcurrentLinkedQueue
如果要实现一个线程安全的队列有两种方式:
一种是使用阻塞算法,使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。
另一种是使用非阻塞算法。非阻塞的实现方式则可以使用循环CAS的方式来实现。
5.2ConcurrentLinkedQueue的数据结构是什么?
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。
ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。
5.3ConcurrentLinkedQueue的入队是如何实现的?
入队列就是将入队节点添加到队列的尾部。
第一是将入队节点设置成当前队列尾节点的下一个节点;
第二是更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点(性能考虑)
源码分析:
public boolean offer(E e) { if (e == null) throw new NullPointerException(); // 入队前,创建一个入队节点 Node<E> n = new Node<E>(e); retry: // 死循环,入队不成功反复入队。 for (;;) { // 创建一个指向tail节点的引用 Node<E> t = tail; // p用来表示队列的尾节点,默认情况下等于tail节点。 Node<E> p = t; for (int hops = 0; ; hops++) { // 获得p节点的下一个节点。 Node<E> next = succ(p); // next节点不为空,说明p不是尾节点,需要更新p后在将它指向next节点 if (next != null) { // 循环了两次及其以上,并且当前节点还是不等于尾节点 if (hops > HOPS && t != tail) continue retry; p = next; } // 如果p是尾节点,则设置p节点的next节点为入队节点。 else if (p.casNext(null, n)) { /*如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点, 更新失败了也没关系,因为失败了表示有其他线程成功更新了tail节点*/ if (hops >= HOPS) casTail(t, n); // 更新tail节点,允许失败 return true; } // p有next节点,表示p的next节点是尾节点,则重新设置p节点 else { p = succ(p); } } } }
5.4ConcurrentLinkedQueue的出队是如何实现的?
出队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。
并不是每次出队时都更新head节点,当head节点里有元素时,直接弹出head节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head节点。这种做法也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。
首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。
5.5阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。 2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
5.6阻塞队列的实现原理
ArrayBlockingQueue使用了Condition来实现;
使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.park(this)来实现。
先保存一下将要阻塞的线程,然后调用unsafe.park阻塞当前线程。是个native方法。
JVM是如何实现park方法?
park在不同的操作系统中使用不同的方式实现,,在Linux下使用的是系统方法pthread_cond_wait实现。cond是condition的缩写字面意思可以理解为线程在等待一个条件发生,这个条件是一个全局变量。这个方法接收两个参数:一个共享变量cond,一个互斥量mutex。而unpark方法在Linux下是使用pthread_cond_signal实现的。
5.7Java中的7中阻塞队列
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
5.8说说Fork/Join框架
把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
5.9CountDownLatch、CyclicBarrier、Semaphore、Exchanger工具类分别说明作用?
CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段。
CountDownLatch允许一个或多个线程等待其他线程完成操作。
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
Exchanger工具类则提供了在线程间交换数据的一种手段。线程间协作的工具类。
5.10CountDownLatch中的join()方法
join用于让当前执行线程等待join线程执行结束。
join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。
直到join线程中止后,线程的this.notifyAll()方法会被调用,调用notifyAll()方法是在JVM里实现的;
5.11CountDownLatch的countDown方法
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。
当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。
如果有某个解析sheet的线程处理得比较慢,我们不可能让主线程一直等待,所以可以使用另外一个带指定时间的await方法——await(long time,TimeUnit unit),这个方法等待特定时间后,就会不再阻塞当前线程。join也有类似的方法。
5.12CyclicBarrier默认的构造方法CyclicBarrier(int parties)表示什么意思?
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrier-Action),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景,
5.13CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。
CyclicBarrier还提供其他有用的方法
比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。
isBroken()方法用来了解阻塞的线程是否被中断。
5.14Semaphore的构造方法Semaphore(int permits)
Semaphore的构造方法Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。
5.15Exchanger的exchange()方法
它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
Exchanger可以用于遗传算法;Exchanger也可以用于校对工作,
6.线程池
6.1线程池能够带来3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
6.2ThreadPoolExecutor执行execute方法有哪几种情况?
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。 2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。 3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。 4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
6.3线程池的核心参数
1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。
3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。
4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。
饱和策略:
·AbortPolicy:直接抛出异常。 ·CallerRunsPolicy:只用调用者所在线程来运行任务。 ·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。 ·DiscardPolicy:不处理,丢弃掉。
实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
6.4向线程池提交任务,execute()和submit()方法区别?
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
6.5关闭线程池调用线程池的shutdown或shutdownNow方法有什么区别?
相同点:
原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。
不同点:
shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表;
shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
6.6如何合理地配置线程池?
首先分析任务特性:
·任务的性质:CPU密集型任务、IO密集型任务和混合型任务。 ·任务的优先级:高、中和低。 ·任务的执行时间:长、中和短。 ·任务的依赖性:是否依赖其他系统资源,如数据库连接。
CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。
IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。
混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。
建议使用有界队列。1. 如果是无界队列,那么queue永远不会满,永远不会触发到maximumPoolSize,意味着maximumPoolSize这个参数就没有他的作用了; 2. 最重要的是无界队列无法控制队列最终包含的数据量,导致内存资源的极大的消耗甚至耗尽。3. 选用有界队列并合理的配置maximumPoolSize。4 饱和策略的使用根据需求选择。一旦我们触发了饱和策略,就说明:要么是我们的线程池配置有问题,要么真的是并发量太高,任务太多,导致的问题。警醒我们进行深入的参数调查及合理分配。
综合:
-
我们的线程池是生存在一个复杂的系统环境里,我们还有其他的接口需要使用我们的服务器资源,所以在进行线程池coresize的配置以及maxsize的配置的时候,我们需要明确我们当前的接口的重要性,如果当前接口占据了未来业务访问的50%,那么就可以分配50%的系统资源给当前接口。(我们一个服务,总有一些重要接口和非重要接口,在我们进行项目开发初期,需求就定好了。)
-
一定要基于压测。来评估线程池的参数是否合理。(全服务压测。)
-
我们要给线程池开后门,可以动态的调整线程池的参数。(我们现在很多大型项目都有自己的配置中心,appolo是一个非常好的配置组件,你可以将coresize和maxsize配置到配置中心,一旦发生不可控的高并发场景,可以随时修改配置中心的参数,我们的项目就会按照新的标准进行调整。)
6.7ScheduledThreadPoolExecutor的运行机制
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
// 创建 ScheduledThreadPool的方式及源码 // ScheduledThreadPool 其实是我们 ThreadPoolExecutor的一个子类,所以构造函数使用的是 ThreadPool的 // 但是,它使用的是 delayQueue new DelayedWorkQueue() // 这两种创建方式有什么不同呢?Executors.newScheduledThreadPool(5);调用的方法也是 new ScheduledThreadPoolExecutor(5); // ScheduledThreadPoolExecutor 是我们的 ScheduledExecutorService 的一个子类 // 建议选择第一种创建方式 和 第三种创建方式。更倾向于第三种创建方式。 // ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5); // ScheduledExecutorService service = Executors.newScheduledThreadPool(5); ScheduledThreadPoolExecutor pool = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(5); // schedule 方法只适用于 一次性的执行。一个任务只执行一次就完事儿了。 pool.schedule(new STask(1), 5, TimeUnit.SECONDS); // 如果我想要重复执行一个任务。怎么办? // 这个方法是以固定的频率进行循环执行。5s以后开始第一次执行,之后每2秒再次执行一次。 // 这个方法不会考虑前一次执行的时间。 // 如果第一次执行的时间超过了时间间隔,那么不可以在预期的时间执行第二次。 //因为: 我们task 是周期执行的,每次执行完一次之后,我们的ScheduledThreadPoolExecutor会从新计算下次的执行时间 // 然后将下次的执行时间修改完后再次 add 到我们的 delayQUEUE 里。现在如果是一个 while循环,那么永远不能执行结束 //我们的ScheduledThreadPoolExecutor就没有办法再次的从新计算时间,并且从新添加到我们的delayqueue中。 pool.scheduleAtFixedRate(new STask(1), 5, 5, TimeUnit.SECONDS); //5s以后开始第一次执行,执行完毕之后 2s 后开始执行第二次; 第二次执行完成后,2s后执行第三次。 // 考虑前一次执行的时间,只有前一次执行完成之后,才会计算2s的时间间隔。 // pool.scheduleWithFixedDelay(new STask(1), 5, 5, TimeUnit.SECONDS); } private static class STask implements Runnable { private int taksNum; public STask(int taksNum) { this.taksNum = taksNum; } @SneakyThrows @Override public void run() { System.out.println("Scheduled task is running! taskNum = " + taksNum); Thread.sleep(6000); } }
6.8通过Executor框架的工具类Executors,可以创建哪3种类型的ThreadPoolExecutor?
-
·FixedThreadPool。可重用固定线程数的线程池。
-
·SingleThreadExecutor。使用单个worker线程的Executor(SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1)
-
·CachedThreadPool。会根据需要创建新线程的线程池。
6.9这3种线程池分别有什么特点?
FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。 1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。 2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。 3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。 4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。
CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。
7.实践
7.1线程池与生产消费者模式
生产者把任务丢给线程池,线程池创建线程并处理任务,如果将要运行的任务数大于线程池的基本线程数就把任务扔到阻塞队列里,这种做法比只使用一个阻塞队列来实现生产者和消费者模式显然要高明很多,因为消费者能够处理直接就处理掉了,这样速度更快,而生产者先存,消费者再取这种方式显然慢一些。
7.2需要处理任务时间比较长的场景使用线程池与生产消费模式
比如上传附件并处理,用户把文件上传到系统后,系统把文件丢到队列里,然后立刻返回告诉用户上传成功,最后消费者再去队列里取出文件处理。再如,调用一个远程接口查询数据,如果远程服务接口查询时需要几十秒的时间,那么它可以提供一个申请查询的接口,这个接口把要申请查询任务放数据库中,然后该接口立刻返回。然后服务器端用线程轮询并获取申请任务进行处理,处理完之后发消息给调用方,让调用方再来调用另外一个接口取数据。
7.3线上问题定位
看日志、系统状态和dump线程
CPU利用率100%
很有可能程序里写了一个死循环。
第一种情况,某个线程CPU利用率一直100%,则说明是这个线程有可能有死循环,那么请记住这个PID。 ·第二种情况,某个线程一直在TOP 10的位置,这说明这个线程可能有性能问题。 ·第三种情况,CPU利用率高的几个线程在不停变化,说明并不是由某一个线程导致CPU偏高。
把线程dump下来,看看究竟是哪个线程、执行什么代码造成的CPU利用率高。
dump出来的线程ID(nid)是十六进制的,而我们用TOP命令看到的线程ID是十进制的,所以要用printf命令转换一下进制。然后用十六进制的ID去dump里找到对应的线程。
7.4性能测试
7.4.1如何支持2万的QPS
必须先要知道该接口在单机上能支持多少QPS,如果单机能支持1千QPS,我们需要20台机器才能支持2万的QPS。需要注意的是,要支持的2万的QPS必须是峰值,而不能是平均值,比如一天当中有23个小时QPS不足1万,只有一个小时的QPS达到了2万,我们的系统也要支持2万的QPS。(压测开发服务器。)
-