Java并发编程实战:并发基础构建模块

一、同步容器类

同步容器类包括Vector和Hashtable以及一些功能相似的类,这些同步的封装器类是由Collections.synchronizedXxx等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来(即设为私有,使得外部无法直接访问,只能通过公有方法来访问),并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

同步容器类的问题:

同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保证复合操作。如下代码:

public static Object getLast(Vector list){
    int lastIndex = list.size() - 1;     //(1)      
    return list.get(lastIndex);          //(2)    
}
public static void deleteLast(Vector list){ 
    int lastIndex = list.size() - 1;     //(3)     
    list.remove(lastIndex);              //(4) 
}
考虑这种情况:线程A在包含1个元素的Vector上调用getLast并执行到(1)语句,同时线程B在同一个Vector上调用了deleteLast并已经执行完(4)语句,这是线程A在往下执行(2)语句时就会抛出异常。
同步容器类通过其自身的锁来保护它的每个方法,通过获得容器类的锁,我们可以使getLast和deleteLast成为原子操作,并确保Vector的大小在调用size和get之间不会发生变化,如下代码所示:

public static Object getLast(Vector list){
    synchronized(list){
        int lastIndex = list.size() - 1;           
        return list.get(lastIndex);    
    }            
}
public static void deleteLast(Vector list){
    synchronized(list){    
        int lastIndex = list.size() - 1;          
        list.remove(lastIndex);         
    }           
}
而在进行list的迭代时,也要加锁。

二、并发容器

同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种方法严重降低并发性。Java5.0提供了多种并发容器类来改进同步容器的性能。

1、ConcurrentHashMap

用来替代同步且基于散列的Map。在Hashtable和synchronizedMap中,获得Map的锁能防止其他线程访问这个Map。ConcurrentHashMap并不是每个方法都在同一个锁上同步并使得每次只有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。(ConcurrentHashMap详细介绍参照博客)
ConcurrentHashMap提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程中对容器加锁。
在ConcurrentMap中还声明了一下复合的操作,并保证了这些操作的原子性,如下代码所示:
public interface ConcurrentMap<K,V> extends Map<K,V>{
    //仅当K没有相应的映射值时才插入
    V putIfAbsent(K key,V value);

    //仅当K被映射到V时才移除
    boolean putIfAbsent(K key,V value);
 
    //仅当K被映射到oldValue时才替换为newValue
    boolean replace(K key,V oldValue,V newValue);

    //仅当K被映射到某个值时才替换为newValue
    V replace(K key,V newValue);
}

2、CopyOnWriteArrayList/CopyOnWriteArraySet

CopyOnWriteArrayList用于在遍历操作为主要操作的情况下代替同步的List。CopyOnWriteArraySet用于替代同步Set。
CopyOnWrite容器即写时复制的容器。当我们往一个容器添加元素时,不直接往当前容器添加,而是先将当前容器进行copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为读的容器时旧的容器,而我们操作的
容器是新复制的容器。所以CopyOnWrite容器也是一种读写分离的思想,读和写的是不同的容器。
以下代码是向CopyOnWriteArrayList中add元素的方法实现,可以发现在添加的时候是需要加锁的,否则多线程写的时候会copy出N个副本。
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
 }
CopyOnWrite并发容器用于读多写少的并发场景,也有很明显的缺点:
(1)内存占用问题:在进行写操作的时候,内存里会同时有两个对象的空间占用,旧的对象和新写入的对象。如果这些对象占用的内存比较大,那这个时候很有可能造成频繁的Yong GC和Full GC。针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗。
(2)数据一致性问题:CopyOnWrite只能保证数据的最终一致性,不能保证数据的实时一致性。

三、阻塞队列

阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以是有界的也可以是无界的,无界队列永远都不会充满,因此无界队列上的put方法也永远不会阻塞。
在类库中包含了BlockingQueue的多种实现,其中,LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,二者分别与LinkedList和ArrayList类似,但比同步List拥有更好的并发性能。
PriorityBlockingQueue是一个按优先级排序的队列,当希望按照某种顺序而不是FIFO来处理元素时,这个队列将非常有用。PriorityBlockingQueue既可以根据元素的自然顺序来比较元素(如果它们实现了Comparable方法),也可以使用Comparator来比较。
SynchronousQueue也实现了BlockingQueue,与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移除队列。SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。不能在同步队列上进行peek,因为仅在试图要取得元素时,该元素才存在;除非另一个线程试图移除某个 元素,否则也不能(使用任何方法)添加元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队线程元素;如果没有已排队线程,则不添加元素并且头为null。Java6增加了两种容器类型,Deque和BlockingDeque,它们分别对Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和LinkedBlockingDeque。阻塞队列适用于生产者-消费者设计,而双端队列适用于工作密取模式。所谓的工作密取就是每个消费者都有自己的双端队列,如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。工作密取非常适用于即使消费者也是生产者的问题。

四、同步工具类

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

1、闭锁

闭锁是一种同步工具类,可以延迟线程的进度直到其达到终止状态,闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。

(1)CountDownLatch是一种闭锁的实现,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生,而await方法等待计数器达到零,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。如下代码演示了CountDownLatch的用法

import java.util.concurrent.CountDownLatch;
class Task implements Runnable{
	@Override
	public void run() {
		long count = 0;
		// TODO Auto-generated method stub
		for(int i = 0; i < Integer.MAX_VALUE; i++){
			count += i;
		}
		System.out.println(count);
	}
}
public class TestClass{
	public void timeTasks(int nThreads,final Runnable task) throws InterruptedException{
		final CountDownLatch startGate = new CountDownLatch(1);
		final CountDownLatch endGate = new CountDownLatch(nThreads);
		for(int i = 0;i < nThreads; i++){
			Thread t = new Thread(){
				public void run(){
					try{
						startGate.await();
						try{
							task.run();
						}finally{
							endGate.countDown();
						}
					}catch(InterruptedException ignored){}
				}
			};
			t.start();
		}
		long start = System.currentTimeMillis();
		startGate.countDown();
		endGate.await();
		long end = System.currentTimeMillis();
		System.out.println("共运行了"+(end-start)+"毫秒");
	}
	public static void main(String[] args) throws InterruptedException{	
		Task task = new Task();
		TestClass test = new TestClass();
		test.timeTasks(10, task);
	}
}
(2)FutureTask也可以用做闭锁。FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable。

