java三个线程同步器的用法与源码解析

三种同步器的功能

本文主要介绍三个线程同步器,它们的功能分别是:

  1. CountDownLatch:确保所有子线程执行完成以后再执行汇总, 内部有一个计数器,一个子线程执行完就倒数一下,倒数完后返回;
  2. CyclicBarrier:让一组线程全部达到一个状态以后再全部同时执行, 当所有线程执行完毕以后,重置CyclicBarrier的状态之后还可以被重用;
  3. Semaphore:大家都很熟悉的信号量,内部也有一个计数器但是是递增的,一开始不知道需要同步的线程个数,而是在需要同步的地方调用acquire方法指定需要同步的线程个数。

CountDownLatch

确保所有子线程执行完成以后再执行汇总, 内部有一个计数器,一个子线程执行完就倒数一下,倒数完后返回。首先看一个简单的使用方法:

//创建一个CountDownLatch实例
	private static 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() {
				// TODO Auto-generated method stub
				try {
					Thread.sleep(1000);
					System.out.println("child threadOne over!");
				}catch (InterruptedException e) {
					// TODO: handle exception
					e.printStackTrace();
				}finally {
					countDownLatch.countDown();
				}
			}
		});
		
		//将线程B添加到线程池
		executorService.submit(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					Thread.sleep(1000);
					System.out.println("child threadTwo over!");
				}catch(InterruptedException e) {
					e.printStackTrace();
				}finally {
					countDownLatch.countDown();
				}
			}
		});
		
		System.out.println("wait all child thread over!");
		//等待子线程执行完毕,返回
		countDownLatch.await();
		System.out.println("all child thread over!");
		executorService.shutdown();
	}

这里使用了一个线程池,先后创建了两个线程放进池中。然后在主线程中调用CountDownLatch的await()方法,等待两个子线程执行完毕。这个代码的输出大概是这样的:
在这里插入图片描述
也可能是这样的:
在这里插入图片描述
也就是说,threadOne和threadTwo的顺序可能会交换,因为这取决于线程池是怎么执行它们的。但是wait语句和all over语句一定是一个在开头一个在结尾。因为all over语句是在await()返回以后才输出的,相当于一个最后的汇总方法。
下面我们来看一下CountDownLatch内部的构造。它里面有一个私有类Sync,继承了AbstractQueuedSynchronizer(AQS)抽象类。实际上,它把计数器的值赋给了AQS的状态变量state,用它来表示计数器值。

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

Sync(int count) {
            setState(count);
        }

它的await()方法,也是委托sync调用了AQS的acquireSharedInterruptibly方法:

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

这个方法的代码如下:

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 如果线程被中断则抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 查看当前计数器值是否为0,为0则返回,不为0则进入AQS的队列等待
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

方法很简单,就是查看一下计数器,是0就直接返回,不是0就放到AQS的等待队列里面。后续如果计数器变成0了,CountDownLatch会调用AQS的释放资源方法,在那个方法里面唤醒等待队列里的await()线程并让其返回。
以上这个逻辑的实现在countDown()方法中:

public void countDown() {
        sync.releaseShared(1);
    }

啊,什么都没有,原来又是调用的sync里的releseShared方法。

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {  // 让计数器-1
            doReleaseShared();  // AQS的释放资源方法,唤醒await()线程使其返回
            return true;
        }
        return false;
    }

再来看一下让计数器-1的关键方法tryReleaseShared():

protected boolean tryReleaseShared(int releases) {
            // 循环进行CAS,直至成功完成CAS,使计数器值-1并更新到state
            for (;;) {
            	// 获取当前state值
                int c = getState();
                // 如果当前state就是0就返回false,因为计数器值已经为0,没办法-1
                if (c == 0)
                    return false;
                // CAS执行
                int nextc = c - 1;
                if (compareAndSetState(c, nextc))
                	// CAS执行成功,是0的话就返回true,否则返回false
                    return nextc == 0;
            }
        }

之所以需要判断当前state是否为0,是为了防止当计数器值已经为0时其他线程又调用了countDown()方法,使得计数器值变为负数。

相比起使用join()方法来实现线程间同步,CountDownLatch更具有灵活性和方便性。在初始化时设置计数器值,线程调用countDown()方法递减计数器值,当计数器值变为0时,激活由于调用await()方法而被阻塞的线程。

CyclicBarrier

功能:让一组线程全部达到一个状态以后再全部同时执行,当所有线程执行完毕以后,重置CyclicBarrier的状态之后还可以被重用。同样看一个使用方法:

