java多线程并发基础(三,ReentrantLock,CountDownLatch,CyclicBarrier,Semaphore实现原理)

一、独占锁ReentrantLock的原理

ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞放入该锁的AQS阻塞队列里面。
在这里插入图片描述
从图中可以看出,ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是一个公平锁还是非公平锁

public ReentrantLock() {
	//默认非公平锁
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • Sync类直接继承AQS,它的子类NofairSync和FairSync分别实现了非公平锁和公平锁
  • 这里AQS的状态值state状态值表示线程获取锁的可重入次数,默认情况下,state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取时会尝试CAS操作设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程,可重入,重入会判断是否是持有者,是就状态值state+1。该锁释放锁的时候,会尝试使用CAS让状态值-1,如果减后状态值为0,则当前线程释放该锁。
1.1 获取锁

void lock()方法:
一个线程调用该方法时,如果锁没有被其他线程占有:

  1. 当前线程也没有占有该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。
  2. 当前线程已经获得该锁,则把state+1后返回

如果该锁被其他线程占有,则当前线程被放入AQS队列后阻塞挂起

public void lock() {
      sync.lock();
}

上面是委托给Sync类实现lock,有两种实现:

NonfairSync:

final void lock() {
	//(1)CAS设置状态值为1 
    if (compareAndSetState(0, 1))
    	//设置锁的持有者为当前线程 这个方法很简单,内部就是字面意思
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	//(2)调用AQS的acquire方法
        acquire(1); //注意: 传的参数为1
}

AQS的acquire的核心代码

    public final void acquire(int arg) {
    	//调用ReentrantLock重写的tryAcquire(arg)方法
        if (!tryAcquire(arg) &&
        	//tryAcquire返回false会把当前线程放入AQS阻塞队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

ReentrantLock重写的tryAcquire方法:非公平锁的代码

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //(1)当前AQS状态值为0,为0则说明当前锁空闲,尝试CAS获取该锁
    if (c == 0) {
    	//这里的acquire值为1
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //(2)c不为0,判断是否当前线程是该锁持有者,是状态值+1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

FairSync重写的tryAcquire方法:公平锁的代码

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //当前AQS状态值为0
    if (c == 0) {
    	//(*)公平锁策略
        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;
}

和非公平锁不同的地方就是(*)在设置CAS前添加了hasQueuedPredecessors()方法,该方法为公平锁的核心代码

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

在如上代码中,如果当前线程节点有前驱节点则返回 true ,否则如果当前 AQS 队列为空或者当前线程节点是 AQS 的第一个节点则返回 false 。其中如果 h == t 则说明当前队列为空,直接返回 false ;如果 h ! = t 并且 s == null则说明有一个元素将要作为 AQS 的第一个节点入队列(回顾前面的内容, enq 函数的第一个元素入队列是两步操作:首先创建一个哨兵头节点,然后将第一个元素插入哨兵节点后面),那么返回 true ,如果 h ! = t 并且 s ! = null 和 s . thread ! = Thread . currentThread ( )则说明队列里面的第一个元素不是当前线程,那么返回 true 。

1.2 释放锁
public void unlock() {
        sync.release(1);
}
protected final boolean tryRelease(int releases) {
	//如果不是锁持有与这调用UNlock则抛异常
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //如果当前可重入次数为0,则清空锁持有线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //设置可重入次数为原始值-1
    setState(c);
    return free;
}

二、同步器CountDownLatch原理

在日常开发中经常会遇到需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后在进行汇总的场景。j之前讲过join可以实现,但是不够灵活,不能满足不同场景的需要,所以出现了CountDownLatch。

2.1例子:
public class CountDownLatchTest {
    private static volatile CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException{
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //将线程A添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
                System.out.println("child threadone is over");
            }
        });
        //将线程B添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }
                System.out.println("child threadtwo is over");
            }
        });

        System.out.println("wait all child thread over");

        //等待子线程执行完毕,返回
        countDownLatch.await();

        System.out.println("all child thread over");
        executorService.shutdownNow();
    }
}

执行结果:
在这里插入图片描述
与join方法的区别:

  1. 调用子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而CountDownLatch则使用计数器来允许子线程运行完毕或者在运行中递减计数,也就是CountDownLatch可以在子线程运行的任何时候让await方法返回而不一定必须等待线程结束。
  2. 如果使用线程池的话就不能使用join了
2.2 原理探究

在这里插入图片描述
CountDownLatch是使用也是AQS实现的,把计数器的值赋给了AQS的状态变量赋值给了AQS的状态变量state,也就是使用AQS的状态值来表示计数器值。

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
Sync(int count) {
  setState(count);
}

1.void await()方法:
当线程调用await方法后,会阻塞,直到一下情况之一发生:

  1. 所有线程调用了CountDownLatch对象的countDown方法后,也就是计数器的值为0
  2. 其他线程调用了当前线程的interrupt方法中断
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

可以看到,await方法委托了sync调用了AQS的acquireSharedInterruptibly方法

