Java-多线程并发之锁原理

LockSupport工具类:

主要用于挂起和唤醒线程,是创建锁和其他同步类的基础。

每个使用LockSupport的线程会和他关联一个许可证。

主要功能由Unsafe类实现:

1.park():如果已经拿到了许可证,就返回。没有拿到就阻塞。不会抛异常。

2.unpark(Thread thread):1) 无条件下调用,如果传入的线程没有许可证,就给许可证。  2) 如果线程调用了park被挂起就被唤醒。 

              最好对park进行判断,因为这个方法不会抛异常。也不会告诉你为什么park被返回了。

3.parkNanos(long nanos):与park类似。 拿到许可证就返回,如果没有拿到就在nanos时间后自动返回。

4.park(Object blocker):当调用park被挂起时会记录到线程内部。用诊断工具getBlock(Thread),可以把block设置成this。就可以获取有关阻塞的信息。

 

 Thread类里边有个变量  volatile Object  parkBlocker.  就是用来存放blocker对象的

5.parkNanos(Object blocker,long nanos)

相比park(Object blocker)多了一个超时的时间

6.parkUntil(Object blocker,long deadline) 这个deadline是一个随机的时间。

 

抽象同步队列AQS:AbstractQueueSynchronizer,同步器的基础组件,锁的底层结构。

 

 

 

AQS的结构:

1.AQS是一个FIFO(first in first out)的双向队列,有头尾指针(head,tail)。在AQS的类中有一个静态内部类Node。Node组成了AQS。

Node中:

thread变量:存放进入AQS队列的线程

exclusive:用来标记该线程是获取独占资源时被挂起放入AQS队列的。

shared:用来标记该线程时获取共享资源被阻塞挂起放入AQS队列的。

waitStatus:记录当前线程等待的状态(cancel线程被取消,signal需要被唤醒,condition在条件队列,propagate释放共享资源需要通知其他节点)

pre:前驱节点

next:后继节点

state:状态信息。可以通过get,set,CAS进行操作。state在不同的锁中有不同的涵义。是一个灵活的int变量。 

 

 

ConditionObject内部类:结合锁实现线程的同步。可以直接访问AQS对象内部的变量,如state或者队列。  

ConditionObject是条件变量,每个条件变量对应一个条件队列(单链表),用来存放调用条件变量的await方法后被阻塞的线程。

 

2.AQS的线程同步的关键是对state进行操作。根据state是否属于一个线程,操作state的方式分为独占和共享:

独占:acquire(int args)   acquireInterruptibly(int args) release(int args)

共享:acquireShared  acquireSharedInterruptibly  releaseShared

1)独占方式获取资源:与具体线程绑定,其他线程尝试去操作state获取资源会获取失败阻塞。

比如独占锁ReentrantLock,线程获取到独占锁后,通过CAS把state从0->1,然后设置当前独占锁的持有者为currentThread。

当该线程再次去获取锁,会把状态值从1变成2,代表可重入次数。

当有其他线程去获取锁的时候发现该锁的持有者不是自己,就会被放入AQS阻塞队列挂起。

 

2)共享方式获取资源:不与具体线程绑定,通过CAS去竞争资源,如果一个线程获得资源后其他线程再去获取资源还能满足需要,CAS即可。

比如Semaphore信号量。当一个线程通过acquire获取信号量,看信号量个数是否满足,不满足就进阻塞队列,满足就通过CAS自旋获取。

 

3.对于独占锁:

acquire。 release.....

对于共享锁:

acquireShared  releaseShared...

在acquire方法中通过tryAcquire设置state的值,失败则把Node封装

为独占(EXCLSIVE)的属性,插入到AQS阻塞队列的尾部,并调用LockSupport.park方法挂起自己。
 
release方法中调用tryRelease方法释放资源,在这里是设置状态变量state的值。然后调用unpark方法激活AQS阻塞队列中的一个线程
被激活的线程通过tryAcquire尝试,看当前的state值是否满足自己的需要。如果满足则被激活,不满足扔要挂起到阻塞队列尾部
 
4.需要根据各种锁去定义的除了tryAcquire和tryRelease之外还有isHeldExclusively,用来判断锁是否是独享锁。
 
5.关于Interruptibly是用来表明其他线程是否可以进行中断抛出中断异常。
 
6.Enq的入队操作:

 

 

 

入队操作和双向链表队尾添加元素的操作一致。需要判断是否是初始化添加。 

 

AQS条件变量的支持:

Synchronized的wait和notify对应

AQS中的signal和await操作。不同的是Synchronized同时只能与一个共享变量的notify和wait同步。而AQS的一个锁可以对应多个条件变量。

在使用signal和await之前仍然需要获取条件变量对应的锁。

 

lock.newCondition()的作用就是创建一个锁对应的条件变量ConditionObject。

获取独占锁。lock.lock();相当于进入Synchronized代码块(即获取共享变量的内部锁)

                     lock.unlock()相当于退出Synchronized代码块

