java中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等一些方法来实现了CAS,CAS包含三个操作数:
偏移量: 工作内存中的旧值内存地址
预期值: 主内存中的值,因为预期这个值和偏移量值会相等,所以叫预期值
新值: 工作内存中修改后的新值
例如两个线程要修改主内存中的一个值A,两个线程会把A的值都加载到自己的工作内存中,此时这个工作内存中的值就是偏移量,主内存的值就是预期值。假如A先修改完成,去做写回操作,会先去拿偏移量跟预期值去比较,如果一样,就写回,如果不一样就会拿到主内存中的新值,再去比较,以此自旋,直到成功。
模拟CSA实现:
public class CAS_Test {
//锁状态
private volatile int state = 0;
//state偏移量
private static final long stateOffset;
//线程栅栏,表示阻塞五个线程,都执行完成后再释放屏障
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
private static Unsafe unsafe;
private static CAS_Test cas = new CAS_Test();
//通过反射获取Unsafe实例
public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe)field.get(null);
}
//获取state偏移量
static {
try {
unsafe = getUnsafe();
//取到state的偏移量(地址值)
stateOffset = unsafe.objectFieldOffset(CAS_Test.class.getDeclaredField("state"));
} catch (Exception e) {
throw new Error();
}
}
//通过CAS实现修改state的值
public final boolean compareAndSwapState(int oldValue,int newValue){
//this:
//oldValue:主内存的值
//newValue:要替换的新值
return unsafe.compareAndSwapInt(this,stateOffset,oldValue,newValue);
}
static class Worker implements Runnable{
@Override
public void run() {
log.info("请求:{}准备开始抢state执行权:)",Thread.currentThread().getName());
try {
//线程栅栏,模拟线程同时抢锁
cyclicBarrier.await();
//通过CAS把state的旧值0修改为1
if(cas.compareAndSwapState(0,1)){
log.info("当前请求:{},抢到锁!",Thread.currentThread().getName());
}else{
log.info("当前请求:{},抢锁失败!",Thread.currentThread().getName());
}
} catch (InterruptedException|BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception {
//五个线程抢锁
new Thread(new Worker(),"t1").start();
new Thread(new Worker(),"t2").start();
new Thread(new Worker(),"t3").start();
new Thread(new Worker(),"t4").start();
new Thread(new Worker(),"t5").start();
}
}
Java并发编程核心在于java.util.concurrent包,而juc当中的大多数同步器实现都是几种基础行为如:等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。
ReentrantLock是一种基于AQS框架的应用实现,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。
ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,true为公平锁
lock.lock() //加锁
int a = 1;//业务代码
lock.unlock() //解锁
首先要了解AQS的实现方式,ReentrantLock类的内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized(AQS),对该抽象类的部分方法做了实现,并且还定义了两个子类:
1、FairSync:公平锁的实现
2、NonfairSync:非公平锁的实现
这两个类都继承自Sync,使用了设计模式中的模板模式。
公平锁
:例如多个线程在抢锁,线程A抢到了锁,其他线程就会去排队等待锁释放,此时又进来了一个线程B,正好赶上了线程A释放了锁,此时B不会拿到锁,而是去其他线程后边排队,这就是公平锁。
非公平锁
:与公平锁相反,如果正好赶上了线程A释放了锁,B就会立刻插队拿到锁。
什么是线程中断?
首先要知道的是中断会唤醒线程,比如有两个线程A、B,线程A先执行逻辑,如果在A线程中调用B的interrupt方法就会立刻唤醒B线程,即使线程sleep的时候也会被立刻唤醒。
InterruptedException
线程处于Blocked、Waiting、Timed_waiting状态,或者从这三种状态转变为Runnable状态的过程中被中断,会抛出InterruptedException异常,例如sleep、wait。抛出InterruptedException异常就是为了线程在这些非活跃状态下,可在代码中自行处理中断,然后让线程回到运行状态执行处理中断的逻辑。如果在线程运行期间中断,此时线程状态本来就是活跃的,会无视中断。
查看线程中断有两个方法isInterrupted() 和interrupted(),isInterrupted只是查询中断状态,interrupted会查看中断状态并且清除中断状态。
自旋
:线程循环获取锁,入队时也会循环入队
LockSuport
:在加锁时,加锁失败的线程如果一直自旋加锁会过多的消耗cpu,为了降低自旋带来的性能消耗,对加锁失败的线程可以通过LockSuport.park()让线程阻塞住,在持有锁的线程运行结束后,进行解锁操作时,再通过LockSuport.unpark()去唤醒线程继续自旋抢锁。
CAS
:在修改同步器状态时,通过CAS对state进行修改,保证锁的互斥性
queue队列
:存放阻塞线程的队列(CLH队列)
state: 用来记录同步器状态,表示是否持有锁,持有几次锁。最上边介绍中说AQS是一个依赖状态(state)的同步器,就是这个状态,state是一个被volatile修饰的整型值,默认为0,表示没有任何线程持有锁。
private volatile int state;
exclusiveOwnerThread: 在ReentrantLock的继承关系中,有一个超类AbstractOwnableSynchronizer,其中有一个变量exclusiveOwnerThread,是一个线程类型,这个变量用来记录当前获取锁的线程是哪个。
private transient Thread exclusiveOwnerThread;
head、tail: head指向线程队列的头部节点,tail指向线程队列的尾部节点,如果都为空,表示队列中没有线程
private transient volatile Node head;
private transient volatile Node tail;
Node、prev、next、nextWaiter: Node类型用来存储队列中的线程信息,一个Node就是一个线程,这些Node组成一个队列,每个Node的prev属性指向前一个Node,next属性指向后一个Node
volatile Node prev;
volatile Node next;
Node nextWaiter;
thread: 存储当前Node的线程引用。
volatile Thread thread;
waitState: 当前Node的生命状态(信号量)
初始状态 = 0
SIGNAL = -1 代表可被唤醒
CANCELLED = 1 代表出现异常,需要废弃
CONDITION = -2 代表条件等待
PROPAGATE = -3 传播
volatile int waitStatus;
ReentrantLock的lock实现
调用ReentrantLock.lock时,会调用子类sync的lock方法,这个lock是不响应中断的lock。还有一个lockInterruptibly是响应中断的lock,会用到cancelAcquire方法,把中断的node清理掉。
//公平锁入口
static final class FairSync extends Sync {
//响应中断lock
final void lock() {
acquire(1);
}
/**
* 获取锁的方法,获取失败就进行入队,park
**/
public final void acquire(int arg) {
//尝试加锁
if (!tryAcquire(arg) &&
//加锁成功不入队,加锁失败尝试入队
//acquireQueued:对入队成功的节点进行阻塞
//addWaiter:尝试入队 EXCLUSIVE:独占属性
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 尝试加锁,返回true表示加锁成功,false为加锁失败
**/
protected final boolean tryAcquire(int acquires) {
//当前线程对象的引用
final Thread current = Thread.currentThread();
//获取同步器状态
int c = getState();
//如果是0,可以获取锁
if (c == 0) {
//判断队列是否为空 true是不为空, fasle为空
if (!hasQueuedPredecessors() &&
//通过CAS修改同步器的state,对当前线程加锁
compareAndSetState(0, acquires)) {
//给exclusiveOwnerThread属性赋值成当前线程的
setExclusiveOwnerThread(current);
return true;
}
}
//如果有持有锁,判断是否是自己持有锁(ReentrantLock是可重入锁)
else if (current == getExclusiveOwnerThread()) {
//当前线程再次持有到锁,对state进行+1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//加锁失败
return false;
}
/**
*判断队列是否是空
**/
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//头结点不等于尾结点 &&(头结点的下一结点等于null || 头结点的下一结点的线程指向不等于当前线程)
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
/**
* 线程入队,mode为独占模式(EXCLUSIVE)
**/
private Node addWaiter(Node mode) {
//new一个独占模式的节点,Thread属性指向当前线程
//并且waitState是0,表示刚初始化
Node node = new Node(Thread.currentThread(), mode);
//把tail指针复制给当前节点
//第一次初始化时,AQS中的head和tail都为null,不走下边的if
Node pred = tail;
if (pred != null) {
//把新节点的前驱节点指向当前的尾节点
node.prev = pred;
//CAS将新节点入队
if (compareAndSetTail(pred, node)) {
//入队成功把前驱节点的后驱节点指向新节点
pred.next = node;
return node;
}
}
//自旋入队
enq(node);
//入队成功,返回Node(currentThread)
return node;
}
/**
* 自旋入队
**/
private Node enq(final Node node) {
//自旋是为了保证所有节点都入队成功
for (;;) {
//把队尾指针赋值给当前节点
Node t = tail;
if (t == null) {
//tail指针为null时,创建一个空节点,CAS初始化队列
if (compareAndSetHead(new Node()))
tail = head;
//tail指针不为null时,此时这个tail指向的是当前节点的前一个节点
} else {
//把当前节点的前驱指针指向tail节点
node.prev = t;
//CAS移动tail指针指向当前节点
if (compareAndSetTail(t, node)) {
//把tail节点的后驱指针指向当前节点
t.next = node;
//入队成功,入队失败的线程会自旋再次尝试入队
return t;
}
}
}
}
/**
* 对入队成功的节点进行阻塞
**/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//当前线程是否被中断过
boolean interrupted = false;
for (;;) {
//获取当前node的前驱节点
final Node p = node.predecessor();
//如果前驱节点是头部节点会尝试重新获取锁
if (p == head && tryAcquire(arg)) {
//获取到锁,此时前驱节点是一个空节点
//把head指针指向当前node,并且把线程属性和前驱节点都 置为null
setHead(node);
//把前驱节点的后驱指针置为null,此时前驱节点就变成了一个空的头节点
//之前的节点会被Gc回收
p.next = null;
//修改为false,表示当前线程获取锁过程中没有被中断
failed = false;
//返回当前线程的中断标记,此处为fasle,表示未被中断过
return interrupted;
}
/** 没有获取到锁,尝试修改waitState,两轮循环之后会进行节点阻塞操作
* 第一轮把前驱节点waitState从0修改为-1,第二轮就能返回true
* 只有前驱节点的waitState为-1时,队列的第一个节点线程才能被唤醒
* 因为在释放锁时,会判断头节点的watiState!=0,如果=0会再次把waitState从-1修改为0
* 唤醒节点后,就会继续循环去抢锁,但在非公平锁场景下,可能会有新加入的线程抢到锁,
* 当前节点就可能会再次被阻塞,释放锁时把waitState改为0就是为了恢复节点的状态,等待下一次抢锁
**/ if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞节点,判断线程是否是由中断信号唤醒,并清除中断信号
parkAndCheckInterrupt())
//表示当前node的线程是被中断信号唤醒的
interrupted = true;
}
} finally {
if (failed)
//如果线程被中断过,把节点置为无效状态1
cancelAcquire(node);
}
}
/**
* 修改watiState
**/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取到前驱节点的waitStatus,
int ws = pred.waitStatus;
//如果前驱节点的waitStatus = -1,表示当前节点可被唤醒,返回true执行阻塞操作
if (ws == Node.SIGNAL)
return true;
//如果前驱节点的waitStatus > 0,表示不可被唤醒,做进一步处理
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱节点的waitStatus = 0,CAS修改前驱节点的waitStatus为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* 阻塞节点
**/
private final boolean parkAndCheckInterrupt() {
//阻塞节点
LockSupport.park(this);
//判断线程是否是由中断信号唤醒,并且清楚中断标记
return Thread.interrupted();
}
公平锁加锁方法调用流程:
ReentrantLock中有一个内部类Sync,FairSync继承Sync来实现lock加锁,lock调用了acquire方法,参数为1,表示对设置同步器状态为1,进行加锁。
1、先通过tryAcquire()方法去尝试加锁,在tryAcquire()方法中先获取到当前同步器状态state如果state=0的话,表示没有线程持有锁。
2、没有锁的话就通过hasQueuedPredecessors()方法判断CLH阻塞队列是否为空,如果为空或者队列中只有自己线程在排队,CAS修改state = 1,并且把当前线程记录到exclusiveOwnerThread属性中,表示当前线程持有锁,返回true,加锁成功。
3、如果队列不为空,并且下一个排队线程不是自己线程,此时需要排队,返回false,继续执行入队逻辑
4、线程入队调用addWaiter()方法,参数mode表示Node是独占模式的,此方法会new一个新节点,并且把tail指向的节点保存到pred变量中,pred用来判断队列中是否已经有node。
5、如果是第一次初始化,AQS中的head、tail都为null,调用enq()方法,把tail的引用赋值给t节点,此时t=null表示队列中没有节点,会创建一个新节点,并把t节点设置成头节点。再进行第二次自旋,第二次tail就不为null了,会把addWaiter()中new的新节点的前驱节点设置成t节点,把t节点的后驱节点设置成new的新节点,新节就变成了尾节点,入队成功,返回成功点。
6、如果不是第一次初始化,tail不为null,会先把新节点的前驱节点指向当前的尾节点,然后CAS将新节点入队,入队成功把前驱节点的后驱节点指向新节点,入队成功,返回成功点。
7、接着调用acquireQueued()方法,对入队成功的节点进行阻塞,获取当前节点的前驱节点,如果前驱节点是头部节点,表示这个节点是队列中的第一个,会尝试重新获取锁。
8、如果获取到了锁,会把head指向当前节点,并且把线程属性和前驱节点都置为null,把前驱节点的后驱指针置为null,此时前驱节点就变成了一个空的头节点,当前节点就变成了新的头节点,旧的头节点GC时会被回收,此时通过这种修改前后指针的方法,就把新的节点设置成了头节点。然后会把failed变量设置为false,表示当前线程获取锁过程中没有被中断。
9、如果没有获取到锁,调用shouldParkAfterFailedAcquire()方法,尝试修改waitState,两轮循环之后会进行节点阻塞操作。第一轮把前驱节点waitState从0修改为-1,第二轮就能返回true。
只有前驱节点的waitState为-1时,队列的第一个节点线程才能被唤醒。因为在释放锁时,会判断头节点的watiState!=0,如果!=0会再次把waitState从-1修改为0。因为在非公平锁场景下,可能会有新加入的线程抢到锁,当前节点抢锁就会再次阻塞,释放锁时把waitState改为0就是为了恢复节点的状态,等待下一次抢锁。
10、修改waitState成功后,调用parkAndCheckInterrupt()方法对线程进行阻塞操作(LockSupport.park(this)),然后判断线程是否是由中断信号唤醒并且清除中断标志,如果是,把interrupted 设置成true,表示是由中断信号唤醒。
11、如果不是由中断信号唤醒,逻辑结束,加锁成功。
12、如果是由中断信号唤醒的,在finally中调用会cancelAcquire()方法,找出队列中所有无效节点(有中断标记的),判断节点类型(区分头节点、尾节点、中间节点走不同处理逻辑)然后通过修改前后指针的方法,把无效节点取消掉,重新连接有效节点。
13、接着调用selfInterrupt()方法,再次给线程一个中断信号,加锁结束。再次标记是为了保证外部业务逻辑也能识别到中断信号
阻塞队列BlockingQueue是JUC包提供的用于解决并发中生产者与消费者问题的类。保证在任意时刻只有一个线程可以对队列进行take、put操作,如果队列满了就必须阻塞等待消费,如果队列是空的必须阻塞等待生产。
队列类型分为两种:
无限队列
:几乎可以无限增长
有限队列
:可定义最大容量
阻塞队列类型:
ArrayBolckingQueue(常用)
:由数组组成的有界阻塞队列,基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞,在线程池中、生产者消费者场景有比较多的应用。
LinkedBlockingQueue(常用)
:由链表结构组成的有界阻塞队列 大小默认为 Integer最大值
PriorityBlockingQueue : 支持优先级排序的无界阻塞队列。
DelayQueue: 使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue: 不存储元素的阻塞队列。
LinkedTransferQueue: 由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 由链表结构组成的双向阻塞队列
多线程的生产者-消费者的阻塞队列模型
BlockingQueue源码解析:
//队列构造方法
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
//创建一把锁
lock = new ReentrantLock(fair);
//创建两个条件对象
//notEmpty是take时需要等待的条件
notEmpty = lock.newCondition();
//notFull时put时需要等待的条件
notFull = lock.newCondition();
}
//put方法
public void put(E e) throws InterruptedException {
checkNotNull(e);
//给put操作加锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//判断当前队列是否已满
while (count == items.length)
//满了就入队到条件等待队列,notFull是put时需要等待的条件
notFull.await();
//没有满就入同步队列
enqueue(e);
} finally {
//解锁
lock.unlock();
}
}
//take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
//如果队列为空,直接通过await方法阻塞
notEmpty.await();
//对节点进行出队操作,同时通知put的一方把条件队列的节点放进同步队列
return dequeue();
} finally {
//解锁
lock.unlock();
}
}