//创建一个CyclicBarrier实例,并添加一个所有子线程全部到达屏障后执行的任务
	private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
		
		@Override
		public void run() {
			// TODO Auto-generated method stub
			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() {
				// TODO Auto-generated method stub
				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) {
					// TODO: handle exception
					e.printStackTrace();
				}
			}
		});
		
		//将线程B添加到线程池
		executorService.submit(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				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();
	}

也是创建了一个容量为2的线程池并放入两个线程,执行完后的结果是这样的:
在这里插入图片描述
可以看出,线程1先执行,到了await那一步后,线程1就“停下来”了,“等待”着线程2也执行到了await那一步,然后执行cyclicBarrier中的那个当所有子线程到达屏障后执行的任务(输出了merge result),然后两个线程才退出屏障。
那么它的可复用性又是怎么一回事呢?再看一个例子:

//创建一个CyclicBarrier实例,并添加一个所有子线程全部到达屏障后执行的任务
	private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
		
		@Override
		public void run() {
			// TODO Auto-generated method stub
			System.out.println(Thread.currentThread() + "task2 merge result");
		}
	});
	
	public static void main(String args[]) throws InterruptedException{
		ExecutorService executorService = Executors.newFixedThreadPool(2);
		//将线程A添加到线程池
		executorService.submit(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					System.out.println(Thread.currentThread() + "step1");
					cyclicBarrier.await();
					
					System.out.println(Thread.currentThread() + "step2");
					cyclicBarrier.await();
					
					System.out.println(Thread.currentThread() + "step3");
				}catch (Exception e) {
					// TODO: handle exception
					e.printStackTrace();
				}
			}
		});
		
		//将线程B添加到线程池
		executorService.submit(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					System.out.println(Thread.currentThread() + "step1");
					cyclicBarrier.await();
					
					System.out.println(Thread.currentThread() + "step2");
					cyclicBarrier.await();
					
					System.out.println(Thread.currentThread() + "step3");
				}catch(Exception e) {
					e.printStackTrace();
				}
			}
		});
		
		executorService.shutdown();
	}

以上,跟第一个例子比起来,其他的代码都没变,就是把两个线程try代码块中执行的任务换成了3步。这里我们要求,两个线程都完成了step1才可以开始step2,两个线程都完成了step2才可以开始step3。这就可以通过CyclicBarrier,像上面那样去完成。输出结果如下:
在这里插入图片描述
可以看到任务是按顺序执行的,并且cyclicBarrier输出了两次merge。这也告诉我们CyclicBarrier里面的Runnable中的run方法是只要cyclicBarrier到了屏障点就会被触动的。 而且在上面的代码中,没有看到我们显式地去重置cyclicBarrier的状态,所以它的状态应该是到了屏障点就会被触发重置
有了以上感性的认识以后,我们再来看它的源码。
CyclicBarrier类并没有继承什么类,也没有实现什么接口。它里面内置了一个私有类叫做Generation,也同样没有什么继承类和接口。它有以下几个变量:

	// 基于独占锁实现,也就是说底层还是基于AQS
    private final ReentrantLock lock = new ReentrantLock();
    // 条件变量
    private final Condition trip = lock.newCondition();
 	// parties这个变量用来记录线程个数,也就是有parties个线程调用await()方法后,线程们才会冲破屏障继续往下执行
    private final int parties;
    // 就是上面构造方法里的Runnable,在线程们冲破屏障时触发其run方法
    private final Runnable barrierCommand;
    // 初始化generation
    private Generation generation = new Generation();
    // 记录当前时刻实际上调用了await()方法的线程数,因为parties不能递减(还要被用来复用)
    private int count;

构造方法,没什么稀奇的,这里不多说。来看await()方法:

public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