代码4调用了条件变量condition.await()阻塞挂起了当前的线程。就相当于调用共享变量的wait()方法。

调用condition.signal()就相当于调用了共享变量的notify()方法。

每个条件变量都维护一个条件队列,用来存放被await阻塞挂起的线程

 

await():

在内部构造一个Node.Condition的Node节点,然后将该节点插入条件队列的队尾,修改state值以释放锁,阻塞挂起。如果有其他的现在再进行lock.lock获取锁并使用await,循环步骤。

signal():

把条件队列的队头的线程节点移除队列加入AQS阻塞队列,然后激活这个线程。

 AQS不提供newCondition函数,需要继承的子类去重写。
当多个线程去用lock.lock()获取锁的时候,只有一个线程获取到了锁,其他线程在AQS的阻塞队列中自旋CAS操作。
如果获取到锁的线程调用await方法,则会释放掉lock获取的锁,然后进入到
 
AQS实现自定义同步器
 
独占锁ReentrantLock:

 

 

Sync实现lock。

并根据RenntrantLock的参数是否是fair选择公平和非公平的Sync同步,默认是非公平的

 

实现AQS来实现,根据Syn参数来决定是公平还是非公平的锁,默认是非公平锁。
AQS的state表示锁的可重入次数。
lock():获取锁
条件:①锁没有被其他线程占有         ②当前线程身上没有其他锁
AQS  state+1
如果线程之前获取过该锁,state+1
否则:线程放入AQS阻塞队列。
 
 
NofairSync:

 

 1.CAS将0->1 说明获取到了锁。

2.setExclusiveOwnerThread.设置该锁的持有线程

 

重写Acquire:

 

 unlock就是先对线程进行一系列的判断。然后通过CAS把state-1,再判断减完是否为0去释放锁。

 

如果三个线程同时去获取独占锁ReentrantLock,加入线程1争取到了锁,那么剩下的2、3线程就进AQS阻塞队列中等待。

如果线程1获取到锁的情况下,调用了条件变量,例如条件变量用了await,这时就需要1释放锁,然后AQS阻塞队列中的2、3线程根据是否是公平的策略去获取锁。

如果是公平的情况下,通过底层的算法判断是否有前驱节点在他进来之前就在等待这个锁,如果有的话就先给他。

如果是非公平的策略下,就谁后进来谁先获取,抢占式获取。

这时候,线程1由于调用await被阻塞挂起,进入那个条件变量的条件阻塞队列中等待,条件变量1调用signal唤醒他。这就是抢占式的锁ReentrantLock.

 

读写锁RenntrantReadWriteLock:

写锁:

当其他线程没有读锁、写锁的时候。可以获取写锁。实现的方法主要是通过判断AQS的satate的值。

state的低16位和高16位分别代表写锁和读锁。

 

读锁:

首先要判断写锁是否被占用,如果写锁被占用不能获取读锁。如果没有其他线程获取写锁,读锁可以被占用。

读锁是共享锁。

如果一个线程有读锁和写锁(先写后读),要在释放读锁时候把写锁一起释放掉。

Lock过程:

SharedCount():持有读锁的线程数

readerShouldBlock():当前有别的线程在尝试获取锁的时候是否需要阻塞

cacheHoldCounter():记录最后一个获取到读锁的线程和该线程的锁可重入数

 

判断写锁是否被占用,如果占用阻塞>>>>>>获取持有读锁的线数>>>>>>>判断是否超出最大值,调用readerShouldBlock判断是否应该阻塞,是否可以CAS成功,如果都成功,获取读锁成功。

参数设置更新>>>>>>没有获取到读锁的自旋等待。

参数更新:记录第一个和最后一个线程,和该线程获取读锁可重入次数

释放锁的过程不再赘述。

 

小结:

线程进入读锁的前提条件:

没有其他线程的写锁,

没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

没有其他线程的读锁

没有其他线程的写锁

而读写锁有以下三个重要的特性:

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

 

 

JDK8新增:StampedLock

不直接实现Lock和ReadWriteLock接口,内部自己维护一个双向阻塞队列。

读写都不可重入,在多读的情况下,乐观读的效率会提高很多,因为不进行CAS,而是简单的测试状态。

在获取锁时候都返回一个stamp状态值,在释放锁和转换锁的时候需要传入获取时返回的stamp。

 三种模式:

独占写锁:不可重入的独占写。

悲观读:在没有独占写的情况下,多个线程可以同时获取该锁。与ReentrantReadWriteLock类似。

乐观读:最大的不同是操作数据前不进行CAS。  获取stamp后需要进行validate有效性验证判断,判断是否有其他线程持有了写锁。

    操作的数据是方法栈里面的数据,是一个快照。进行validate验证及时止损。如果validate发现其他线程也持有了读锁,要么重试要么切换成悲观锁。

posted @ 2021-04-07 22:41  NobodyHero  阅读(345)  评论(0编辑  收藏  举报