Java并发编程的艺术-----第五章读书笔记
前言:哇,这一章是真的费劲,以前是知其然,这次探索了如此多的同步组件的源码,终于是知其所以然了。
下一步也能开发自己的同步组件了,嘎嘎。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
5.1
Lock接口与Synchronized比,需要显示的获取与释放锁。
Lock接口的三大特性:
(1)非阻塞的获取锁 tryLock(); 获取了锁返回true。没有获取到就返回false。内部使用的是同步器Sync.tryAcquired(1)
(2)能中断的获取锁 获取锁的线程会相应中断。
(3)超时锁 可以在指定时间内尝试获取锁,在这个时间内是阻塞状态。
Lock lock = new ReentrantLock();
//获取锁,尝试tryAcquired(1)
lock.lock();
try {
//业务
} finally {
//释放同步状态
lock.unlock();
}
不要将lock.lock()放到try中,即不要把获取锁的过程放到try中,因为在获取锁的时候出现了异常,可能会导致锁无故释放。也就是说,只有真实的获取了锁以后,再去调用lock.unlock()释放同步锁。
5.2
队列同步器AbstractQueuedSynchronizer,简称同步器。使用的是int类型的成员变量来表示同步状态,内部维护了一个先进先出FIFO的同步队列。
同步器内置的修改同步状态的三个方法。 getState()、setState()、compareAndSetState(int expect,int update)
开发同步组件,内部的嵌套类一般继承同步器AQS。
锁是面向使用者的,同步器面向的锁的实现者。
同步器已经完成了方法的骨架,锁的开发者需要完成骨架中定义的模版方法,一般try开头的方法需要被开发者重写。
5.2.1
可以被重写方法。AQS同步器有两种模式,独占式与共享式。
同步器提供的模版方法:(1)独占式获取与释放同步状态方法(2)共享式..(3)查询同步队列中等待的线程
5.2.2
同步队列遵循FIFO,首节点是获取同步状态成功的节点,头节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
调用同步器的acquired(int arg)方法可以获取同步状态。该方法对中断不敏感,即该线程在获取同步状态失败后,构造节点并加入了同步队列后,若进入阻塞状态,那么中断该线程并不会让该节点从同步队列中移除。
接下来是AbstractQueuedSynchronizer源码分析,大佬早就会了:
//同步器为独占式提供的acquried()方法
public final void acquire(int arg) {
//tryAcquire方法由锁的开发者重写。若返回false,说明线程获取锁失败。那么进入自旋
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//线程自旋获取同步状态
//acquireQueued方法返回当前线程是否被中断过,只是记录,不抛异常。
selfInterrupt();
}
//该方法就是将当前线程构造成Node对象,并加入到AQS维护的FIFO队列的末尾
private Node addWaiter(Node mode) {
//构造节点
Node node = new Node(Thread.currentThread(), mode);
//尝试快速的加入队列尾部
Node pred = tail;
if (pred != null) {
//把当前节点的前驱节点设置为之前的尾部节点
node.prev = pred;
//使用CAS保证这个时候没有其它的节点加入到尾巴节点。
if (compareAndSetTail(pred, node)) {
//设置之前尾巴节点的后继节点是当前的节点
pred.next = node;
//返回新构造的当前线程的节点
return node;
}
}
//快速加入队列失败,说明这个时候队列还没初始化呢。
//所以就先创建一个Node对象赋给head与tail
enq(node);//方法略
return node;
}
//该方法是:节点自旋的获取同步状态
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//找到当前节点的前驱节点
final Node p = node.predecessor();
//如果当前节点的前驱节点是头节点,并且又获取到了同步状态
if (p == head && tryAcquire(arg)) {
//那么就把当前节点作为头结点,并唤醒后继节点
setHead(node);
p.next = null; // help GC
failed = false;
//单纯的返回中断位,提示外层这个线程可能被中断过
return interrupted;
}
//如果自旋失败,那么就使用工具park阻塞线程。
//线程再次醒来的条件是:前驱节点被释放、或者线程被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//只是单纯的记录一下这个线程中断过
interrupted = true;//而相应中断则throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
为什么只有前驱节点是头节点才能尝试获取同步状态?
(1)头节点是获取了同步状态的节点,释放同步状态之后会唤醒后继节点,所以被唤醒的节点要判断一下自己的前驱节点是不是头节点
(2)维护队列先进先出原则。前驱节点是头节点的节点是最接近头结点的节点,也是最早加入队列的节点。
在获取同步状态时,同步器维护了一个同步队列。获取状态失败的线程都会被加入到这个队列中,并且在队列中进行自旋。移出队列的条件是前驱节点为头节点并且成功获取了同步状态。在释放同步状态的时候,调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。即让别的线程开始执行任务。
共享式同步状态的获取
//AQS的共享式获取同步状态
public final void acquireShared(int arg) {
//tryAcquiredShared方法若返回大于等于0,表明获取同步状态成功
//tryAcquiredShared由开发者重写
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//线程自旋获取同步状态
private void doAcquireShared(int arg) {
//构造当前线程并加入到同步队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for (;;) {
//获取前驱节点
final Node p = node.predecessor();
if (p == head) {
//若前驱节点是头节点
int r = tryAcquireShared(arg);
//并且成功获取到同步锁,tryAcquireShared重写的方法>=0
if (r >= 0) {
//设置当前节点是头节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
//响应外部中断该线程
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//本次自旋获取同步状态失败
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞当前节点,并且判断当前线程是否被外部中断过
parkAndCheckInterrupt())
//修改中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享式同步状态的释放
public final boolean releaseShared(int arg) {
//由开发者重写的方法,若tryReleaseShared方法返回true
if (tryReleaseShared(arg)) {
//那么就释放同步状态,唤醒头节点的后继节点
doReleaseShared();
return true;
}
return false;
}
TripleSharedLock.java
package com.ssi.javase.thread2;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* Created by jay.zhou on 2018/9/27.
*/
public class TripleSharedLock implements Lock {
//创建同步器对象,共享状态为3,表示同时只有3个线程能够同时获取临界资源
private static final Sync sync = new Sync(3);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count < 0) {
throw new IllegalArgumentException("synchronized state must larger than 0");
}
//设置底层同步状态为count
setState(count);
}
//返回 大于等于0 ,表示线程获取到同步状态
@Override
protected int tryAcquireShared(int reduceCount) {
//更改同步状态通过CAS保证原子性
for (; ; ) {
//获取当前同步状态
int current = getState();
//计算出最新的同步状态
int newCount = current - reduceCount;
//如果当前newCount大于等于0,那么就是获取成功,那么就进行CAS更改同步状态
//如果当前newCount小于0,那么就返回这个小于0的数值,表示获取同步状态失败
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
@Override
protected boolean tryReleaseShared(int increaseCount) {
for (; ; ) {
//释放同步锁,相当于在同步状态state上加1
//获取当前同步状态
int current = getState();
//计算出最新的同步状态
int newCount = current + increaseCount;
//如果成功归还同步状态,那么就退出循环。
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
}
@Override
public void lock() {
//尝试获取同步状态,失败了AQS会用 LockSupport.park(currentThreadNode)
sync.acquireShared(1);
}
@Override
public void unlock() {
//尝试释放同步状态,死循环,直到释放成功
sync.releaseShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
//响应中断的获取同步资源
sync.acquireSharedInterruptibly(1);
}
@Override
public boolean tryLock() {
//返回大于等于0,表示当前线程已经获取到同步锁
return sync.tryAcquireShared(1) >= 0;
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
//尝试在指定时间内获取同步资源(本方法响应中断)
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("暂时还不会实现");
}
}
5.3
重入锁表ReentrantLock表明可以对资源重复加锁。
公平锁与非公平锁。公平体现在:先请求锁的线程优先获取锁。
实现重入锁的关键:(1)当前线程能够再次获取锁。(2)重复获取同步状态n次,同步状态也需被释放释放n次。
实现原理:判断当前现线程是否是获取锁的线程。如果是的话,那么就将同步状态自行增加。释放同步锁判断同步状态是否为0,为0 的话说明已经释放同步锁。
因为lock()与unlock()是成对出现的,所以重进入了n次,也会释放n次同步状态。
公平锁的优势:避免一个线程长时间得不到锁
非公平锁的优势:避免大量的线程切换带来的性能损耗,它能保证更大的吞吐量
RentrantLock默认调用顺序:ReentrantLock.lock(); Sync.acquire(1); Sync.tryAcquire(1); Sync.nonfairTryAcquire(1);
//ReentrantLock内部的子类AQS的nonfairTryAcquire方法
//ReentrantLock获取同步状态
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前同步状态
int c = getState();
//如果当前没有其它线程获取了同步状态
if (c == 0) {
//那么就成功获取同步状态
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前持有同步锁的线程就是自己
else if (current == getExclusiveOwnerThread()) {
//计算最新的同步状态
int nextc = c + acquires;
//设置同步状态,已经加锁
setState(nextc);
return true;
}
return false;
}
//释放同步锁
protected final boolean tryRelease(int releases) {
//计算最新的同步状态
int c = getState() - releases;
//如果当前的线程不是占有同步状态的线程,那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//free表示是否成功设置最新的同步状态
boolean free = false;
//如果最新的同步状态为0,说明已经释放完同步状态
if (c == 0) {
//表示已经无锁了
free = true;
//设置占有同步状态的线程为null
setExclusiveOwnerThread(null);
}
//在同步状态下设置最新的同步状态
setState(c);
return free;
}
ReentrantLock内部还有一个公平的子类AQS。唯一不同的是,在尝试获取同步状态的时候,增加了判断是否前驱节点的方法。tryAcquired方法与nonfairTryAcquire方法的唯一区别。
判断当前节点是否有前驱节点,如果有,说明有比它更早的节点,所以它要等待前驱节点获取并释放锁之后才能继续尝试获取锁。
//公平锁的AQS的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//判断当前节点是否有前驱节点,返回true,说明有比它更早的节点。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
5.4
读写锁维护了一对锁,一个读锁ReadLock接口,与一个写锁WriteLock接口。
通过分离读锁与写锁,使得并发性能比一般其它的排他锁有了很大提升。
写线程获取到同步锁,其它线程全部阻塞。
Lock接口的三大特性:
(1)非阻塞的获取锁 tryLock(); 获取了锁返回true。没有获取到就返回false。内部使用的是同步器Sync.tryAcquired(1)
(2)能中断的获取锁 获取锁的线程会相应中断。
(3)超时锁 可以在指定时间内尝试获取锁,在这个时间内是阻塞状态。
读写锁三大特性
(1)可重入
(2)公平性选择 通过构造函数
(3)锁降级
读写锁设计关键:按位切割,整形变量的高16位维护读同步状态,低16位维护写同步状态。
总结:读锁读读共享,乃共享锁。线程安全的增加和减少同步状态。
写锁,除当前线程外的所有线程阻塞,可获取读锁(锁降级),线程安全的操作同步状态。
锁降级:先获取写锁,再获取读锁,再释放写锁。这个时候线程只剩下读锁,所以写锁降级为读锁。
5.5
LockSupport.park()方法,阻塞当前线程。再次醒来的条件是:LockSupport.unpark()方法或者线程被中断。
5.6
Condition接口的await()方法,将当前线阻塞,并释放同步锁。直到被通知 signal()或者中断。
Condition接口的signal()方法唤醒一个等待在当前Condition对象上的线程。
ConditionObject是AQS的内部类。每个Conditoin内部都有一个等待队列。
线程调用condtion.await()方法,那么该线程会释放同步锁,并构造节点加入等待队列中。