前言
在编程中,很多人都会尝试使用多线程的方式去编程,但是却很难保证自己写出来的多线程程序的正确性,在多线程中如果涉及到对共享资源的并发读写,这时就会产生资源的争夺。而在资源争夺中,第一想到的就是使用锁 ,对共享资源进行数据保护。java中提供了2种基本也是最常用的锁,synchronized、Lock!但是这2种锁有什么特点?分别使用的场景?在使用过程中应该注意哪些?各自有哪些不足呢?
本文将带着上面的问题并结合源码进行仔细分析,让你对锁有一个深入的了解。
synchronized 详解
一、概念
synchronized 是java中的关键字,利用锁的机制来实现同步,来达到对共享资源保护。
锁的机制有2种特质:
1、互斥性:在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制。互斥性我们往往称作原子性
2、可见性:确保在锁被释放之前,对共享变量所做的修改,对其余的线程是可见的(也就是说在获得锁的时候应获得最新的共享变量值),否则对于另外一个线程在操作共享变量是自己在本地缓存的副本上,这样就会引发数据的不一致。
二、对象锁和类锁
在使用Synchronized之前,先了解什么是对象锁和类锁
1、对象锁
在java中,每个对象都会有一个monitor对象,这个对象其实就是java对象锁,通常也被称作为"内置锁"或"对象锁",类的对象有多个,所有对象锁也有多个,相互之间互不干涉。
2、类锁
在java中,针对每一个类都有一个帧,可以称作为"类锁",类锁实际也是通过对象锁实现的,即类的Class对象锁,每一个类有且仅有一个Class对象(JVM加载类的时候产生),所有每一个类只有一个类锁。
三、修饰方法
1、修饰实例方法:作用于当前实例加锁,在进入同步代码方法时先获取当前实例锁
2、修饰静态方法:作用于当前类对象锁,在进入同步代码方法时先获取当前对象锁(类.class)
修饰实例方法:
顾名思义就是修饰类中的实例方法,并且默认是当前对象作为锁的对象,而一个对象只有一把锁,所以同一时刻只能有一个线程执行被同步的方法,等到线程执行完方法后,其他线程才能继续执行被同步的方法。正确实例代码如下:
public class SynchronizedTest implements Runnable {
public static int count = 0;
@Override
public void run() {
for (int i = 1; i <= 1000000; i++) {
add();
}
}
public synchronized void add() {
++count;
}
public static void main(String[] args) throws Exception {
SynchronizedTest test = new SynchronizedTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count); //2000000
}
}
错误代码:对同一共享资源使用不同实例锁
public class SynchronizedTest implements Runnable {
private Object object;
public SynchronizedTest(Object object) {
this.object = object;
}
public static int count = 0;
@Override
public void run() {
synchronized (object) {
for (int i = 1; i <= 1000000; i++) {
add();
}
}
}
public static void add() {
++count;
}
public static void main(String[] args) throws Exception {
Object object1 = new Object();
Object object2 = new Object();
SynchronizedTest firstLock = new SynchronizedTest(object1);
SynchronizedTest secondLock = new SynchronizedTest(object2);
Thread t1 = new Thread(firstLock);
Thread t2 = new Thread(secondLock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);//小于 20000000
}
}
运行程序后,会发现结果永远小于2000000,说明synchronized没有起到同步的作用了,说明修饰实例方法只能作用实例对象,不能作用到类对象
修饰静态方法:
public class SynchronizedTest implements Runnable {
public static int count = 0;
@Override
public void run() {
for (int i = 1; i <= 100000; i++) {
add();
}
}
public static synchronized void add() {
++count;
}
public static void main(String[] args) throws Exception {
SynchronizedTest firstLock = new SynchronizedTest();
SynchronizedTest secondLock = new SynchronizedTest();
Thread t1 = new Thread(firstLock);
Thread t2 = new Thread(secondLock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);=2000000
}
}
作用与静态方法的时候,不管实例化多少个实例对象,结果用于等2000000,说明锁对象是当前类.class,有且仅有一把锁,最终结果和实际结果一致!
修饰代码块:正确案例
ppublic class SynchronizedTest implements Runnable {
private Object object;
public SynchronizedTest(Object object) {
this.object = object;
}
public static int count = 0;
@Override
public void run() {
synchronized (object) {
for (int i = 1; i <= 1000000; i++) {
add();
}
}
}
public static void add() {
++count;
}
public static void main(String[] args) throws Exception {
Object object = new Object();
SynchronizedTest firstLock = new SynchronizedTest(object);
SynchronizedTest secondLock = new SynchronizedTest(object);
Thread t1 = new Thread(firstLock);
Thread t2 = new Thread(secondLock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);//= 20000000
}
}
错误案例:
public class SynchronizedTest implements Runnable {
private Object object;
public SynchronizedTest(Object object) {
this.object = object;
}
public static int count = 0;
@Override
public void run() {
synchronized (object) {
for (int i = 1; i <= 1000000; i++) {
add();
}
}
}
public static void add() {
++count;
}
public static void main(String[] args) throws Exception {
Object object1 = new Object();
Object object2 = new Object();
SynchronizedTest firstLock = new SynchronizedTest(object1);
SynchronizedTest secondLock = new SynchronizedTest(object2);
Thread t1 = new Thread(firstLock);
Thread t2 = new Thread(secondLock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);//小于 20000000
}
}
代码块使用加Synchronized的时候,使用同一把锁,其他的线程就必须等待,这样也就保证了每次只有一个线程执行被同步的代码块。不是同一把同锁,无法达到共享资源同步结果
四、synchronized底层原理
在java虚拟机中的同步是基于进入和退出管程(Monitor)对象实现的,同步有显示同步(有明确的monitorenter和moniterexit)和隐士同步(ACC_SYNCHRONIZED)
1、代码块底层原理
下面是一段被synchronized修饰 的同步代码块,在代码块中操作共享变量:
public class SynchronizedBlockTest {
public int j = 0;
public void excuteSynTask() {
synchronized (this) {
j++;
}
}
}
通过javap -c反编译出来如下:
Compiled from"SynchronizedBlockTest.java"
public class com.example.thread.SynchroniedTest.SynchronizedBlockTest{
public int j;
public com.example.thread.SynchroniedTest.SynchronizedBlockTest();
Code:
0:aload_0
1:invokespecial #1 // Method java/lang/Object."":()V
4:aload_0
5:iconst_0
6:putfield #2 // Field j:I
9:return
public void excuteSynTask();
Code:
0:aload_0
1:dup
2:astore_1
3:monitorenter //进入监视器(进入同步方法)
4:aload_0
5:dup
6:getfield #2 // Field j:I
9:iconst_1
10:iadd
11:putfield #2 // Field j:I
14:aload_1
15:monitorexit //退出监视器(退出同步方法)
16:goto 24
19:astore_2
20:aload_1
21:monitorexit //退出同步方法
22:aload_2
23:athrow
24:return
Exception table:
from to target type
4 16 19any
19 22 19any
}
从上面字节码可以看出同步代码块实现使用的monitorenter 和monnitorexit指令,其中monitorenter指令指向同步代码块起始的位置,monitorexit指令则指明同步代码块结束的位置,当执行monitorenter指令的时候,当前线程将试图获取objectref(即对象锁)所对应的moniter持有权,当objectref的monitor进入计数器为 0,那么此线程就可以成功取得monitor,并且将计数器的值设置为1,获取锁成功。如果当前线程已经拥有objectref的monitor的特有权,那么它可以重入这个monitor,重入时的计算器值也会被执行,执行线程将释放monitor(锁)并设置计数器值为0,其他线程将有机会持有monitor,值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
2、synchronized方法底层原理
方法级别的同步是隐士的,即无需通过字节码指令来控制。JVM可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志来区分一个方法是否是同步方法.当方法调用的时候,调用指令将会检查方法中的ACC_SYNCHRONIZED 访问是否被标志了,如果设置了,执行线程将会先持有monitor,然后在执行方法,最后在方法完成的时候释放monitor。在方法执行的期间,执行线程持有monitor,其他任何线程无法在获得同一个monitor。如果线程在执行同步方法的时候抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法锁持有的monitor将在异常抛到同步方法之外时将会自动释放。
public com.example.thread.SynchroniedTest.SynchronizedBlockTest();
Code:
0:aload_0
1:invokespecial #1 // Method java/lang/Object."":()V
4:aload_0
5:iconst_0
6:putfield #2 // Field j:I
9:return
public synchronized void excuteSynTask();
flags:ACC_PUBLIC,ACC_SYNCHRONIZED
Code:
0:aload_0
1:dup
2:getfield #2 // Field j:I
5:iconst_1
6:iadd
7:putfield #2 // Field j:I
10:return
从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的 确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该 ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在java早期版本中,synchronized是属于重量级锁,效率低下,因为监视器是依赖于底层的操作系统来实现的,但是在操作系统实现线程之间的切换时需要从用户态转换到核心态,这个转态之间的转换是需要相对比较长的时间,时间成本相对比较高,这个也是早期版本中synchronized效率低下的原因。然而在java6之后官方从JVM层面对synchronized有了较大的优化,所以现在的synchonize锁效率也优化了很多。在java6之后,为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁,接下来Java官方在JVM层面synchronized锁的优化。
3、JAVA虚拟机对synchronized的优化
锁的状态总共优化总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,在升级到重量级锁,但是锁的升级只能是单向的,也就是说只能是低到高升级,不会出现锁的降级,关于重量级锁,前面已经讲述过了,下面介绍的剩下几种锁,
偏向锁
偏向锁是在java6之后才加入的新锁,他是针对加锁操作的优化手段,在大多数情况下,锁不仅存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入的偏向锁,偏向锁的核心思想是,如果一个线程获得了锁,那么就进入了偏向模式,此时Mark word 的结构也就变为了偏向锁结构,当这个线程再次请求锁的时候,就不需要再做任何同步操作了,即获取锁的过程,就省去了大量的有关锁的申请操作了,从而就提高了程序的性能。所以,对于没有锁的竞争的场合,偏向锁有很好的优化效果,毕竟这样既有可能连续多次是同一个线程重复的去申请相同的锁,但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样的场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
假如偏向锁失败,虚拟机并不会立即升级为重量级锁,他还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word的结构也会变为轻量级锁结构,轻量级锁能够提升程序的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”。
自旋锁
当上面的轻量级锁失败之后,虚拟机为了避免线程真实的在操作系统层面上被挂起,这个时候可以利用自旋锁的优化手段。在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间转换需要从用户态转化为核心态,这个状态之间的转化需要较长的时间,时间的成本是想多比较高的,因此自旋锁会假设在不久的将来当前的线程就可以获得锁,因此虚拟机会让当前想要获取的线程做几个空的循环,一般不会太久,在经过一段时间的循环之后,如果得到了锁,就可以进入到了临界区。如果还不能获取到锁,那么就会将线程在操作系统层面上挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的,最后没有办法就只能升级为重量级锁。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更加的彻底,在java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次执行时,也叫及时编译 )
通过对运行上下的扫描,去除不可能存在资源竞争的锁,同过这种方式消除没有必要的锁,可以节省毫无意义的请求锁的时间,对于StringBuffer的append方法是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其余的线程锁使用,因此StringBuffer不可能存在资源竞争的背景,JVM会自动将其锁消除。
public class StringBufferRemoveLock {
public void add(String str, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveLock stringBufferRemoveLock = new StringBufferRemoveLock();
for (int i = 0; i < 10000; i++) {
stringBufferRemoveLock.add("abc", "" + i);
}
}
}
}
4、线程中断与synchronized
中断锁表达的意思是,在线程运行期间大断他,在java中,提供了下面三个有关线程中断的方法:
在讲解中断与Synchronized时先了解下面interrupt isInterrupted,interrupted方法的基本用法:
/ 中断线程(此线程并不一定是当前线程,而是指调用该方法的Thread实例所代表的线程)这个其实只是给线程打了一个中断标志,线程任然会继续运行
Thread.interrupt();
// 判断线程是否被中断,并不会清除中断状态
Thread.isInterrupt();
//表示的线程是否被中断并清除当前线程的中断状态
Thread.interrupted();
案例分析
从结果可以看出在调用了interrupt()方法的后,线程仍然在继续运行,并未停止,但是这个时候已经给线程设置了中断标志,两个isInterrupt方法都会的输出true;
对上面的案例在做一下变动:
上面可以看出第一次调用isInterrupted方法的时候返回结果是true
第一次和第二次调用interrupted方法返回结果是false 这个和之前解释interrupted不一致,应该一个是true一个是false才正确。 这其实有一个坑,interrputed方方法测试的是当前线程是否被中断,注意是当前线程!!
上面的当前线程其实就是main线程,而mythread.interrupt()调用的是mythread线程。所以这里调用mythread.interrupted()其调用的是main.interrupted
interrupted源码分析:
从源码中可以看出调用的是currentThread线程,true表示的是是否清除中断标志
请看下面的案例:
实际结果和预料之中是一致的。
注意:
若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。
例如,线程通过wait(),sleep()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
中断与synchronized
public class SynchronizedBlock implements Runnable {
public synchronized void test() {
System.out.println("开始调用test()方法");
while (true) // Never releases lock
Thread.yield(); //将执行权交给其他线程执行
}
public SynchronizedBlock() {
new Thread(new Runnable() {
@Override
public void run() {
test();
}
}).start();
}
@Override
public void run() {
while (true) {
System.out.println("开始执行----");
if (Thread.interrupted()) {
System.out.println("中断线程");
} else {
test();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlock synchronizedBlock = new SynchronizedBlock();
Thread thread = new Thread(synchronizedBlock);
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}
}
运行结果:
开始调用test()方法
开始执行----
通过上面的结果发现 控制台输出结果并没有:中断线程,因为interrupt对Synchronized修饰的方法,代码块不会响应中断。但是后文lock锁 对interrupt可以响应中断。
Lock
在jdk1.5 以后,增加了Juc并发包且提供了Lock接口用来实现锁的功能,他除了与synchronized关键字类似的同步功能,还提供了比synchronized更加灵活的实现。多线程下可以精确控制线程,且在jdk1.5 并发效率比synchornized更高的并发率,但是这也会带来一些缺点,下文将一步步去分析。
Lock本质上是一个接口(位于源码包中的java.util.concurrentLocks中)它包含以下几个方法
//尝试获取锁,获取锁成功则立马返回,否则阻塞当前线程
void lock();
//尝试获取锁,线程在成功获取锁之后被中断。则放弃获取锁并且抛出异常
Void lockInterruptibly() throws InterruptException;
//尝试获取锁,获取成成功之后,就返回true,否则就返回false
Boolean tryLock();
//尝试获取锁,若在规定的时间内获取到锁,立马就返回true,否则就返回false,未获取到锁之前被中断就抛出异常
Boolean tryLock(long time,TimeUnit unit) throws InterruptException;
//释放锁
Void unlock();
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait()a的功能,一个锁可以有多个条件变量
Condition newCondition();
Lock里面有三个实现类,第一个是ReentrantLock ,另外2个是ReentrantReadWriteLock类中ReadLock和WriteLock。
使用方法:多线程下访问共享之源时,访问前需要加上锁,访问结束的时候在解开锁,解锁条件代码必须放在finally中,不然会出现死锁
ReentLock lock = new ReentLock();
lock.lock();
try {
} finally {
lock.lock();
}
注意:加锁必须位于对资源访问的try外部,特别是使用lockInterruptibly方法锁的时候就必须这样子去做,这是为了防止线程在获取锁的时候被中断了,不需要也没有必要去释放锁。
ReentrantLock reentrantLock = new ReentrantLock();
try {
reentrantLock.lockInterruptibly();
try {
//access the resource protected by this lock
} finally {
reentrantLock.unlock();
}
} catch (InterruptedException exception) {
}
ReentLock内部源码分析
概念
ReentLock 是基于AbstractQueendSynchronized来实现的,所以在了解ReentLock之前先简单的说一下AQS
我们最熟悉的同步锁应该就是synchronized(上文已经对其做了详细的介绍)
他是通过底层的monitorEnter 和monitorExit和ACC_SYNCHRONIZED来实现锁的获取和释放的
这里介绍的AbstractQueenSynchronized 同步器(AQS),是基与FIFO队列来实现的,通过state的状态,来实现acquire和release;state为0的表示该锁还没有被任何线程获取可以获取锁;state为1表示已经有线程已经获取了锁。
源码分析AQS
AQS是基于FIFO队列实现的,所以队列必然是有一个个节点组成的,下面从节点开始讲解:
//waitstatus 有下面几个状态
/**
- CANCELLED =1 表示当前的线程被取消了
- SIGNAL =-1 表示当前节点的后继节点阻塞了,需要被唤醒
- CONDITION =-2 表示当前节点在等待Condition ,因为某个节点条件被阻塞
- PROPAGATE=-3 表示锁的下一次获取可以无条件的传播
*/
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
介绍Node
//头节点
Private tansient volatile Node head;
//尾结点
Private transient volatile Node tail;
//同步状态
Private volatile int state;
*上面已经介绍完AQS几个重要的成员,下面开始通过一个demo,来分析ReentLock底层实习原理
public class ReentLockTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
final ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 2; i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
lock.lock(); // 第一步
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.getStackTrace();
} finally {
lock.unlock();
}
}
};
executorService.submit(runnable);
}
executorService.shutdown();
}}
从第一步开始查看lock方法
public void lock() {
sync.lock();
}
ReentLock中有一个抽象类Sync ,它继承了AQS,所以RenntLock的实现也是基于Sync来完成实现的,NonfairSync(非公平锁)和FairSync公平锁继承了Sync。
公平锁:获取锁是有顺序的先来先到
*非公平锁:每个线程抢占锁的顺序是不固定的(不能说是随机的,在读写锁源码中可以看到)
RenntLock 我们平时用的最多的是非公平锁(并发率高),公平锁效率相比较低,下面介绍Nofair的lock方法
final void lock () {
//通过cas将AQS中state变量设置1
if (compareAndSetState(0, 1))
//等cas操作成功之后,将当前线程设置成排它线程,后面的线程无法在获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
前面的demo中启动了2个线程,假设A线程进来之后,调用compareAndSetState(0,1)方法成功,此时由于没有任何线程占有锁所以通过原子操作CAS,将state的状态由0修改为1,之后将该线程设置到AQS变量exclusiveOwnerThread中。此时线程B进入了lock方法,通过CAS操作,发现AQS中的state变量已经变成了1,设置失败于是就进入了else中的acquire方法。
也就是说非公平锁在线程第一次失败之后,会调用acquire方法进入队列中,然而公平锁是直接调用acquire方法
acquire方法调用父类AQS中的acuqire方法,源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//static final Node EXCLUSIVE = null;
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
下面看tryAcquire(arg)方法:
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程(B线程)
final Thread current = Thread.currentThread();
//获取当前AQS中标志位state变量值, 0 表示没有被任何线程占有;1: 表示当前锁已经被某个线程拿走了
int c = getState();
if (c == 0) {
//表示锁还没有任何线程拿走,当前线程可以去获取
if (compareAndSetState(0, acquires)) {
//当前线程获取锁成功,将AQS中exclusiveOwnerThread设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
} //如果是当前线程等于AQS中exclusiveOwnerThread线程表示的是当前线程已经获取锁,本次获取锁是重入
else if (current == getExclusiveOwnerThread()) {
//获取AQS中的变量,并加acquires(可以自由指定,默认是1)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
从上面可以看出如果线程B尝试获取锁失败之后,会调用acquireQueued方法,调用acquireQueued之前会先调用addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //1
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
方法参数中的mode是Node节点类型,在这里表示独占的锁,用当前线程在构造一个Node对象,线程B进来之后,此时队列里面是空的,所以尾节点tail为空,走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;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上面enq方法是一个循环,当前线程B进来之后将tail(此时可能为空)赋值给t变量,在判断t是否等于null(这里判断null是因为多线程,这时t也有可能不为空,目前这里只有A、B线程,不存在这种情况暂时不考虑)接下来调用compareAndSetHead,成功,head指向tail,如果失败 就重复尝试,直到成功为止,当head成功之后,进入else逻辑中,将后续的Node加入到队尾,如果compareAndSetTail 失败的话,通过for循环继续插入到队尾,最终所有没有成功获取到锁的线程全部加入到队列中。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
首先会判断node前驱节点是否为head,如果是证明当前线程就是下一个即将获取锁的线程,所以此时先尝试在在调用了tryAuquire方法,如果获取到了锁,那么就将之前的node(之前为head的node)设置为空,gc回收。
如果不是head或者获取失败之后,那么久调用shouldParkAfterFailedAcquire方法
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 {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
该方法是判断这个节点是否需要挂起,在介绍这个方法之前先看一下前面讲的Node节点中除了当前线程,前驱节点,后去节点,还有一个重要的变量waitstatus 这个变量是来表示当前node是否需要竞争锁,某些情况下,有些线程是会放弃锁的竞争的,比如condition
也就是说只有当node状态为SIGNAL情况下,当前节点才会被挂起,,假设当前的节点的WaitStatus就是SIGNAL,则调用parkAndCheckInterrupt方法,此时线程才被真正的挂起。
在上面调用acquireQueued方法,线程被挂起之后,还是处在for循环中,所以当线程被唤醒的时候,会继续执行,此时tryAcquire成功后,获取锁,之后将节点删除,这样获取锁的Node队列就没有了。
后面继续读写锁分析以及总结。。。。
清风不问烟雨