章节目录
- Lock接口与Synchronized的区别及特性
- 队列同步器的接口与自定义锁示例
- 队列同步器的实现分析
1.Lock接口与Synchronized的区别及特性
特性 | 描述 |
---|---|
尝试非阻塞性的获取锁 | 当前线程尝试获取锁(自旋获取锁),如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断的获取锁 | 已获取锁的线程可以响应中断,当获取到锁的线程被中断时,可以抛出中断异常,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍然没有获取到锁,则返回 |
注意:Lock接口的实现基本上都是通过聚合了一个同步器的子类来完成线程访问控制的
队里同步器的接口与定义锁示例
队列同步器定义:
队列同步器,是用来构建锁与其它同步组件的基础框架,基本数据结构与内容是:
1、int state -> state 标示同步状态;
2、内置的FIFO来完成获取同步状态的线程的排队工作。
队列同步器使用方式
1、子类通过继承同步器并实现它的抽象方法来管理同步状态;
2、实现过程中对同步状态的更改,通过
setState()、
setState(int newState)、
compareAndSetState(int expect,int newUpdateValue)
来进行操作,保证状态改变时原子性的、安全的;
3、实现同步器的子类被推荐为自定义同步组件的静态内部类;
4、同步器可以支持独占式的获取同步状态(ReentrantLock)、也可以支持共享
式的获取同步状态(ReentrantReadWriteLock)
对于同步器与锁的关系可以这样理解:
- 在锁的实现中聚合同步器,利用同步器实现锁的语义。
- 锁面向使用者,它定义了使用者与锁的交互接口,隐藏了实现细节。
- 同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步器状态管理、线程排队、等待与唤醒等底层操作。
2.队列同步器的接口与自定义锁示例
2.1 模板方法模式
同步器的设置是基于**模版方法模式**,使用者需要继承同步器并重写指定的方
法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板
方法,而这些模板方法将会调用使用者重写的方法。
2.2 重写同步器指定的方法
getState():获取当前同步状态
setState(int newState):设置当前同步状态
compareAndSetState(int expect,int update): 使用CAS设置当前的状态,该方
法保证状态设置的原子性
2.3 同步器可重写的方法
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态(公平性获取锁) |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回>=0的值,标示获取成功,反之获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占 |
2.4 独占锁示例
package org.seckill.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 利用了模板方法模式
*/
public class Mutex implements Lock {
private static class Sync extends AbstractQueuedSynchronizer {
//是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
//当状态为0时获取锁
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁,将当前状态设置为0
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//返回一个condition,每个condition中都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
//仅需要将操作代理到Sync上即可
private Sync sync = new Sync();
public void lock() {
sync.acquire(1);//调用tryAccquire
}
//当前已获取锁的线程响应中断,释放锁,抛出异常,并返回
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);//尝试立即获取锁
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));//尝试超时获取锁
}
public void unlock() {
sync.release(1);//释放锁
}
public Condition newCondition() {
return sync.newCondition();
}
}
总结-实现同步组件的方法
1. 独占锁Mutex 是一个自定义同步组件,它允许同一时刻只允许同一个线程占有锁。
2.Mutex中定义了一个私有静态内部类,该类继承了同步器并实现了独占式获取和释放同步状态。
3.在tryAcquire(int acquires)方法中,经过CAS设置成功(同步状态设置为1),则
代表获取了同步状态,而在tryRelease(int releases) 方法中只是将同步状态重
置为0。
3 队列同步器的实现分析
3.1 同步队列数据结构
- 同步器依赖内部的同步队列,即一个FIFO的队列,这个队列由双向链表实现。节点数据从 队列尾部插入,头部删除。
- node 数据结构
struct node {
node prev; //节点前驱节点
node next; //节点后继节点
Thread thread; //获取同步状态的线程
int waitStatus; //等待状态
Node nextWaiter; //等待队列中的后继节点
}
等待队列 后续篇章介绍到condition会有相关记录。
3.2 无法获取到同步状态的线程节点被加入到同步队列的尾部
本质上是采用 compareAndSetTail(Node expect,Node update),当一个线程成功的获取了同步状态
(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程
必须要保证线程安全。所以采用了基于CAS的方式来设置尾节点的方法。
,需要传递当前节点认为的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
3.3 成功获取同步状态
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放
同步状态时,会唤醒后继节点,而后继节点将会在获取同步状态成功时,将自己设置为首节点。
3.4 独占式同步状态获取与释放
- 前驱节点为头节点且能够获取同步状态的判断条件和线程进入同步队列 来获
取同步状态是自旋的过程。 - 设置首节点是通过获取同步状态成功的线程来完成的acquireQueued(node,args)完成的
独占式获取同步状态的流程图