一、同步容器类
同步容器类包括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,因为仅在试图要取得元素时,该元素才存在;除非另一个线程试图移除某个 元素,否则也不能(使用任何方法)添加元素;也不能迭代队列,因为其中没有元素可用于迭代。
四、同步工具类
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、信号量
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();
}
}