并发编程 CAS、AQS、BlockingQueue学习总结

CAS(比较与交换)原理

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();
    }
}

执行结果:只有t3线程抢到了锁
并发编程 CAS、AQS、BlockingQueue学习总结_第1张图片


AQS

介绍

Java并发编程核心在于java.util.concurrent包,而juc当中的大多数同步器实现都是几种基础行为如:等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。

ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。

ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,true为公平锁
lock.lock() //加锁
int a = 1;//业务代码
lock.unlock() //解锁
AQS底层原理

首先要了解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会查看中断状态并且清除中断状态。

AQS核心原理

自旋:线程循环获取锁,入队时也会循环入队
LockSuport:在加锁时,加锁失败的线程如果一直自旋加锁会过多的消耗cpu,为了降低自旋带来的性能消耗,对加锁失败的线程可以通过LockSuport.park()让线程阻塞住,在持有锁的线程运行结束后,进行解锁操作时,再通过LockSuport.unpark()去唤醒线程继续自旋抢锁。
CAS:在修改同步器状态时,通过CAS对state进行修改,保证锁的互斥性
queue队列:存放阻塞线程的队列(CLH队列)

AQS核心参数:

并发编程 CAS、AQS、BlockingQueue学习总结_第2张图片
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();
 }

公平锁加锁方法调用流程:
并发编程 CAS、AQS、BlockingQueue学习总结_第3张图片
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

阻塞队列BlockingQueue是JUC包提供的用于解决并发中生产者与消费者问题的类。保证在任意时刻只有一个线程可以对队列进行take、put操作,如果队列满了就必须阻塞等待消费,如果队列是空的必须阻塞等待生产。
队列类型分为两种:
无限队列 :几乎可以无限增长
有限队列 :可定义最大容量
阻塞队列类型:
ArrayBolckingQueue(常用):由数组组成的有界阻塞队列,基于ReentrantLock保证线程安全,根据Condition实现队列满时的阻塞,在线程池中、生产者消费者场景有比较多的应用。
LinkedBlockingQueue(常用):由链表结构组成的有界阻塞队列 大小默认为 Integer最大值
PriorityBlockingQueue : 支持优先级排序的无界阻塞队列。
DelayQueue: 使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue: 不存储元素的阻塞队列。
LinkedTransferQueue: 由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 由链表结构组成的双向阻塞队列
多线程的生产者-消费者的阻塞队列模型
并发编程 CAS、AQS、BlockingQueue学习总结_第4张图片
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();
        }
}

你可能感兴趣的:(学习,java)