//AQS获取共享资源时可被中断的方法
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //如果线程被中断则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //查看当前计数器的值是否为0,为0则直接返回,否则进入AQS的队列等待
    if (tryAcquireShared(arg) < 0)
    	//不是0就让当前线程阻塞
        doAcquireSharedInterruptibly(arg);
}

//sync类实现的AQS接口
protected int tryAcquireShared(int acquires) {
 	return (getState() == 0) ? 1 : -1;
}

tryAcquireShared传递的参数arg参数没有被用到,调用tryAcquireShared的方法仅仅是为了检查当前状态值是不是为0,并没有调用CAS让当前状态值减1

2.void countDown()方法:
线程调用该方法后,计数器的值递减,递减后如果计数器值为0则唤醒所有因调用await方法被阻塞的线程,否则什么都不做

//CountDownLatch的方法
public void countDown() {
	//委托这个类内部的sync调用AQS方法
    sync.releaseShared(1);
}

CountDownlatch的countDown()方法委托sync调用了AQS的releaseShared方法

//AQS的方法
public final boolean releaseShared(int arg) {
	//调用sync实现的tryReleaseShared
    if (tryReleaseShared(arg)) {
    	//AQS释放资源方法
        doReleaseShared();
        return true;
    }
    return false;
}
//sync的方法
protected boolean tryReleaseShared(int releases) {
    //循环进行CAS,直到当前线程成功完成CAS使计数器值(状态值state)减1并更新到state
    for (;;) {
        int c = getState();
        //(1)如果当前状态值为0则直接返回
        if (c == 0)
            return false;
        //(2)使用CAS让计数器值减1
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

这里代码(1)看起来好像是多余的,其实不然,这是为了防止计数器值为0后,其他线程又调用了countDown方法,state就可能为负

三、同步器CyclicBarrier原理

CountDownLatch的计数器是一次性的,计数器为0后再次调用await和countDown就会直接返回。CyclicBarrier是回环屏障的意思,可以让一组线程全部到达一个状态后再全部同时执行。之所以叫做回环是因为所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等待所有线程都调用了await方法后,线程就会冲破屏障,继续向下执行

3.1例子:
public class CyclicBarrierTest {

    //创建一个CyclicBarrier实例,添加一个所有子线程全部到达屏障后执行的任务
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread()+ "task1 merge result");
        }
    });

    public static void main(String[] args) throws InterruptedException{

        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        //将线程A添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+ "task1-1");
                    System.out.println(Thread.currentThread()+ "enter in barrier");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+ "enter out barrier");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        //将线程B添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+ "task1-2");
                    System.out.println(Thread.currentThread()+ "enter in barrier");
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread()+ "enter out barrier");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        //关闭线程池
        executorService.shutdown();
    }
}

执行结果:
在这里插入图片描述

3.2原理探究

在这里插入图片描述

由类图知,CyclicBarrier是基于独占锁实现,本质还是AQS的。parties用来记录线程个数,这里表示多少线程调用await后,所有线程才会冲破屏障继续执行。而count一开始等于parties,当所有线程调用await方法就递减1,当count为0时就表示所有线程都到了屏障点。因为CyclicBarrier是可以被复用的,使用两个变量原因是parties始终用来记录总的线程个数,当count计数器变成0后,会将parties的值赋给count,从而进行复用。上面使用了lock保证了更新计数器count的原子性

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

barrierCommand是一个任务,这个任务执行时机是当所有线程都达到屏障点后。
变量generation内部有一个变量broken,用来记录当前屏障是否被打破。这里broken并没有声明为volatile,因为这个变量是在锁内使用的。

private static class Generation {
    boolean broken = false;
}

1.int await()方法:
当线程调用await方法后,会阻塞,直到一下情况之一发生:

  1. parties个线程都调用了await方法,也就是线程到达屏障点
  2. 其他线程调用了当前线程的interrupt方法中断
  3. 与屏障点关联的broken标志被设置为true,会抛出BrokenBarrierException异常,然后返回

内部调用了dowait方法:
dowait的部分代码如下

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
 		//(1)如果index==0说明所有线程都到了屏障点,此时执行初始化时传递的任务
        int index = --count;
        if (index == 0) {  // tripped
            boolean ranAction = false;//这里是构造函数里面的broken标志
            try {
 				//(2)执行任务
 				final Runnable command = barrierCommand;//构造函数里面的任务
                if (command != null)
                    command.run();
                ranAction = true;
                //(3)激活其他因调用await方法而被阻塞的线程,并重置CyclicBarrier
                nextGeneration();
                //返回
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // (4)如果index != 0
        for (;;) {
            try {
            	//(5)没有设置超时时间
                if (!timed)
                    trip.await();//trip为条件阻塞队列,进入后释放锁,其他被阻塞线程竞争
                //(6)设置了超时时间
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                ...
            }
				...
        }
    } finally {
        lock.unlock();
    }
}

