Java并发——并发工具

365 阅读4分钟

       

      同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)。 所有的同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态。

CountDownLatch

        闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。

CountDownLatch与join方法的区别

       在CountDownLatch出现之前一般都使用线程的join()方法来实现这一点,但是join方法不够灵活,不能够满足不同场景的需要。 与join方法的区别一个区别是,调用一个子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而CountDownLatch则使用计数器来允许子线程运行完毕或者在运行中递减计数,也就是CountDownLatch可以在子线程运行的任何时候让await方法返回而不一定必须等到线程结束。另外,使用线程池来管理线程时一般都是直接添加到线程池,这时候就没有办法再调用线程的join方法了,就是说相比join方法让我们对线程同步有更灵活的控制。

CountDownLatch使用场景

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行。
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
  • 等待真到某个操作的所有参与者(例如,在多玩家游戏中的所有玩家)都就绪再继续执行。在这种情况中,当所有玩家都准备就绪时,闭锁将到达结束状态。

 CountDownLatch是一种灵活的闭锁实现,可以在上述各种情况中使用,它可以使一个或多个线程等待一组事件发生。

CountDownLatch实现原理

       CountDownLatch是使用AQS实现的。在CountDownLatch内部维护了一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。

void await()方法

       当线程调用CountDownLatch对象的await方法后,当前线程会被阻塞,直到下面的情况之一发生才会返回:当所有线程都调用了CountDownLatch对象的countDown方法后,也就是计数器的值为0时;其他线程调用了当前线程的interupt()方法中断了当前线程,当前线程就会抛出InterruptedException异常,然后返回。

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

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

void countDown()方法

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

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

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

同步屏障CyclicBarrier

       CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier简介

       默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

static CyclicBarrier c = new CyclicBarrier(2);
public static void main(String[] args) {
	new Thread(new Runnable() {
		@Override
		public void run() {
			try {
				c.await();
			} catch (Exception e) {
			}
			System.out.println(1);
		}
	}).start();
	try {
		c.await();
	} catch (Exception e) {
	}
	System.out.println(2);
}

CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrier- Action),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。

static CyclicBarrier c = new CyclicBarrier(2, new Runnable() {
	@Override
	public void run() {
		System.out.println(3);
	}
});
public static void main(String[] args) {
	new Thread(new Runnable() {
		@Override
		public void run() {
			try {
				c.await();
			} catch (Exception e) {
			}
			System.out.println(1);
		}
	}).start();
	try {
		c.await();
	} catch (Exception e) {
	}
	System.out.println(2);
}
-----------------输出 3  1  2 ------------

CyclicBarrier的应用场景

     CyclicBarrie用于实现一些协议,例如几个家庭决定在某个地方集合:“所有人6:00在麦当劳碰头,到了以后要等其他人,之后再讨论下一步要做的事情”。 也可以用于多线程计算数据,最后合并计算结果的场景,例如,用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

与CountDownLatch的关键区别

        CountDownLatch的计数器是一次性的,一旦进入终止状态,就不能被重置。而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。 CountDownLatch用于等待事件,而CyclicBarrier用于等待其他线程。

CyclicBarrier实现原理

        CyclicBarrier基于独占锁实现,本质底层还是基于AQS的。parties用来记录线程个数,这里表示多少线程调用await后,所有线程才会冲破屏障继续往下运行。而count一开始等于parties,每当有线程调用await方法就递减1,当count为0时就表示所有线程都到了屏障点。使用两个变量的原因是,parties始终用来记录总的线程个数,当count计数器值变为0后,会将parties的值赋给count,从而进行复用。这两个变量是在构造CyclicBarrier对象时传递的。

int await()方法

        当前线程调用CyclicBarrier的该方法时会被阻塞,直到满足下面条件之一才会返回:parties个线程都调用了await)方法,也就是线程都到了屏障点;其他线程调用了当前线程的interrupt()方法中断了当前线程,则当前线程会抛出InteruptedException异常而返回;与当前屏障点关联的Generation对象的broken标志被设置为true时,会抛出BrokenBarrierException异常,然后返回。

信号量Semaphore

         Semaphore(信号量)是用来控制同时访问特定资源的线程数量,或者同时执行某个指定操作的数量,它通过协调各个线程,以保证合理的使用公共资源。可以把它比作是控制流量的红绿灯。比如马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待。

Semaphore的应用场景

       可以用于做流量控制,实现某种资源池,也可以使用Semaphore将任何一种容器变成有界阻塞容器。特别是公用资源有限的应用场景,比如数据库连接。我们可以构造一个固定长度的资源池,当资源池为空时,请求资源将会阻塞。

//线程数量
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors
		.newFixedThreadPool(THREAD_COUNT);
//连接数量
private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {

	for (int i = 0; i< THREAD_COUNT; i++) {
		threadPool.execute(new Runnable() {
			@Override
			public void run() {
				try {
					s.acquire();
					System.out.println("save data");
					s.release();
				} catch (InterruptedException e) {
				}
			}
		});
	}
	threadPool.shutdown();
}

将Semaphore的计数值初始化为池的大小,并在从池中获取一个资源之前首先调用acquire方法获取一个许可,在将资源返回给池之后调用release释放许可,那么acquire将一直阻塞直到资源池不为空。

Semaphore实现原理

        Semaphore中管理着一组虚拟的许可(permit),许可的初始数量可通过构造函数来指定。 在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可(或者直到被中断或者操作超时)。release方法将返回一个许可给信号量。 

void acquire()方法

        当前线程调用该方法的目的是希望获取一个信号量资源。如果当前信号量个数大于0,则当前信号量的计数会减1,然后该方法直接返回。否则如果当前信号量个数等于0,则当前线程会被放入AQS的阻塞队列。当其他线程调用了当前线程的interrupt()方法中断了当前线程时,则当前线程会抛出InterruptedException异常返回。

void release()方法

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

数据交换Exchanger

        Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过 exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也 执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

Exchanger的应用场景

        Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换 两人的数据,并使用交叉规则得出2个交配结果。Exchanger也可以用于校对工作,比如我们需 要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行 录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。

private static final Exchanger<String>exgr = new Exchanger<String>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
	threadPool.execute(new Runnable() {
		@Override
		public void run() {
			try {
				String A = "银行流水A";// A录入银行流水数据
				exgr.exchange(A);
			} catch (InterruptedException e) {
			}
		}
	});
	threadPool.execute(new Runnable() {
		@Override
		public void run() {
			try {
				String B = "银行流水B";// B录入银行流水数据
				String A = exgr.exchange("B");
				System.out.println("A和B数据是否一致:" + A.equals(B) 
                                                 + ",A录入的是:"
						+ A + ",B录入是:" + B);
			} catch (InterruptedException e) {
			}
		}
	});
	threadPool.shutdown();
}

如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)设置最大等待时长。

根据实际场景使用并发工具类会大大减少你在Java中使用wait、notify等来实现线程同步的代码量,在日常开发中当需要进行线程同步时使用这些同步类会节省很多代码并且可以保证正确性。

参考

Java并发编程实战
Java并发编程艺术
Java并发编程之美