Future.get的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则get将阻塞直到任务进入完成状态,然后返回结果或者抛出异常。FutrureTask将计算结果从执行计算的线程传递到获取这个结果的线程。以下代码是FutureTask的用法:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Task implements Callable<Long>{
	@Override
	public Long call() throws Exception {
		long count = 0;
		// TODO Auto-generated method stub
		for(int i = 0; i < Integer.MAX_VALUE; i++){
			count += i;
		}
		return count;
	}
}
public class TestClass{
	public static void main(String[] args) throws InterruptedException{	
		Task task = new Task();
		FutureTask<Long> future = new FutureTask<Long>(task);
		Thread thread = new Thread(future);
		thread.start();
		try {
			long result = future.get();
			System.out.println(result);
		} catch (ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

2、信号量

计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。
Semaphore中管理着一组虚拟的许可(permit),许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可(或者直接被中断或者操作超时)。release方法将返回一个许可给信号量。计算信号量是一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量可以做互斥体,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。
Semaphore可以将任何一种容器变成有界阻塞容器,如下代码可以根据给定的bound来创建可以盛放bound个元素的容器:
class BoundedHashSet<T>{
	private final Set<T> set;
	private final Semaphore sem;
	public BoundedHashSet(int bound){
		this.set = Collections.synchronizedSet(new HashSet<T>());
		sem = new Semaphore(bound);
	}
	public boolean add(T o){
		boolean wasAdded = false;
		try {
			sem.acquire();	
			try{
				wasAdded = set.add(o);
			}
			finally{
				if(!wasAdded)
					sem.release();
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return wasAdded;
	}
	public boolean remove(Object o){
		boolean wasRemoved = set.remove(o);
		if(wasRemoved)
			sem.release();
		return wasRemoved;
	}
	public int getSize(){
		return set.size();
	}
}

3、栅栏

它允许一组线程互相等待,直到到达某个公共屏障点。利用栅栏,可以使线程互相等待,直到所有的线程都达到某一点,然后栅栏将打开,所有线程将通过栅栏继续执行。CyclicBarrier支持一个可选的Runnable参数,当线程通过栅栏时,runnable对象将被调用。构造函数CyclicBarrier(int parties,Runnable barrierAction),当线程在CyclicBarrier对象上调用await()方法时,栅栏的计数器将增加1,当计数器为parties时,栅栏将打开。

栅栏和闭锁的区别是:闭锁用于所有线程等待一个外部事件的发生;栅栏则是所有线程互相等待,直到所有线程都到达某一点时才打开栅栏,然后线程可以继续执行。

如下是和闭锁程序对应的用栅栏实现的代码:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
class Task implements Runnable{
	private CyclicBarrier barrier;
	public Task(CyclicBarrier barrier){
		this.barrier = barrier;
	}
	@Override
	public void run() {	
		try {
			long count = 0;
			// TODO Auto-generated method stub
			for(int i = 0; i < Integer.MAX_VALUE; i++){
				count += i;
			}
			System.out.println(count);
			barrier.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
public class TestClass{
	public static void main(String[] args) throws InterruptedException{			
		final long start = System.currentTimeMillis();
		CyclicBarrier barrier = new CyclicBarrier(10,new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				long end = System.currentTimeMillis();
				System.out.println("共运行了" + (end-start) + "毫秒");
			}
		});
		for(int i = 0; i < 10; i++){
			Thread t = new Thread(new Task(barrier));
			t.start();
		}
	}
}

另一种形式的栅栏是Exchanger,它是一种两方栅栏,各方在栅栏位置上交换数据。当两方执行不对称的操作时,Exchange会非常有用,例如当一个线程想缓冲区写入数据,而另一个线程从缓冲区读取数据时,这些线程可以使用Exchanger来汇合,并将满的缓冲区与空的缓冲区交换。如下代码生产者开启了10个线程生产数据,一旦达到7个数据就开始和消费者交换数据:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Exchanger;

class Worker implements Runnable{
	private CyclicBarrier barrier;
	List<Long> list;
	public Worker(CyclicBarrier barrier,List<Long> list){
		this.barrier = barrier;
		this.list = list;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub		
		try {
			Thread.sleep((long) (Math.random() * 5000));
			long count = 0;
			// TODO Auto-generated method stub
			for(int i = 0; i < 1000; i++){
				count += Math.random() * 1000;
			}
			list.add(count);
			barrier.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
class Producer extends Thread{
	List<Long> list = new ArrayList<Long>();
	Exchanger<List<Long>> exchanger = null;
	public Producer(Exchanger<List<Long>> exchanger){
		this.exchanger = exchanger;
	}
	@Override
	public void run() {		
		 CyclicBarrier barrier = new CyclicBarrier(7,new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				while(true){
				try {
					list = exchanger.exchange(list);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				}
			}
		});
		
		for(int i = 0; i < 10; i++){
			new Thread(new Worker(barrier, list)).start();
		}
	}
}
class Consumer extends Thread{
	List<Long> list = new ArrayList<Long>();
	Exchanger<List<Long>> exchanger = null;
	public Consumer(Exchanger<List<Long>> exchanger){
		this.exchanger = exchanger;
	}
	public void run(){
		
		try {
			list = exchanger.exchange(list);
			for(int i = 0; i < list.size(); i++){
				System.out.println(list.get(i));
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
}
public class TestClass{
	public static void main(String[] args) throws InterruptedException{			
		Exchanger<List<Long>> exchanger = new Exchanger<>();  
		new Producer(exchanger).start();
		new Consumer(exchanger).start();
	}
}







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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值