private void nextGeneration() {
    // (7)唤醒条件队列里面阻塞线程
    trip.signalAll();
    // 重置CyclicBarrier
    count = parties;
    generation = new Generation();
}

线程首先获取独占锁lock,如果创建CycleBarrier是传递的参数是5,那么后面的几个线程会被阻塞。当前获取到锁的线程对count递减,递减后:

  1. 如果count !=0,执行代码(4),有超时时间和无超时时间的wait函数都会进入trip的条件阻塞队列。
  2. 如果count == 0,执行代码(2),如果创建了barrierCommand任务,则在唤醒前先执行任务,任务完成执行代码(3),唤醒其他所有线程,重置CycleBarrier,这5个线程就可以继续向下运行了。

四、同步器Semaphore原理

与CountDownLatch和CyclicBarrier不同,它内部的计数器是递增的,并且在一开始初始化Semaphore时可以指定一个初始值,但是并不需要知道同步的线程个数,而是在需要同步的地方调用acquire方法时指定需要同步的线程个数。

4.1例子:
public class SemaphoreTest {

    //创建一个Semaphore实例
    private static Semaphore semaphore = new Semaphore(0);//当前计数器的值为0

    public static void main(String[] args) throws InterruptedException{

        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        //将线程A添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+ "over");
                    //让计数器加1
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        //将线程B添加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread()+ "over");
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        //等待子线程执行完毕,返回
        //参数为2说明调用acquire会一直阻塞,直到信号量的计数变为2才会返回
        semaphore.acquire(2);
        System.out.println("all child thread over");

        //关闭线程池
        executorService.shutdown();
    }
}

执行结果:
在这里插入图片描述

4.2 原理探究

在这里插入图片描述
Semaphore还是使用AQS实现的。sync使用了公平和非公平策略,这个和ReentrantLock内部调用一样,默认非公平。这里的state也是表示当前持有者的信号量个数。

1.void acquire()方法:
调用该方法是希望获取一个信号量资源

  1. 如果当前信号量个数大于0,则当前信号量会减1,然后该方法直接返回
  2. 如果当前信号量个数等于0,则当前线程会被放入AQS的阻塞队列。
  3. 其他方法调用了当前线程的interrupt方法中断了当前线程时,则当前线程会抛出InterrupedException异常返回
public void acquire() throws InterruptedException {
	//传递参数为1,说明要获取1个信号量资源
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //如果线程中断,则抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //否则调用sync子类方法尝试获取
    if (tryAcquireShared(arg) < 0)
    	//如果获取失败则放入阻塞队列。然后再次尝试,如果失败则调用park方法挂起当前线程
        doAcquireSharedInterruptibly(arg);
}

非公平策略NonfairSync类的tryAcquireShared方法:

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
    	//获取当前信号量
        int available = getState();
        //计算当前剩余值
        int remaining = available - acquires;
        //如果当前剩余值小于0或者CAS设置成功则返回
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

如果剩余值小于0则说明当前信号量个数满足不了需求,那么直接返回负数,如果剩余值大于0,CAS后返回剩余值
公平策略fairSync类的tryAcquireShared方法:

protected int tryAcquireShared(int acquires) {
    for (;;) {
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

唯一区别就是使用了hasQueuedPredecessors函数,这个函数在上面的ReentrantLock介绍过。看当前线程节点的前驱节点是否在等待获取资源,如果是则自己放弃获取资源的权限,然后当前线程会被放入AQS阻塞队列,否则就去获取。

2.void acquire(int permits)方法:
该方法是获取permits个信号量值,上面的是获取一个,获取到直到满足这个个数的信号量才会返回。

public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    //只是传递的参数不同,上面传递的是1
    sync.acquireSharedInterruptibly(permits);
}

3.void release()方法:
该方法的作用是把当前Semaphore对象的信号量增加1,如果当前有线程因为调用aquire方法被阻塞而被放入了AQS的阻塞队列,则会根据公平策略选择一个信号量个数能被满足的线程进行激活,激活的线程会尝试获取刚新增的信号量

//Semaphore类的方法
public void release() {
    sync.releaseShared(1);
}
//AQS方法
public final boolean releaseShared(int arg) {
	//尝试获取资源
    if (tryReleaseShared(arg)) {
    	//资源释放成功则调用unpark方法唤醒AQS队列里面最先挂起的线程
    	//调用AQS的方法激活因为调用aquire方法而被阻塞的线程
        doReleaseShared();
        return true;
    }
    return false;
}
//Semaphore类的方法
protected final boolean tryReleaseShared(int releases) {
     for (;;) {
     	 //获取当前信号量
         int current = getState();
         //将当前信号量值增加release,这里为增加1
         int next = current + releases;
         if (next < current) // overflow
             throw new Error("Maximum permit count exceeded");
         //使用CAS保证更新信号量值的原子性
         if (compareAndSetState(current, next))
             return true;
     }
 }

4.void release(int permits)方法:
这个每次调用会在信号量值原来的基础上加permits

public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值