转载请注明原创出处懂不东
支持和尊重原创,谢谢!
说明(废话一下)
这是自己写简书的第一篇文章,其实很早就有想法开始写,但是一直总觉得不到时候,因为总想以比较完美开始进行。但时不待人,最近想法上出现了一些变化,我觉得完美是经过不断探索,纠正和思考之后所精炼出来的。因此,想借助简书这个平台,让自己和文章不断地取得进步。写简书的目标有以下几点:
1.文章追求极简,系统和精细化知识体系,作为自己的思维映射,简单点,就是为了复习和记忆。
2.树立自己一套写文章最舒服的风格,着重原理上讲解。
3.希望自己写的东西能给有需要的人一点点帮助和借鉴吧。
最后,非常希望网友能够神吐槽,指出不足之处,自己不断思索和改进,万事开头难,不喜勿喷,谢谢!
前言
Java并发库是一个非常重要的知识点,在java1.5之前可能用synchroized关键字较多,但是在真正高并发系统里,对于锁的细粒化操作和灵活度上明显synchroized心有余而力不足,比如要保证读写一致synchroized一定对读写都加锁,在极端情况下,全为读读操作,这性能牺牲的就很不值得了。因此java1.5就引入了Lock,它是一个接口,指定了锁的基本操作,ReentrantLock是其中一个实现类,个人认为也是最重要的一个,因为它是看其他并发类的基础, ReentrantLock里面包装了Sync,继承自AbstractQueuedSynchronizer(AQS),AQS是一个抽象类,实现了很多基础方法,利用模板模式,让子类去能够很好的扩展和运用。该文章不是为了讲解ReentrantLock的基础知识或者源码解读,关于这部分知识,网上已经有很多优秀的文章了,可以自行查看。 下面对ReentrantLock的一些自己认为容易疑惑的点进行讲解,建议事先对 ReentrantLock的源码能够有所了解,看了可能会有点收获吧。
一。独占锁(排他锁)
独占锁(排他锁):多个线程并发请求获取锁,只能一个线程得到锁,其他线程放到双向不循环队列等待
T1,T2,T3是三个线程,同时来获取锁,只有T1获得到锁;T2和T3加入到双向队列中

二。双向队列与锁竞争
场景1.多个线程同时竞争锁,只有一个线程获得锁,其他线程存入双向队列中等待(第一点已经说明了,不多说)
下面的场景根据前提:
已知T1线程获取锁,T2,T3线程在等待队列中

场景2: T1线程释放锁了,在没有新的线程来竞争锁的情况下,队列中head节点的下一个节点T2直接获取锁,T2线程变成head节点,原来的head节点置空,被GC

场景3:T1线程释放锁了,此时新来线程T4,那么新的线程T4和head节点的下一个节点T2互相竞争锁,T4获取锁,队列结构不变(非公平锁)

场景4:T1线程释放锁了,此时新来线程T4和head节点的下一个节点T2互相竞争锁,T2获得锁,T4就要入队列尾部 (非公平锁)

场景5:T1线程释放锁了 ,此时新来的线程T4不跟head节点的下一个节点T2竞争锁,直接入队列,T2获得锁(不存在竞争,公平锁FIFO,如果队列为空(只有一个线程),则能直接获取锁,否则都要加入到队尾,按照队列顺序获得锁,这样就保证了公平性),图与场景4一样,只是内部原理不一样
三。可重入锁与State
可重入锁指的是,一个线程获取到锁之后,在没有进行释放锁,该线程还能进行再次获取锁。state是一个volatile类型,是一个同步状态位,在ReentrantLock可表现为获取锁的个数。ReentrantLock与synchroized都是可重入的。
重入锁的意义:为了避免造成死锁而设计的,同一个线程,同一个对象,用同一个锁,锁住多个方法,如果这些方法之间互相调用,如果锁不可重入,那么在进入一个锁方法后,再调用另外一个锁的方法,则会一直堵塞,造成死锁;如下举例说明
方法sync1与方法sync2用同一个锁锁住,使用同一个对象ReentrantLockTest 调用sync1方法里面又调用sync2方法,如果ReentrantLock是不可重入的,那么在执行sync1获得锁之后就不能再继续获取锁,调用sync2方法是遇到lock.lock()获取不了锁就会一直堵塞在那里等待sync1释放锁,sync1在一直等待sync2执行完毕,相互等待就造成死锁
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
public void sync1() {
try {
lock.lock();
sync2();
} finally { lock.unlock(); } }
public void sync2() {
try {
lock.lock();
System.out.println("sync2");
} finally { lock.unlock(); } }
四。自旋与堵塞挂起
自旋:指的是不断的重复循环,直到达到特定条件,则跳出循环;好处是能够避免上cpu上下文切换,不好的地方是不断自旋相对会耗cpu资源;随着线程数的增多性能降低;一般自旋代码执行时间较短,线程数少,可以用自旋,能够提高响应
堵塞挂起:堵塞挂起Cpu占有率会相对低,通过改变线程状态进行堵塞,要继续执行时需要被通知唤醒,在线程数运行比较多,竞争激烈的情况优势比较明显
ReentrantLock对应自旋和堵塞挂起的运用很多,以下通过例子说明:
场景:ReentrantLock通过自旋+堵塞挂起的方式进行锁的竞争,等待队列中的线程通过堵塞挂起的方式等待锁释放的通知信号,然后自旋的去再次竞争锁
伪代码如下:

