六、Java 锁机制
1、悲观锁和乐观锁
(1)、悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
(2)、乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
2、自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用CAS循环的方式去尝试获取锁,这样的好处是减少上下文切换的消耗,缺点是循环会消耗CPU。
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁方法
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "线程已进入lock方法”);
int i = 1;
//比较替换成功跳出循环
while (!atomicReference.compareAndSet(null, thread)){
i++;
}
System.out.println(thread.getName() + "线程第"+ i +"次加锁成功");
}
//解锁方法
public void unLock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + "线程解锁成功");
}
/**
* T1线程先获取锁,持有5秒钟,T2线程进来后通过compareAndSet操作失败,说明有线程持有该锁,通过while自旋等待。
**/
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
try{ TimeUnit.SECONDS.sleep(5); }catch(Exception e){e.printStackTrace();}
spinLockDemo.unLock();
}, "T1").start();
try{ TimeUnit.SECONDS.sleep(1); }catch(Exception e){e.printStackTrace();}
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "T2").start();
}
}
3、可重入锁/递归锁
可重入锁(递归锁)指的是同一线程外层函数获得锁之后 ,内层递归函数会自动获取锁,不会因为之前已经获取过还没释放而阻塞,并且使用的是同一把锁。可重入锁最大的作用是避免死锁。
(1)、隐式锁
Synchronized关键字使用的锁称为隐式锁,Synchronized也是内置锁或重量级锁,是可重入锁。
(2)、显式锁
需要显式释放的锁称为显式锁(即Lock)也有ReentrantLock这样的,ReentrantLock是轻量级锁,是可重入锁。
(3)、范例
如果T1线程在methodA()获取锁之后,到methodB()还需要获取锁,此时methodB()要获取的锁被T2线程获取了,T2在等待methodA()的锁,这样就会造成死锁的情况。所以T1线程在methodA()获取锁之后,进入methodB()方法会自动获取锁,并且使用的是同一把锁,这样就可以避免死锁,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
public synchronized methodA(){
methodB();
}
public synchronized methodB(){
......
}
4、Synchronized关键字
Synchronized是Java的一个关键字,也就是Java语言内置的特性,是在JVM层面上实现的。如果一个代码块被Synchronized修饰了,当一个线程获取了对应的锁,执行代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁。关键字Synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时Synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
(1)、修饰代码块
public class Test {
Test test = new Test();
public void run(){
//实现方式一
synchronized(test){
...
}
//实现方式二
synchronized(this){
...
}
//实现方式三
synchronized(Test.class){
...
}
}
}
①、作用范围:大括号{}括起来的代码
②、作用对象:调用这个代码块的对象实例
③、一个线程获取了该对象实例的锁之后,其他线程来访问该实例的相同synchronized代码块或其他synchronized代码块都需要阻塞,访问该实例的非synchronized代码不阻塞。
④、当多个线程分别作用于不同的对象实例,则获得的是不同对象实例的锁,所以线程之间互相并不影响。
(2)、修饰方法
①、作用范围:整个方法
②、作用对象:调用这个方法的对象实例
③、一个线程获取了该对象实例的锁之后,其他线程来访问该实例的相同synchronized方法或其他synchronized方法都需要阻塞,访问该实例的非synchronized方法不阻塞。
④、当多个线程分别作用于不同的对象实例,则获得的是不同对象实例的锁,所以线程之间互相并不影响。
(3)、修饰静态方法
①、作用范围:整个静态方法
②、作用对象:调用静态方法的类,也就是该类创建的所有对象实例
③、静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。
(4)、修饰类
①、作用范围:整个类,synchronized后面括号括起来的部分
②、作用对象:这个类创建的所有对象实例
(5)、导致线程安全原因
①、存在共享数据;
②、多线程共同操作共享数据;
(6)、获取锁的线程释放锁的三种情况
①、获取锁的线程执行完该代码块,然后线程释放对锁的占有;
②、线程执行发生异常,此时JVM会让线程自动释放锁;
③、调用wait方法,在等待的时候立即释放锁,方便其他的线程使用锁。
(7)、实现同步
Synchronized关键字配合Object的wait()、notify()系列方法实现同步,可以实现等待/通知模式。
5、Synchronized原理/锁升级
(1)、Java对象内存布局
对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。
①、对象头(Header)
对象头是实现synchronized锁对象的基础,主要由MarkWord和Klass Point(类型指针)组成。
②、实例变量
存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
③、对齐填充
由于JVM要求对象的大小必须是8字节的整数倍,对象头已经满足,则当对象的实例数据部分没有对齐时,需要对齐填充来补全。
(2)、对象头(Header)-> 3字宽
1字宽 = 4 Byte = 32 bit
①、Mark Word -> 1字宽
存储对象哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
②、Klass Point(Class Metadata Address)-> 1字宽
Klass Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
③、Array Length -> 1字宽
如果当前对象是数组,存储数组长度。
④、Hotspot 32位JVM中MarkWord存储格式
锁状态 | 25bit | 4bit | 1bit (是否为偏向锁) | 2bit (锁标志位) | |
23bit | 2bit | ||||
无锁 | 对象hashCode | 对象分代年龄 | 0 | 01 | |
偏向锁 | threadId(锁偏向的线程ID) | Epoch | 对象分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
⑤、Hotspot 64位JVM中MarkWord存储格式
锁状态 | 56bit | 1bit | 4bit | 1bit (是否为偏向锁) | 2bit (锁标志位) | |
25bit | 31bit | |||||
无锁 | unused | 对象hashCode | Cms_free | 对象分代年龄 | 0 | 01 |
偏向锁 | threadId(锁偏向的线程ID)(54bit) | Epoch(2bit) | Cms_free | 对象分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||
重量级锁 | 指向重量级锁的指针 | 10 | ||||
GC标记 | 空 | 11 |
注:分代年龄也就是GC在from和to之前最多复制15次,因为在对象头中次数是存储4位,也就是最大是16,也就是从0-15。
(3)、锁升级
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁,并且升级过程是不可逆的。
(4)、无锁
MarkWord标志位01,没有线程执行同步方法或同步代码块时的状态。
(5)、偏向锁
①、偏向锁是通过CAS设置当前正在执行的ThreadID来实现的,以后该线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。假设线程A获取偏向锁执行同步代码块(即对象头设置了ThreadA_Id),线程A同步块未执行结束时,线程B通过CAS尝试设置ThreadB_Id会失败,因为存在锁竞争情况,这时候就需要升级为轻量级锁。
偏向锁是针对于不存在资源抢占情况时候使用的锁,如果被synchronized修饰的方法/代码块竞争线程多可以通过禁用偏向锁来减少一步锁升级过程。JDK1.6中默认是开启偏向锁,可以通过参数-XX:-UseBiasedLocking = false来禁用偏向锁。
②、偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
(6)、轻量级锁(自旋锁)
轻量级锁是采用自旋锁的方式来实现的,自旋锁分为固定次数自旋锁和自适应自旋锁。
①、在ThreadA进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在ThreadA的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
②、拷贝对象头中的Mark Word复制到ThreadA的锁记录中。
③、拷贝成功后,JVM将使用CAS操作尝试将对象的Mark Word更新为指向ThreadA锁记录的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
④、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
⑤、如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。
⑥、如果ThreadB进来更新失败且对象的Mark Word没有指向当前线程的栈帧,说明多个线程竞争锁,会继续自旋。ThreadB自旋到一定次数(固定次数/自适应),或者ThreadC进来发现ThreadB自旋等待,轻量级锁就要膨胀为重量级锁。锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
(7)、重量级锁
轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被称为互斥锁。
(8)、锁的优缺点对比
6、Lock接口
Lock是java.util.concurrent.locks包里的一个接口,ReentrantLock是Lock的唯一实现类。
(1)、方法定义
①、void lock();
获取锁。
②、void lockInterruptibly() throws InterruptedException;
获取可中断的锁。例如使用ReentrantLock获取锁,如果获取了锁立即返回,如果没有获取锁,当前线程处于休眠状态等待获取锁,因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程,所以当前线程能够响应中断,即中断当前线程的等待状态去做其他的事情。但是如果是synchronized的话,如果没有获取到锁,则会一直等待下去。
例如,当两个线程A和B同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
③、boolean tryLock();
如果获取了锁立即返回true,如果别的线程正持有,立即返回false,不会等待。
④、boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
如果获取了锁立即返回true,如果别的线程正持有锁,会等待参数给的时间。在等待的过程中,如果获取锁,则返回true,如果等待超时,返回false。
⑤、void unlock();
释放锁。
⑥、Condition newCondition();
返回绑定到此Lock的新的Condition实例,用于线程间的协作。
lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。
(2)、Synchronized和Lock的区别
①、Synchronized是关键字,是Java语言内置的,属于JVM层面。Lock是具体类,是api层面的锁。
②、Synchronized不需要用户手动释放锁,当Synchronized代码块执行完后系统会自动让线程释放对锁的占用。ReentrantLock需要用户手动是否锁,若没有主动释放锁,可能出现死锁现象。
③、Synchronized不可中断,除非抛出异常或者运行完成。ReentrantLock可中断,通过tryLock(long timeout, TimeUnit unit)方法设置超时时间,或者调用interrupt()方法。
④、Synchronized是非公平锁。ReentrantLock两者都可以,默认非公平锁,构造方法传入true则为公平锁。
⑤、Synchronized不可以绑定多个Condition,ReentrantLock可以绑定。
⑥、阻塞和解除阻塞不同。
a、Synchronized利用Object对象的wait()和notify()来实现该功能,并且wait()和notify()必须要在同步代码块或方法里成对出现,必须要先执行wait()再执行notify(),顺序不可颠倒。
b、Lock是通过Condition对象的await()和signal()来实现该功能,并且await()和signal()必须要在lock和unlock之间出现,否则报异常,必须要先wait()再notify(),顺序不可颠倒。
7、ReadWriteLock接口
ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。
(1)、ReadWriteLock接口方法只有两个
package java.util.concurrent.locks;
public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}
(2)、ReentrantReadWriteLock实现类
①、ReentrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。ReentrantReadWriteLock底层基于AQS实现的,是共享锁。
②、读写锁机制
读-读不互斥
读-写互斥
写-写互斥
(3)、ReentrantReadWriteLock特性
①、获取顺序
a、非公平模式(默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。
b、公平模式
当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。
②、可重入
允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。
③、锁降级
允许写锁降低为读锁。
④、中断锁的获取
在读锁和写锁的获取过程中支持中断。
⑤、支持Condition
写锁提供Condition实现。
(4)、使用
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//写锁
rwLock.writeLock().lock();
rwLock.writeLock().unlock();
//读锁
rwLock.readLock().lock();
rwLock.readLock().unlock();
8、ReentrantLock实现类
ReentrantLock是基于AQS(AbstractQueuedSynchronizer)和LockSupport实现的,是接口Lock唯一的实现类,存在于java.util.concurrent.locks包里。ReentrantLock是独占锁。
(1)、公平锁与非公平锁的区别
//默认为false非公平锁
ReentrantLock nonFairLock = new ReentrantLock();
//true为公平锁
ReentrantLock fairLock = new ReentrantLock(true);
①、new ReentrantLock()默认是非公平锁,new ReentrantLock(true)认为是公平锁。Synchronized为非公平锁,因为相比公平锁,非公平锁性能更好。
②、ReentrantLock中的公平锁和非公平锁的区别,非公平锁在调用lock方法之后,首先就会调用CAS进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。非公平锁在CAS失败后,和公平锁一样都会进入到tryAcquire方法,在tryAcquire方法中,如果发现锁这个时候被释放了(state==0),非公平锁会直接CAS抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
③、如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
(2)、应用
ReentrantLock lock = new ReentrantLock();
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
//释放锁
lock.unlock();
}
使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
(3)、Condition对象
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//阻塞当前线程
condition.await();
//唤醒当前线程
condition.signal();
(4)、常用方法
①、公平锁tryAcquire()方法和非公平锁的tryAcquire方法的区别就是在获取完同步状态,判断该状态为0后,准备修改state值,抢占资源的时候,公平锁比非公平多了一个限制条件:hasQueuePredecessors()
hasQueuePredecessors()是公平锁加锁时判断等待队列中是否存在有效节点,也就是是不是需要进行入队排队操作。如果存在有效节点返回true,需要排队;如果不存在有效节点返回false,不需要排队。
(5)、源码解析
9、Condition接口
Condition是java.util.concurrent.locks包里的一个接口,依赖于Lock接口,意思就是说Condition对象需要通过Lock接口的newCondition()方法进行创建。
(1)、特性
Condition它更强大的地方在于能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。
如果采用Object类中的wait(),notify(),notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程,但是notifyAll无法区分唤醒的线程是读线程,还是写线程。但是,通过Condition,就能明确的指定唤醒读线程。
(2)、方法
①、void await() throws InterruptedException;
当前线程进入等待状态直到被通知(signal)或中断。
②、boolean await(long time, TimeUnit unit) throws InterruptedException;
当前线程进入等待状态直到被通知(signal)或中断或到达指定等待时间。
③、long awaitNanos(long nanosTimeout) throws InterruptedException;
当前线程进入等待状态直到被通知、中断或者超时。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
④、void awaitUninterruptibly();
当前线程进入等待状态直到被通知,该方法不响应中断。
⑤、boolean awaitUntil(Date deadline) throws InterruptedException;
当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false。
⑥、void signal();
唤醒一个在Condition上等待的线程,该线程从等待方法返回前必须获得与Condition相关联的锁。
⑦、void signalAll();
唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。
(3)、应用
int number = 0;
private Lock lock = new ReentrantLock;
//获取该锁的条件对象
private Condition condition = lock.newCondition();
public void increment() throws Exception{
//加锁
lock.lock();
try {
//注意!!!!!!多线程的判断要用while
while(number != 0){
//满足该条件的线程等待
condition.await();
}
//操作业务
number++;
//操作完成后,唤醒该条件的线程
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
10、LockSupport
LockSupport是一个线程阻塞工具类,可以让线程在任意位置阻塞,其所有的方法都是静态方法,通过类名直接调用。LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程最多只有一个permit,线程一开始默认没有permit。当调用park()方法时就会消费permit,如果没有就线程阻塞。当调用unpark(thread)方法就会给指定线程发放permit,线程继续执行。和信号量(Semaphores)不同的是,重复调用unpark()方法permit也不会累计,因为permit数量最多只有一个。
(1)、源码方法
①、LockSupport的底层和Atomic原子类一样是基于Unsafe类来实现。
package java.util.concurrent.locks;
public class LockSupport {
//底层调用Unsafe类的native方法
private static final sun.misc.Unsafe UNSAFE;
/**
* 当调用此方法时
* 如果有许可证,则会消耗掉这个许可证,然后正常退出;
* 如果没有许可证,就会阻塞当前线程,等待许可证。
*/
public static void park() {
UNSAFE.park(false, 0L);
}
/**
* 当调用此方法时,会增加一个凭证,但凭证对多只能有一个,累加无效。
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
}
②、Object的wait()、notify()和Condition的await()、signal()方法,都需要严格按照顺序先执行阻塞再执行唤醒,而park()、unpark()对顺序没有要求。
阻塞 | 解除阻塞 | 全部解除阻塞 | |
Object(Synchronized) | wait() | notify() | notifyAll() |
Condition(Lock) | await() | signal() | signalAll() |
LockSupport | park() | unpark() |
(2)、LockSupport应用
//线程T1
Thread thread = new Thread(() -> {
......
//消费permit
LockSupport.park();
......
}, "T1");
thread.start();
//线程T2
new Thread(() ->{
......
//为thread发放permit
LockSupport.unpark(thread);
......
}, "T2").start();
11、AbstractQueuedSynchronizer(AQS)
抽象队列同步器,它是多线程访问共享资源的同步器框架,Java中的ReentrantLock/ReentrantReadWriteLock/Semaphore/CountDownLatch等同步组件都依赖于它。
(1)、它维护了一个同步器状态(volatile int state代表共享资源)和一个线程等待队列(FIFO),通过该FIFO队列来完成资源获取线程的排队工作,将每条要去抢占资源的线程封装成一个个队列的Node节点来实现共享资源的分配。通过CAS、自旋以及LockSupport.park()的方式维护state变量的状态,使并发达到同步的控制效果。
(2)、CLH队列
AQS里面的CLH队列是CLH同步锁的一种变形。其主要从两方面进行了改造:节点的结构与节点等待机制。
①、在结构上,AQS类引入了头结点和尾节点,他们分别指向队列的头和尾,从addWaiter可以看到,CLH队列的入队实际上就是把当前节点设置为队尾,那么相应的,出队就是处理队首节点。
在Node类内,还有对前驱节点、后继节点的引用。前驱结点主要用在取消等待的场景:当前节点出队后,需要把后继结点连接到前驱结点后面;后继结点的作用是避免竞争,在doRelease方法中,当前节点出队后,会让自己的后继结点接替自己获取锁,有了明确的继承关系,就不会出现竞争了。
②、在等待机制上由原来的自旋改成阻塞+唤醒,以doAcquireInterruptibly为例,parkAndCheckInterrupt方法使用LockSupport令当前线程阻塞,直到收到信号被唤醒后,进入下一轮自旋。
(3)、Java中AQS框架应用
①、ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch
这四种都是通过静态内部抽象类直接继承AbstractQueuedSynchronizer抽象类,来实现各自同步组件的功能。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
//各自内部逻辑的实现
......
}
②、CyclicBarrier
CyclicBarrier内部是基于ReentrantLock和Condition来实现的,ReentrantLock同步器组件是通过AbstractQueuedSynchronizer来实现的。
(4)、常用方法
①、如果当前state的状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。
protected final boolean compareAndSetState(int expect, int update) {
//this:当前线程;stateOffset:state的内存地址偏移量,是volatile修饰。
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
②、设置当前拥有独占访问权限的线程。
此方法是AbstractOwnableSynchronizer类的,为AbstractQueuedSynchronizer的父类。
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
}
(5)、源码解析