-
面试题:
说一些线程安全的集合类: HashTable ConcurrentHashMap CopyOnWriteArrayList Vector(List的前身)
-
集合相关面试题
考点:在对集合进行遍历过程中,想删除集合中的元素,只能使用迭代器的删除方法,若在迭代过程中,使用集合的删除方法,会抛出并发修改异常 笔试题:编程题 1. 对一个list集合去重 eg:list: tom amy jack tom amy rose 去重后:tom amy jack rose 思路:遍历原集合,对遍历到的元素进行判断,判断元素是否已经出现,若已经出现,说明是重复元素,从原集合中删除.
List<String> firstList = new ArrayList<>(); //获取迭代器对象,对list进行遍历 Iterator<String> it = list.iterator(); while (it.hasNext()){ String ele = it.next(); if (firstList.contains(ele)){ it.remove(); }else{ firstList.add(ele); } } System.out.println(list);
Queue队列(FIFO-先进先出):
1.offer(E) :返回boolean数值表示是否成功, 入队操作,元素放到队列末尾。
2.poll() 返回队首元素并从队首中删除此元素, 如果队列为空则返回空
3.peek() 返回队首元素不会从队首中删除此元素, 如果队列为空则返回空
其他:
4.add(E): 添加 返回boolean数值表示是否成功, 入队操作,元素放到队列末尾。 和offer方法作用相同,add来自于Collection接口中的方法。但一般建议用offer,因为add添加不成功会抛异常。
5.element:返回队首元素不会从队首中删除此元素, 如果队列为空则抛出异常,和peek方法作用相同。element来自于Collection接口中的方法。但一般建议用peek,因为element添加不成功会抛异常。
6.remove(): 返回队首元素并从队首中删除此元素, 如果队列为空则抛出异常,和poll方法作用相同。remove来自于Collection接口中的方法。但一般建议用poll,因为remove添加不成功会抛异常。
建议使用offer插入, 用poll移除, 用peek检查(获取/查询)。 因为他们不会抛出异常。
从队尾放元素,从队首取元素。
用法案例:
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
//add()和remove()方法在失败的时候会抛出异常(不推荐)
Queue<String> queue = new LinkedList<String>();
//添加元素
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
queue.offer("e");
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("poll="+queue.poll()); //返回第一个元素,并在队列中删除
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("element="+queue.element()); //返回第一个元素
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("peek="+queue.peek()); //返回第一个元素
for(String q : queue){
System.out.println(q);
}
}
}
Dequeue双端队列:
Queue是一个接口,LinkedList是实现了Queue的一个类。 Deque也是一个接口,LinkedList也实现了Deque接口。
虽然Deque继承自Queue,但是使用Deque时,最好不要调用offer( ),而用offerLast( )。
如果直接写deque.offer( ),就需要思考,而用offerLast( )就能直接看出是添加到队尾。
因此使用Deque时,推荐总是明确使用offerLast、offerFirst、pollFirst、pollLast。
public class Test {
public static void main(String[] args) {
Deque<Integer> deque = new LinkedList<>();
deque.add(1);
deque.add(2);
deque.add(3);
System.out.println("原始队列:");
System.out.println(deque);
System.out.println("队列头添加元素:");
deque.addFirst(4);
System.out.println(deque);
System.out.println("队列尾添加元素:");
deque.addLast(5);
System.out.println(deque);
System.out.println("队列头删除元素:");
deque.removeFirst();
System.out.println(deque);
System.out.println("队列尾删除元素:");
deque.removeLast();
System.out.println(deque);
}
}
栈Stack(FILO-先进后出)
当用LinkedLists时调用它的双端队列或队列对应的方法时时它就是双端队列或队列,调用它的栈对应的方法时它就是栈。
Java中Stack类从Vector类继承,底层是用数组实现的线程安全的栈。栈是一种后进先出(LIFO)的容器,常用的操作push/pop/peek。
不过Java中用来表达栈的功能(push/pop/peek),更适用的是使用双端队列接口Deque,并用实现类ArrayDeque/LinkedList来进行初始化。
Deque<Integer> stack = new ArrayDeque<>(); Deque<Integer> stack = new LinkedList<>();
不用Stack至少有以下两点原因 1、从性能上来说应该使用Deque代替Stack。
Stack和Vector都是线程安全的,其实多数情况下并不需要做到线程安全,因此没有必要使用Stack。毕竟保证线程安全需要上锁,有额外的系统开销。
2、Stack从Vector继承是个历史遗留问题,JDK官方已建议优先使用Deque的实现类来代替Stack。
Stack从Vector继承的一个副作用是,暴露了set/get方法,可以进行随机位置的访问,这与Stack只能从尾巴上进行增减的本意相悖。
此外,Deque在转成ArrayList或者stream的时候保持了“后进先出”的语义,而Stack因为是从Vector继承,没有这个语义。
Stack<Integer> stack = new Stack<>();
Deque<Integer> deque = new ArrayDeque<>();
stack.push(1);
stack.push(2);
deque.push(1);
deque.push(2);
System.out.println(new ArrayList<>(stack)); // [1,2]
List<Integer> list1 = stack.stream().collect(Collectors.toList());//[1,2]
// deque转成ArrayList或stream时保留了“后进先出”的语义
System.out.println(new ArrayList<>(deque)); // [2,1]
List<Integer> list2 = deque.stream().collect(Collectors.toList());//[2,1]
该用ArrayDeque还是LinkedList? ArrayDeque和LinkedList这两者底层,一个采用数组存储,一个采用链表存储;
数组存储,容量不够时需要扩容和数组拷贝,通常容量不会填满,会有空间浪费;
链表存储,每次push都需要new Node节点,并且node节点里面有prev和next成员,也会有额外的空间占用。
那么问题来了,在用作栈时到底用ArrayDeque好还是LinkedList好呢?
注意到ArrayDeque源码注释中有一句话: This class is likely to be faster than {@link Stack} when used as a stack, and faster than {@link LinkedList} when used as a queue.
ArrayDeque用作栈时比Stack快没有疑问,用作队列的时候似乎也会比LinkedList快!
笔者经过50W数据量的测试,发现两者性能基本接近,ArrayDeque平均耗时在18-24ms,LinkedList耗时平均在20-28ms。
如果数据量上升到100W的话,ArrayDeque的优势会更明显。
结论:ArrayDeque会略胜一筹,不过差别通常可以忽略
public static void main(String[] args) {
int length = 500000;
int max = length;
// 生成一个长度为length,值从1~max的随机数组
int[] data = new RandomIntArray(length,1,length,max).next();
int loopCount = 10;
long t1, t2;
t1 = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
// testArrayDeque(data);
testLinkedList(data);
}
t2 = System.currentTimeMillis();
// 测试loopCount次取平均结果
System.out.println("timeTaken: " + String.format("%.1f", (t2-t1)/(double)loopCount));
}
public static void testArrayDeque(int[] data) {
int length = data.length;
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < length/2; i++) {
stack.push(data[i]);
stack.push(data[i+1]);
stack.pop();
stack.push(stack.peek()+1);
}
}
public static void testLinkedList(int[] data) {
int length = data.length;
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < length/2; i++) {
stack.push(data[i]);
stack.push(data[i+1]);
stack.pop();
stack.push(stack.peek()+1);
}
}
阻塞式队列BlockingQueue接口:
阻塞式队列的put和take方法是线程安全的。
API:
接口: BlockingQueue<E> 实现类: LinkedBlockingQueue<E> -通过该类的有参构造方法可以设置阻塞队列的容量 带有阻塞功能的方式: put(E):void 存入元素,若队列满,则让生产者线程阻塞 take():E 出队元素,若队列空,则让消费者线程阻塞
public LinkedBlockingDeque() public LinkedBlockingDeque(int capacity) public LinkedBlockingDeque(Collection<? extends E> c)
阻塞式队列的作用:实现生产者和消费者线程的分离解耦,从而提高二者各自的执行效率.
举例1:服务器处理消息
举例2:日志记录
总结:LinkedBlockingQueue和LinkedBlockingDeque区别 两个都是队列,只不过前者只能一端出一端入,后者则可以两端同时出入,并且都是结构改变线程安全的队列。
其实两个队列从实现思想上比较容易理解,有以下特点: ①、链表结构(动态数组) ②、通过ReentrantLock实现锁 ③、利用Condition实现队列的阻塞等待,唤醒
测试1:队列空时,消费者线程阻塞
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
Integer i = queue.take(); //此处阻塞
System.out.println("取出元素");
System.out.println(i);
测试2:队列满时,生产者线程阻塞
//生产者阻塞
queue.put(1);
queue.put(2);
queue.put(3);
queue.put(4);
queue.put(5);
System.out.println(queue);
queue.put(6); //此处阻塞
场景1:阻塞式队列为空,线程1不停的存,一会儿就把队列填满了。 线程2每隔2秒取出一个元素,就会发现线程2每2秒取出一个元素后,队列就会空出一个空间,就会立即被线程1存入一个新元素
//场景:生产者快速的产生数据并存入队列,消费者每隔2s取出一个数据,最终造成的结果是程序启动, //在不到2s的时间,生产者就可以将队列填满,之后生产者阻塞,直到2s到,消费者取出一一个数据后, //生产者才会接触阻塞,继续存入一- 个数据,之后就绪进入阻塞状态.
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class TestBlockingQueue {
public static void main(String[] args) {
BlockingQueue<Integer> queue=new LinkedBlockingDeque<Integer>(10);
Thread productor=new Thread(){
public void run(){
while(true){
try {
//Thread.sleep(2000);
int ran=(int)(Math.random()*10000);
queue.put(ran);
System.out.println(queue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
productor.start();
Thread consumer=new Thread(){
public void run(){
while(true){
try {
Thread.sleep(3000);
Integer i=queue.take();
//System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
}
}
// [2318]
// [2318, 8661]
// [2318, 8661, 5268]
// [2318, 8661, 5268, 7060]
// [2318, 8661, 5268, 7060, 446]
// [2318, 8661, 5268, 7060, 446, 7435]
// [2318, 8661, 5268, 7060, 446, 7435, 9730]
// [2318, 8661, 5268, 7060, 446, 7435, 9730, 4729]
// [2318, 8661, 5268, 7060, 446, 7435, 9730, 4729, 5055]
// [2318, 8661, 5268, 7060, 446, 7435, 9730, 4729, 5055, 7685]
//-------------------------------------------------------------------------
// [8661, 5268, 7060, 446, 7435, 9730, 4729, 5055, 7685, 308]
// [5268, 7060, 446, 7435, 9730, 4729, 5055, 7685, 308, 1178]
// [7060, 446, 7435, 9730, 4729, 5055, 7685, 308, 1178, 3078]
// [446, 7435, 9730, 4729, 5055, 7685, 308, 1178, 3078, 7418]
// [7435, 9730, 4729, 5055, 7685, 308, 1178, 3078, 7418, 6566]
// [9730, 4729, 5055, 7685, 308, 1178, 3078, 7418, 6566, 8209]
// [4729, 5055, 7685, 308, 1178, 3078, 7418, 6566, 8209, 9649]
// [5055, 7685, 308, 1178, 3078, 7418, 6566, 8209, 9649, 1717]
场景2: 阻塞式队列为空,线程2不停的取,因为队列为空取不出来,所以一直阻塞。 线程1每隔2秒存入一个元素,就会发现线程1每2秒存入一个元素就会立即被线程2取出
场景:生产者每隔2s产生-一个数据并存入队列,消费者一直不停的取数据,则没
//到2s期间,消费者线程就是阻塞的,当2s-到, 生产者存入-一个数据,消费者阻
//塞解除,取出数据,然后继续进入阻塞状态,以此类推
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class TestBlockingQueue2 {
public static void main(String[] args) {
BlockingQueue<Integer> queue=new LinkedBlockingDeque<Integer>(10);
Thread productor=new Thread(){
public void run(){
while(true){
try {
Thread.sleep(2000);
int ran=(int)(Math.random()*10000);
queue.put(ran);
System.out.println(queue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
productor.start();
Thread consumer=new Thread(){
public void run(){
while(true){
try {
Integer i=queue.take();
//System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
}
}
本题中两个消费者线程执行的任务是一样的,不能创建2个匿名内部类来表示2。个消费者,代码完全重复,解决办法是:
答案:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class TrainBlockingQueue {
public static void main(String[] args) {
BlockingQueue<Integer> queue=new LinkedBlockingDeque<Integer>();
Runnable r=new Runnable() {
public void run() {
try {
while(true){
if(queue.size()>=10){
System.out.println(
Thread.currentThread().getName()+"取出元素"+queue.take());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1=new Thread(r);
Thread t2=new Thread(r);
t1.start();
t2.start();
Thread productor=new Thread(){
public void run() {
try {
while(true) {
Thread.sleep(1000);
int ran=(int)(Math.random()*10000);
queue.put(ran);
System.out.println(queue);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
productor.start();
}
}