同步容器与并发容器
Vector、HashTable -- JDK提供的同步容器类
Collections.synchronizedXXX本质是对相应的容器进行包装
同步容器类的缺点
在单独使用里面的方法的时候,可以保证线程安全,但是,复合操作需要额外加锁来保证线程安全,使用Iterator迭代容器 或使用for-each遍历容器,在迭代过程中修改容器会抛出ConcurrentModificationException异常。想要避免出现 ConcurrentNodificationException,就必须在迭代过程中持有容器的锁。但是若容器较大,则迭代时间也会越长。那么需要 访问该容器的其他线程会长时间等待。从而会极大降低性能。
若不希望在迭代期间对容器加锁,可以使用"克隆"容器的方式。使用线程封闭,由于其他线程不会对容器进行修改,可以避 免 ConcurrentModificationException。但是在创建副本的时候,存在较大性能开销。
toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都会隐式的Iterate,也即可能抛出 ConcurrentModificationException。
并发容器
CopyOnWrite、Concurrent、BolockingQueue
根据具体场景进行设计,尽量避免使用锁,提高容器的并发访问性。
ConcurrentBolckingQueue:基于queue实现的FIFO队列。队列为空,取操作会被阻塞。
ConcurrentLinkedQueue,队列为空,取的时候直接返回空
LinkedBolckingQueue使用
在并发编程中LinkedBolckingQueue使用的非常频繁。因其可以作为生产者消费者的中间商
add() 实际上调用offer,区别在队满的时候,add会抛异常。
offer() 队如果满了,直接入队失败。
put() 在队满的时候,会进入阻塞状态。
remove() 实际上调用poll,区别在队空的时候会抛异常,poll直接返回null。
poll() 在队为空的时候直接返回null。
take() 在队列为空时,会进入等待状态。
1.Vector 使用 简单Demo
import java.util.Iterator;
import java.util.Vector;
public class VectorDemo {
public static void main(String[] args) {
//实现方法都是synchronized修饰
Vector<String> vector = new Vector<>();
for (int i=0;i<1000;i++){
vector.add("demo"+i);
}
// vector.forEach(e->
// {
// if(e.equals("demo3"))
// vector.remove(e);
// }
// );
//单线程运行没毛病
// Iterator<String> iterator = vector.iterator();
// while(iterator.hasNext()){
// String next = iterator.next();
// if("demo3".equals(next)){
// iterator.remove();
// }
// }
/**多线程环境会报错*/
// Iterator<String> iterator = vector.iterator();
// for(int i = 0;i<10;i++){
// new Thread(()->{
// while (iterator.hasNext()){
// String next = iterator.next();
// if("demo3".equals(next)){
// iterator.remove();
// }
// }
// }).start();
// }
//多线程运行加锁没毛病
Iterator<String> iterator = vector.iterator();
for(int i = 0;i<10;i++){
new Thread(()->{
synchronized (iterator){
while (iterator.hasNext()){
String next = iterator.next();
if("demo3".equals(next)){
iterator.remove();
}
}
}
}).start();
}
}
}
直接使用forEach在遍历的时候做移除操作会报下列错误,原因没记错的话是因为,列表的遍历的时候一开始就读取了列表的总存储量,但是中途做了修改容器存储量的操作,但是外面遍历的代码感知不到内部容器的改变还是按原来的存储量去遍历,所以就报错了,举个简单例子,假设一个容器里面本来里面存了三个值,现在来遍历整个容器的值,你会按下标0,1,2这么去依次取,中间有某个遍历操作移除了一个值,其实容器里面现在只有2个值也就是只有下标0,1,但是遍历的代码不知道,它还想继续取下标为2的值,肯定是不对的。
单线程环境下使用Iterator去遍历,遍历的时候就不会出现上面的问题,因为Iterator的遍历条件是判断下一个元素是否存在。时刻感知容器的变化。但是判断和移除是两个操作,不是原子操作,所以在多线程环境下,可能两线程在判断的时候都是有值的。但是其中一个线程做了remove操作后恰好移除了最后一个值,这个时候另外一个线程不知道还在对容器做remove操作就会报如下错误。
解决这个错误的方法就是将判断是都有值和移除操作加锁,也就是上面代码中没被注释掉的部分。
2.Collections.synchronizedXX使用
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedListDemo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
//synchronized 代码块同步
List<String> synList = Collections.synchronizedList(list);
}
}
没什么特别的其实就是在操作的get、set等方法的时候加了synchronized同步块,和Vector的实现大同小异,Vector是在get()、add()操作时用synchronized修饰方法。
3.LinkedBlockingQueue常用方法
import java.util.concurrent.LinkedBlockingQueue;
public class LinkedBlockingQueueDemo {
public static void main(String[] args) {
LinkedBlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
linkedBlockingQueue.add("111");
linkedBlockingQueue.offer("222");
try {
linkedBlockingQueue.put("333");
}catch (InterruptedException e){
e.printStackTrace();
}
String remove = linkedBlockingQueue.remove();
System.out.println("remove:"+remove);
String poll = linkedBlockingQueue.poll();
System.out.println("poll:"+poll);
try {
String take = linkedBlockingQueue.take();
System.out.println("take:"+take);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
LinkedBolckingQueue方法的详细解析在上面已经说清楚了,它提供了线程安全的设置和取值方法,put()和take(),会在队满和队空时进入阻塞状态。