五。等待队列线程状态
等待对列的每一个节点(Node)都有一个waitStatus属性,表示的该节点线程的线程状态,这个有什么用嘛?
1.用来标识线程类型,比如EXCLUSIVE状态,标识的该线程是一个独占线程,而PROPAGATE标识该线程是具有传播性,意思是当前节点获得锁,会传播到下一个节点,如果下一个节点也是PROPAGATE状态,就能够共享同一把锁。特别是在读写锁里就淋漓尽致体现出来了,这边稍微提一下,后续读写锁会详细讲解,简单画一个图理解
r表示读锁,共享锁,具备传播性,w表示写锁,是独占锁;那么假设另外一个线程释放锁,等待队列就有机会去获取锁了,
根据队列的顺序,前两个节点根据PROPAGATE传播属性,两个线程都能获得同一把锁,而w是EXCLUSIVE属性,一次性只能有一个线程有一把锁

2.有时候我们要对某一个线程进行中断取消,不用了,这个线程刚好在等待队列,那么为了让该线程不参与竞争锁,总得有个标识吧,这个标识就是CANCELLED,在新来一条的等待线程进入队列后,要挂起时,就会从后边向前遍历,将所有CANCELLED的线程剔除掉,排到正确节点后;另外还有CONDITION状态,这个是跟Condition相关的,以后文章再讲解
六。公平锁与非公平锁区别
非公平锁:当等待队列有存在线程,新来的线程也能够有机会获取锁,这就不是新来新得锁(FIFO),因此非公平
公平锁:当等待队列中存在线程,新来的线程没有机会去竞争锁,老实去排队尾去,获取锁按照队列顺序来,排队前的先得
注:当队列是空的,说明没有等着要获取锁的线程,那么新来的线程就是最优先的,能直接获取锁
关键性源码:
公平锁:

非公平锁:

公平锁与非公平锁在获取锁时,差别仅在于判断队列是否有线程等待获取锁,也就是hasQueuedPredecessors()方法
七。等待队列Head头节点
等待队列Head头节点表示的是当前获得锁的线程,当只有一条线程来获取锁,是不存在等待队列,因此是不存在head头节点;当并发时有多条线程,只有一条线程获得锁,另外的线程会进入到等待队列。这时候首先会新初始化一个head节点,表示的是已经获得锁的那条线程,然后才将等待的线程连接到head节点后面。所以为什么锁释放时,要从head节点的下一个节点取出进行竞争,如果获得锁,就将head节点置空,进行释放内存
八。exclusiveOwnerThread的意义
在Reentrantlock有一个 exclusiveOwnerThread属性表示的是当前持有锁的线程,为什么要专门设计这样一个属性,是为了方便定位持锁的线程,假设没有 exclusiveOwnerThread,将这属性放到队列中,还要去专门找,显得有些麻烦。其实这边更想表达的意思是,个人认为这是一种技巧,不单在这里,在我们写程序,或者hashmap源码,读写锁,经常设计到链表结构,队列的,会将头尾节点独立出来作为属性,好处在于能够快速定位和识别特殊节点,也有助于理解。
九。AQS的CAS操作
看一个CAS的API
protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}
CAS:其实就是一个乐观锁的设计思想,当并发时多个线程要对一个共享变量进行设置值,允许这些线程都去设值,但是有且只有一个线程设置是成功的,其他则失败的。典型的以空间换取时间的想法,一提到乐观锁,大家可能会想到version版本,在数据库设计时经常用到,比如电商中的库存减少为了保证多个线程一次性只能有一个线程减库存成功,伪代码如下
update product set version = version + 1,stock = stock -1 where version = ? and productId = ?
假设并发有两条线程执行上面的语句(条件版本号是一样的),那么就只有一条执行成功,版本加1了,另一条用执行不报错,但不会更改库存了。
CAS的原理是一样的,下面我用图来进行讲解
已知,内存中的值为0,此时期望值和更新值都不存在

这时有两条线程(T1,T2)来同时要对内存中的值进行修改,要将更新值设置到内存中,那么预期原值是什么呢,相当于上面sql中条件中的版本号,预期原值跟内存中的值进行比较,如果相同,则运行更新值把值更新给内存,两条线程同时要值更新给内存,如下

如果T1线程首先修改成功,那么就会变成内存中的值为1,此时T2要来改内存中的值,结果预期原值跟内存中的值不一样了,就更改不了。相当于预期原值此时要为1才有可能修改得了(相当于版本号已经从0升为1了,原理其实就是乐观锁)

网友评论