好像就是调用了一个dowait()方法嘛。那么接着看dowait()方法:

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        // 拿到锁并且加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	// 得到当前的generation
            final Generation g = generation;

            if (g.broken)
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

			// 重点:count-1,如果此时count为0则证明到屏障点的线程数够了,该重置状态和触发barrierCommand的run方法了
            int index = --count;
            if (index == 0) {  // tripped
            	// 这个ranAction是用来记录是否调用了barrierCommand的run方法
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 激活其他因调用await()而被阻塞的线程,并重置CyclicBarrier
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // 如果count没到0
            for (;;) {
                try {
                	// 没有设置超时时间
                    if (!timed)
                        trip.await();
                    // 设置了超时时间
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
        	// 释放锁
            lock.unlock();
        }
    }

这么长的代码,这就是CyclicBarrier的核心功能代码了。从代码里我们可以看出,generation这个变量实际上是表示了CyclicBarrier过了几“代”,也就是经历了几个“调用await()的线程数从0到parties”的轮回。首先把count-1,如果count到了0就执行barrierCommand的run方法并调用nextGeneration开启下一个轮回(也就是唤醒条件队列里因await()而阻塞的所有线程并重置CyclicBarrier的状态),如果count没到0就调用trip.await()方法把当前线程塞进条件队列
看看nextGeneration的代码是不是如我们上面所说的那样:

private void nextGeneration() {
        // 唤醒条件队列里面等待的所有线程
        trip.signalAll();
        // 重置状态
        count = parties;
        generation = new Generation();
    }

确实如此。以上就是CyclicBarrier类的核心代码。

Semaphore

Semaphore比其他两个同步器优越的地方就在于它的计数器是递增的,只在调用acquire方法时才通过参数确定需要同步的线程到底有多少个,所以更加灵活。
用Semaphore实现第一个CountDownLatch的例子,开启两个子线程让他们执行,等到他俩执行完后主线程继续向下运行:

//创建一个Semaphore
	private static Semaphore semaphore = new Semaphore(0);
	
	public static void main(String args[]) throws InterruptedException{
		ExecutorService executorService = Executors.newFixedThreadPool(2);
		//将线程A添加到线程池
		executorService.submit(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					System.out.println(Thread.currentThread() + "over");
					semaphore.release();
				}catch (Exception e) {
					// TODO: handle exception
					e.printStackTrace();
				}
			}
		});
		
		//将线程B添加到线程池
		executorService.submit(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					System.out.println(Thread.currentThread() + "over");
					semaphore.release();
				}catch(Exception e) {
					e.printStackTrace();
				}
			}
		});
		semaphore.acquire(2);
		System.out.println("all child thread over");
		executorService.shutdown();
	}

其中,release()方法相当于让信号量的计数器值递增1,acquire(2)方法说明调用了该方法的线程会一直阻塞,直到信号量的计数变为2才会返回。
现在我们来看Semaphore的源码。它和之前的CountDownLatch差不多,也是内置了一个Sync类,继承了AQS抽象类。不同的是,这个Sync也是一个抽象类,它有两个实现:NonfairSync和FairSync。从字面意义我们能看出,这两种实现分别代表着非公平策略和公平策略。
这里再解释一下,非公平策略和公平策略的区别。如果一个线程被放入了阻塞队列,又有一个线程到来了,此时锁被释放,两个线程争夺锁,公平策略就是保证那个先来的线程先获取锁,而非公平策略则没有这个保证。Semaphore默认的实现是非公平的,看构造函数:

public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

如果要指定公平/非公平策略的话有:

public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

参数是true就是fair,反之则为Nonfair。
接下来看几个重点方法的实现。
acquire方法:当前线程调用该方法的目的是希望获取一个信号量资源。如果当前信号量个数大于0,则当前信号量的计数会减1,然后该方法直接返回。否则如果当前信号量个数等于0,则当前线程会被放入AQS的阻塞队列。

public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1); // 传递参数为1说明获取1个信号量资源
    }
    
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 如果线程被中断就抛异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 否则调用Sync子类方法尝试获取一个资源
        if (tryAcquireShared(arg) < 0)
        	// 如果获取失败则放入阻塞队列
            doAcquireSharedInterruptibly(arg);
    }

上面代码中的tryAcquireShared方法是Sync的两个子类实现的。非公平策略是这样的:

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
        	// 主要是调用的这个方法
            return nonfairTryAcquireShared(acquires);
        }
    }


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

这个代码大概就是说,获取就完事了,不管什么先来先得的。而公平性的FairSync就需要额外做一些处理来保证公平的实现:

protected int tryAcquireShared(int acquires) {
            for (;;) {
            	// 如果阻塞队列的前面有已经在等待的线程,就直接返回-1
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

也就是看当前线程节点的前驱节点是否也在等待获取该资源,如果是则自己放弃获取的权限,不跟他争。
最后看看release()方法,它的逻辑其实很简单,就是把信号量加1,再看看阻塞队列里面有没有线程,有的话就选一个信号量个数能被满足的线程把它激活。

public void release() {
		// 无参数的话默认释放一个资源
        sync.releaseShared(1);
    }

public final boolean releaseShared(int arg) {
		// 尝试释放资源,如果释放成功则
        if (tryReleaseShared(arg)) {
        	// 则调用park方法唤醒AQS队列里最先挂起的线程
            doReleaseShared();
            return true;
        }
        return false;
    }

protected final boolean tryReleaseShared(int releases) {
            for (;;) {
            	// 拿到当前的信号量
                int current = getState();
                int next = current + releases;
                // releases<0,抛异常
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                // 设置信号量
                if (compareAndSetState(current, next))
                    return true;
            }
        }

以上就是JUC中三个线程同步器的用法与一些源码的解析。下一次大概会写一些关于AQS和它的阻塞队列的笔记,毕竟所有的锁几乎都是基于AQS实现的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值