整个分析过程遵循:先了解结构,循序渐进,然后将各部分内容串起来。
通过lock接口一步一步引入AQS,了解AQS。
lock接口的出现原因:
·使用synchronized关键字将会隐式地获取锁,释放锁。整个行为都是隐式进行,不需要干预,所以不灵活
·获取到锁的线程没办法相应中断,线程被中断后抛出中断异常释放锁。
·没办法指定获取锁的超时时间
·..............................
SE5之后 java.util.concurrent.locks.Lock
接口方法:
void lock();//获取锁
void unlock();//释放锁
void lockInterruptibly() throws InterruptedException;//响应中断的锁
boolean tryLock();//尝试非阻塞式的获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//尝试非阻塞式的获取锁,响应中断,响应超时
Condition newCondition();//获取等待通知组件,组件与锁绑定。获取锁后才可以调用组件的wait方法,并释放锁
lock接口的实现类ReentrantLock :互斥重入锁
java.util.concurrent.locks.ReentrantLock implements Lock, java.io.Serializable
ReentrantLock 结构简述:
拥有三个静态内部类
ReentrantLock.Sync extends AbstractQueuedSynchronizer
ReentrantLock.NonfairSync extends ReentrantLock.Sync
ReentrantLock.FairSync extends ReentrantLock.Sync
类的实例变量:
private final Sync sync;
赋值操作在构造器中:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
Lock接口的实现基本都是通过聚合了一个同步器(AbstractQueuedSynchronizer)的子类来完成线程访问控制的。
接下来研究AbstractQueuedSynchronizer ,研究之前还需要一个小插曲
设计模式之模板模式
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
何时使用:有一些通用的方法。
关键代码:在抽象类实现,其他步骤在子类实现。
优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
主要解决:一些方法通用,却需要在每一个子类都重写这一方法。
如何解决:将这些通用算法抽象出来。
应用实例:spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。
AbstractQueuedSynchronizer
继承于AbstractOwnableSynchronizer
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable
AQS的设计使用到了模板模式,提供了九个模板方法:
1、public final void acquire(int arg){...}//获取独占的同步状态,如果成功,方法返回。如果不成功,线程进入同步队列等待
2、public final void acquireInterruptibly(int arg){...}//获取独占的同步状态,并响应线程中断
3、public final boolean tryAcquireNanos(int arg,long timeout){...}//获取独占的同步状态,响应中断和超时
4、public final boolean release(int arg){...}//释放独占的同步状态
5、public final void acquireShared(int arg){...}//获取共享的同步状态
6、public final void acquireSharedInterruptibly(int arg){...}//获取共享的同步状态,响应中断
7、public final boolean tryAcquireSharedNanos(int arg,long taimeout){...}//获取共享的同步状态,响应中断加超时
8、public final boolean releaseShared(int arg){...}//释放同步状态
9、public final Collection<Thread> getQueuedThreads(){...}//获取等待在同步队列中的所有线程
方法1,2,3中都调用了tryAcquire(int arg)方法,tryAcquire(int arg)方法就是需要子类根据各自需求去重写的方法。
方法5,6,7中都调用了tryAcquireShared(int arg)方法,tryAcquireShared(int arg)方法就是需要子类根据各自需求去重写的方法。
方法4中调用了tryRelease(arg),该方法需要子类去实现。
方法8中调用了tryReleaseShared(arg),该方法需要子类去实现。
还有一个需要子类去重写的方法:
protected boolean isHeldExclusively(); 该方法是判断当前线程是否独占锁。
AbstractQueuedSynchronizer 内部结构:
拥有两个内部类:
java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject 用于实现等待通知机制,最后研究
java.util.concurrent.locks.AbstractQueuedSynchronizer.Node ADT(abstract data type)队列的实现
类final变量:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;//state对象内存地址偏移量
private static final long headOffset;//head对象内存地址偏移量
private static final long tailOffset;//tail对象内存地址偏移量
private static final long waitStatusOffset;//waitStatus对象内存地址偏移量
private static final long nextOffset;//next对象内存地址偏移量
static final long spinForTimeoutThreshold = 1000L; 作用:优化自旋
实例变量:
//表示同步状态
private volatile int state;
提供了三个方法对实例变量state进行操作:
protected final int getState(){ return state;}
protected final void setState(int newState){ state = new State; }
protected final boolean compareAndSetState(int expect, int update){ return unsafe.compareAndSwapInt(this,stateOffset,expect,update); }使用CAS设置当前状态,该方法能够保证状态设置的原子性
//头节点
private transient volatile AbstractQueuedSynchronizer.Node head;
//尾节点
private transient volatile AbstractQueuedSynchronizer.Node tail;
头节点和尾节点用来构造FIFO双向链表队列
接下来研究AbstractQueuedSynchronizer的静态内部类Node ,很重要。AbstractQueuedSynchronizer中的两个实例变量head,tail都是该类型。在了解队列的情况下同步器中的同步队列是很好理解的。如果想去了解数据结构或者Java中是如何实现这些数据结构可以去看一下《数据结构与算法分析+java语言描述_第3版》
AbstractQueuedSynchronizer的静态内部类Node
Node功能简单描述:
既然是实现锁的功能,锁的竞争是肯定会产生的,那么如果线程没有获取到锁,这些线程信息将存储于什么地方?, 答案就是AbstractQueuedSynchronizer.Node。
AbstractQueuedSynchronizer.Node内部结构:
类final变量:
static final Node SHARED = new Node();//共享锁标识
static final Node EXCLUSIVE = null;//独享锁标识
static final int CANCELLED = 1;//同步队列中该节点所持有的线程等待超时或者被中断。
static final int SIGNAL = -1;//同步队列中该节点后继节点的线程处于等待状态。
static final int CONDITION = -2;
static final int PROPAGATE = -3;
实例变量:
volatile Node prev;//前驱节点
volatile Node next;//后驱节点
volatile int waitStatus;//处于同步队列中,等待队列中的Node节点的状态
volatile Thread thread;//线程引用
Node nextWaiter;//用来实现独占锁与共享锁
AQS是抽象类,并且其子类以聚合在Lock接口实现类中的方式来完成线程访问控制,所以分析AQS要结合具体的锁进行分析。
AQS是抽象类,并且其子类以聚合在Lock接口实现类中的方式来完成线程访问控制,所以分析AQS要结合具体的锁进行分析。
分析ReentrantLock独占同步状态模式:
约定:队列的基本操作CRUD分析省去
1、当多个线程执行lock.lock()时,将通过CAS操作去修改state同步状态的值,如果某个一个线程A修改成功,那么将锁的state同步状态值为1,exclusiveOwnerThread属性设置为线程A信息
2、未获取到锁的所有线程执行同步器模板acquire(int arg)方法。
2.1再次获取同步状态(作用:优化。如果在此处成功,那么对于当前线程来说就可以不执行后续操作),通过重写的tryAcquire方法判断线程A是否已经释放锁(同步状态为0),如果已经释放锁,那么通过CAS操作去修改state同步状态的值,成功则state同步状态值为1,exclusiveOwnerThread属性设置为当前线程信息。
2.2如果没有释放锁,判断竞争锁的线程是否为线程A(既重入),如果是,state同步状态增加。
3、如果2.1,2.2操作均失败,既tryAcquire(int arg)方法返回值为false。执行操作四
4、4.1未获取到锁的线程,并且满足操作3的所有线程都将构建一个Node实例,Node节点的thread变量为当前线程信息,nextWaiter变量为EXCLUSIVE,用来表示独占。
4.2如果同步器的tail节点(尾节点)不为空,将构造的节点通过CAS操作插入同步器的tail节点。未获取到锁的所有线程都会执行插入Node节点的操作,所以需要通过CAS_tail来保证线程安全(防止Node节点出现数量偏差)。4.2操作用来优化4.3操作。如果4.2操作成功,那就不用执行4.3操作中的死循环代码块。
4.3如果4.2操作失败。进入“死循环”for(;;)代码块,如果tail为空,说明head就是空的。CAS方式构建head节点进行初始化(多线程环境下采用CAS保证数据安全),将head节点赋值给tail节点。如果不为空,将构造的节点通过CAS操作插入同步器的tail节点。
5、操作4已经将等待获取锁的线程包装为Node节点放入到了FIFO队列中。
5.1接下来进入死循块处理FIFO队列中的等待锁的线程也就是Node节点(此处的死循环是有条件的,并不是一直执行,当线程执行5.1.24操作时,线程被阻塞,就相当于释放资源后停在了那个地方,等下次唤醒后再进入循环)
5.1.1判断该线程构造的节点的前驱节点是否等于head节点(1、FIFO规则)(2、头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,唤醒后继节点),如果该节点的前驱节点等于head节点,并且通过tryAcquire(arg)获取到了同步状态(再竞争一下,也是一次优化),进入5.1.1.1。如果不满足任一条件,进入5.1.2。
5.1.1.1设置同步器的head节点为当前节点(此处不需要CAS是因为已经通过tryAcquire获取到了锁,是独占状态),返回false。
5.1.2判断该线程构造的节点的前驱节点中waitStatus状态
5.1.2.1如果waitStatus == 0,通过CAS操作设置为SIGNAL(表明该节点的后续节点处于等待锁的状态)
5.1.2.2如果waitStatus == SIGNAL,进入操作5.1.2.4
5.1.2.4将当前线程通过LockSupport.park()阻塞线程,线程进入等待状态,并检查是否中断过,返回当前的中断状态并复位,因为当前线程在执行acquireQuere()方法时,是无法响应中断的,但如果某个线程对该线程执行了thread.interrupt(),那么该线程中断标志为true。所以唤醒的时候再响应。(acquire(int arg)方法中selfInterrupt()响应)
注意:acquire()该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同 步队列中移出。此处只是为了列出waitStatus状态的不同操作。所以操作5.1.2.3并不会被触发。
5.1.2.3如果waitStatus > 0,也就是CANCELLED状态,所以要将该节点从同步队列中移除(队列中节点删除操作)(重复检查)
6、当前线程获得锁成功。如果acquireQuere方法返回true,代表之前该线程被中断过,需要响应中断,执行selfinterrupt()方法;如果返回false,说明没有被中断过,什么也不执行。
总结获取锁过程:
频繁出现的内容:同步状态,CAS操作,节点,队列,节点的waitStatus
操作四:每个线程完成节点的构造以及加入同步队列
操作五:每个线程竞争获取同步状态,获取不到的线程阻塞
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。
在整个过程中有两次优化操作:第一次优化操作线程可以不进入同步队列,第二次优化线程可以不阻塞。
7、锁的释放:lock.unlock()方法
7.1修改同步状态 tryRelease(int arg)
7.2修改head节点waitStatus为0
7.3 7.3.1.1当head节点的下一个节点不为null,LookSupport.unpack(head.next.thread),然后去执行acquireQueue(final Node node,int arg )(线程是在这个方法中pack,并且该方法是自旋操作)(唤醒下一次节点)(参数node为head节点的下一个节点)
7.3.1.2在acquireQuere方法中setHead(node),将head节点设置为node节点(既原head节点的下一个节点变为新的head节点,将新的head节点的thread变量和prev变量设置为null)(原节点就被删除了。)
7.3.2 如果head节点的下一个节点为null,或者head节点的下一个节点状态为CANCELLED,从尾节点开始遍历。(为什么从尾部开始遍历,是因为在将节点通过enq(final Node node)方法加入同步队列时,操作时分为三步,不是原子操作,如果有其他线程释放锁的话,enq方法中next节点有可能还没有构建好,如果从后向前的话不会出现这个问题。)
总结释放锁过程:
在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
上述过程中:子类重写的tryAcquire(int acquires)最主要的都是对AQS设置state状态的三个操作方法的调用,以及设置重入等操作。看一下code都可以明白。
接下来分析ReentrantLock的公平锁与非公平锁
公平与非公平的定义
先对锁请求的线程一定先拿到锁(公平)。反之不公平。
“饥饿”:持有锁的线程释放锁后,刚释放锁的线程再次获取同步状态的几率非常大,所以容易导致同步队列中有些线程等待锁的时间很长
公平锁:减少“饥饿”发生情况,相较于非公平锁效率低(大量的线程切换)。
非公平锁:效率高
ReentrantLock的公平锁与非公平锁:不公平的导致原因如下
1、非公平锁的lock()方法中:上来就是所有线程通过CAS操作竞争。
2、公平锁的tryAcquire(int acquires)方法中:!hasQuerePredecessors() 头节点的下一个节点如果不等于null,头节点下一个节点的thread变量等于当前线程时(既是否还有前驱节点在同步队列中)才允许通过CAS设置stat。
非公平锁:效率高
响应中断与等待超时
响应中断是什么意思?
响应中断:如果线程被其他线程修改其中断标识,那么该线程做什么样的处理。
在acquire(int arg)方法中,当线程在获取同步状态的时候,就算被其他线程修改了中断标记,但该线程不会去理会,直到获取锁成功后再响应处理。获取锁的过程是被办法因为线程被中断而停下来。
在acquireInterruptibly(int arg)方法中是支持响应中断的,响应中断方式是在等待获取同步状态时,如果线程被中断,抛出异常。
中断后会执行cancelAcquire(node)方法: node参数为当前获取锁的线程
1、node.waitStatus = NODE.CANCELLED
等待超时:doAcquireNanos(int arg,long nanosTimeout)方法既支持响应中断,也支持等待超时机制。通过nanosTimeout -= now-lastTime公式计算nanosTimeout,其中now为当前尝试唤醒时间,lastTime为上 次唤醒时间,如果nanosTimeout大于0则表示超时时间未到,需要继续睡眠nanosTimeout纳秒。