Java集合面试题
集合
一、数组和集合的主要的区别(重点)
(1) 数组可以存储基本数据类型和对象,而集合中只能存储对象(可以以包装类形式存储基本数据类型).
(2) 数组的长度是固定的,集合长度是可以动态改变的
(3) 定义数组时必须指定数组元素类型,集合默认其中所有元素都是 Object
(4) 无法直接获取数组数组实际存储的元素个数,length 用来获取数组的长度,但可以通过size( )直接获取集合实际存储的元素个数
(5) 集合有多种实现方式和不同的适用场合,而不像数组仅采用分配连续的空间方式
(6) 集合以接口和类的形式存在.具有封装,继承和多态等类的特性,通过简单的方法和属性调用即可实现各种复杂的操作,大大提高软件的开发效率
二、数据结构中,数组与链表有哪些区别?为什么?(重点)
1、存取方式上,数组可以顺序存取或者随机存取,而链表只能顺序存取;
2、存储位置上,数组逻辑上相邻的元素在物理存储位置上也相邻,而链表不一定;
3、存储空间上,链表由于带有指针域,存储密度不如数组大;
4、按序号查找时,数组可以随机访问,时间复杂度为O(1),而链表不支持随机访问,平均需要O(n);
5、按值查找时,若数组无序,数组和链表时间复杂度均为O(1),但是当数组有序时,可以采用折半查找将时间复杂度降为O(logn);
6、插入和删除时,数组平均需要移动n/2个元素,而链表只需修改指针即可;
7、空间分配方面:
数组在静态存储分配情形下,存储元素数量受限制,动态存储分配情形下,虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且如果内存中没有更大块连续存储空间将导致分配失败;
链表存储的节点空间只在需要的时候申请分配,只要内存中有空间就可以分配,操作比较灵活高效;
三、HashMap原理
1.HashMap****的实现方式是数组+链表,主体是数组,链表只是用来做辅助的。
2.HashMap****的主干是一个Node数组,每一个Node包含一个Key-Value键值对。
3.HashMap****通过计算Key的哈希值来确实Key-Value在HashMap中的插入、查询位置
4.HashMap****最重要和复杂的方法put方法,扩容方法,get相对来说比较简单。
5.HashMap****是基于哈希表的Map接口的非同步实现
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象
四、HashMap和Hashtable的理解与区别
Hashtable是java一开始发布时就提供的键值映射的数据结构,而HashMap产生于JDK1.2。虽然Hashtable比HashMap出现的早一些,但是现在Hashtable基本上已经被弃用了。而HashMap已经成为应用最为广泛的一种数据类型了。造成这样的原因一方面是因为Hashtable是线程安全的,效率比较低。也可能是Hashtable开始设计的时候没有遵循驼峰命名法(手动笑哭)。
1、父类不同:
HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary(已被废弃,详情看源代码)。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
2、 Hashtable比HashMap多提供了elments() 和contains() 两个方法。
elments() 方法继承自Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举。
contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实上,contansValue() 就只是调用了一下contains() 方法。
3、null值问题
Hashtable既不支持Null key也不支持Null value。Hashtable的put()方法的注释中有说明 。
HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
4、线程安全性
Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步
HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。具体的原因在下一篇文章中会详细进行分析。使用HashMap时就必须要自己增加同步处理,
虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
tip:HashMap是JDk1.2之后有的,而在JDK1.5中,伟大的Doug Lea给我们带来了concurrent包,从此Map也有安全的了。也就就是有了ConcurrentHashMap(关于这个的理解下次有机会再写,或自行百度)
5、初始容量不同
Hashtable的初始长度是11,之后每次扩充容量变为之前的2n+1(n为上一次的长度)
而HashMap的初始长度为16,之后每次扩充变为原来的两倍
创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。
6、计算哈希值的方法不同
为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置
Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。 然而除法运算是非常耗费时间的。效率很低
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
7、遍历方式不同
Hashtable、HashMap都使用了Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
HashMap的Iterator是fail-fast迭代器。当有其它线程改变了HashMap的结构(增加,删除,修改元素),将会抛出ConcurrentModificationException。不过,通过Iterator的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。
JDK8之前的版本中,Hashtable是没有fast-fail机制的。在JDK8及以后的版本中 ,Hashtable也是使用fast-fail的。(此处可以去看一下1.5和1.8JDK源码的对比)
五、ArrayList, LinkedList的区别 及应用场景
1、ArrayList是实现了基于动态数组,和数据结构,而LinkedList是基于链表的数据结构;
2、对于随机访问get和set,ArrayList优于LinkedList,是因为LinkedList要移动指针;
3、对于添加和删除操作,add和remove,一般大家都会说LinkedList要比Arraylist快,是因为Arraylist要移动数据,但实际情况并非这样,对于添加或删除,LinkedList和ArrayList并不能明确说明谁快谁慢。
4、从利用效率来看,ArrayList*性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList*性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
5、ArrayList主要控件开销在于需要在List列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
6、都是不同步的,也就是不保证线程安全
什么时候用哪个?
首先ArrayList和linkedList 是两个集合类,用于存储一系列的对象引用
1.ArrayList的内部实现是基于基础的对象数组的,因此,它使用get方法访问列表中的任意一个元素时(random access),它的速度要比LinkedList快
2.LinkedList中的get方法是按照顺序从列表的一端开始检查,直到另外一端。对LinkedList而言,访问列表中的某个指定元素没有更快的方法了。
3.当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;
4.当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
六、List,Set,Map的特点及应用场景
1、List,Set都是继承自Collection接口,Map则不是
2、List特点:元素有放入顺序,元素可重复 ,
Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
3.Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
4.Map适合储存键值对的数据
5.线程安全集合类与非线程安全集合类
LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
HashMap是非线程安全的,Hashtable是线程安全的;
list和set是实现了collection接口的。
List:1.可以允许重复的对象。
2.可以插入多个null元素。
3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set:1.不允许重复对象
2.无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
\3. 只允许一个 null 元素
4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
Map:
1.Map不是collection的子接口或者实现类。Map是一个接口。
2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
\3. TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
\4. Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
5.Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)
2.面试题:什么场景下使用list,set,map呢?
(或者会问为什么这里要用list、或者set、map,这里回答它们的优缺点就可以了)
答:
如果你经常会使用索引来对容器中的元素进行访问,那么 List 是你的正确的选择。如果你已经知道索引了的话,那么 List 的实现类比如 ArrayList 可以提供更快速的访问,如果经常添加删除元素的,那么肯定要选择LinkedList。
如果你想容器中的元素能够按照它们插入的次序进行有序存储,那么还是 List,因为 List 是一个有序容器,它按照插入顺序进行存储。
如果你想保证插入元素的唯一性,也就是你不想有重复值的出现,那么可以选择一个 Set 的实现类,比如 HashSet、LinkedHashSet 或者 TreeSet。所有 Set 的实现类都遵循了统一约束比如唯一性,而且还提供了额外的特性比如 TreeSet 还是一个 SortedSet,所有存储于 TreeSet 中的元素可以使用 Java 里的 Comparator 或者 Comparable 进行排序。LinkedHashSet 也按照元素的插入顺序对它们进行存储。
如果你以键和值的形式进行数据存储那么 Map 是你正确的选择。你可以根据你的后续需要从 Hashtable、HashMap、TreeMap 中进行选择。
1.我们知道了列表要实现排序,需要重写comparable接口的compareTo的方法。
List集合序列排序
1.Comparable自然规则排序
//在自定义类Student里面实现Comparable接口,并重写抽象方法compareTo(Student o);
//Collections.sort(集合);
sort(List)方法中List中的T必须实现Comparable接口,然后实现compareTo()方法,该方法的返回值0代表相等,正数表示大于,负数表示小于
2.Comparator专门规则排序(临时排序)
//新建一个实现了Comparator接口的类,并重写抽象方法compare(Student o1, Student o2);
//Collections.sort(集合,实现了Comparator接口的类的实例化对象);
Set集合排序
1、 自然排序
TreeSet使用元素的自然顺序对元素进行排序,或者根据创建set时提供的Comparator进行排序,具体取决于使用的构造方法。通俗一点来说,就是可以按照排序后的列表显示,也可以按照指定的规则排序。
2、选择器排序:
就是在创建Set集合对象时给它一个选择器,也就是构造函数传参(传一个Comparator的子对象),给集合一个选择排序的标准,具体如何凭借什么选择就看重写Comparator的方法逻辑是怎样实现的
Map排序
TreeMap
TreeMap默认是升序的,如果我们需要改变排序方式,则需要使用比较器:Comparator
Comparator可以对集合对象或者数组进行排序的比较器接口,实现该接口的public compare(T o1,T o2)方法即可实现排序,该方法主要是根据第一个参数o1,小于、等于或者大于o2分别返回负整数、0或者正整数。
HashMap
我们都是HashMap的值是没有顺序的,他是按照key的HashCode来实现的。对于这个无序的HashMap我们要怎么来实现排序呢?参照TreeMap的value排序,我们一样的也可以实现HashMap的排序。
七、java.util.concurrent 包下常用的类
0.0982018.12.21 16:33:33字数 3405阅读 361
1.Callable
Callable与Runnable类似,理解Callable可以从比较其与Runnable的区别开始:
1.从使用上:实现的Callable的类需要实现call()方法,此方法有返回对象V;而Runnable的子类需要实现run()方法,但没有返回值;
2.如果直接调用Callable的子类的call()方法,代码是同步顺序执行的;而Runnable的子类是线程,是代码异步执行。
3.将Callable子类submit()给线程池去运行,那么在时间上几个Callable的子类的执行是异步的。
即:如果一个Callable执行需要5s,那么直接调用Callable.call(),执行3次需要15s;
而将Callable子类交个线程执行3次,在池可用的情况下,只需要5s。这就是基本的将任务拆分异步执行的做法。
4.callable与future的组合用法:
(什么是Future?Future 表示异步计算的结果。其用于获取线程池执行callable后的结果,这个结果封装为Future类。详细可以参看Future的API,有示例。)
一种就像上面所说,对一个大任务进行分制处理;
另一种就是对一个任务的多种实现方法共同执行,任何一个返回计算结果,则其他的任务就没有执行的必要。选取耗时最少的结果执行。
2.Semaphore
一个计数信号量,主要用于控制多线程对共同资源库访问的限制。
典型的实例:1)公共厕所的蹲位……,10人等待5个蹲位的测试,满员后就只能出一个进一个。
2)地下车位,要有空余才能放行
3)共享文件IO数等
与线程池的区别:线程池是控制线程的数量,信号量是控制共享资源的并发访问量。
实例:Semaphore avialable = new Semaphore(int x,boolean y);
x:可用资源数;y:公平竞争或非公平竞争(公平竞争会导致排队,等待最久的线程先获取资源)
用法:在获取工作资源前,用Semaphore.acquire()获取资源,如果资源不可用则阻塞,直到获取资源;操作完后,用Semaphore.release()归还资源
代码示例:(具体管理资源池的示例,可以参考API的示例)
public class SemaphoreTest {
private static final int NUMBER = 5;
private static final Semaphore avialable = new Semaphore(NUMBER,true);
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
Runnable r = new Runnable() {
public void run() {
try {
avialable.acquire();
Thread.sleep(10*1000);
System.out.println(getNow()+ "--"
+ Thread.currentThread().getName()+ "--执行完毕");
avialable.release();
}catch(InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println(avialable.availablePermits());
for(int i = 0;i < 10;i++) {
pool.execute(r);
}
System.out.println(avialable.availablePermits());
pool.shutdown();
}
public static String getNow() {
SimpleDataFormat sdf = new SimpleDataFormat("mm:ss");
return sdf.format(new Data());
}
}
3.ReentrantLock与Condition
1.ReentrantLock:可重入互斥锁。使用上与synchronized关键字对比理解:
1.1)synchronized示例:
synchronized(object){
//do process to object
}
1.2)ReentrantLock示例:(api)
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
由1.1)和1.2)的示例很好理解,ReetantLock也就是一个锁,线程执行某段代码时,需要争用此类实例的锁,用完后要显示的释放此锁。
至于具体区别,后面在说……
2.Condition:此类是同步的条件对象,每个Condition实例绑定到一个ReetrantLock中,以便争用同一个锁的多线程之间可以通过Condition的状态来获取通知。
注意:使用Condition前,首先要获得ReentantLock,当条件不满足线程1等待时,ReentrantLock会被释放,以能让其他线程争用,其他线程获得reentrantLock,然后满足条件,唤醒线程1继续执行。
这与wait()方法是一样的,调用wait()的代码块要被包含在synchronized块中,而当线程r1调用了objectA.wait()方法后,同步对象的锁会释放,以能让其他线程争用;其他线程获取同步对象锁,完成任务,调用objectA.notify(),让r1继续执行。代码示例如下。
代码示例1(调用condition.await();会释放lock锁):
public class ConditionTest {
private static final ReentrantLock lock = new ReentrantLock(true);
//从锁中创建一个绑定条件
private static final Condition condition = lock.newCondition();
private static int count = 1;
public static void main(String[] args) {
Runnable r1 = new Runnable(){
public void run(){
lock.lock();
try{
while(count<=5){
System.out.println(Thread.currentThread().getName()+"--"+count++);
Thread.sleep(1000);
}
condition.signal(); //线程r1释放条件信号,以唤醒r2中处于await的代码继续执行。
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
};
Runnable r2 = new Runnable(){
public void run(){
lock.lock();
try{
if(count<=5){
System.out.println("----$$$---");
condition.await(); //但调用await()后,lock锁会被释放,让线程r1能获取到,与Object.wait()方法一样
System.out.println("----------");
}
while(count<=10){
System.out.println(Thread.currentThread().getName()+"--"+count++);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
};
new Thread(r2).start(); //让r2先执行,先获得lock锁,但条件不满足,让r2等待await。
try {
Thread.sleep(100); //这里休眠主要是用于测试r2.await()会释放lock锁,被r1获取
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r1).start();
}
}
代码示例2(此例子来自于Condition的API):
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionMain {
public static void main(String[] args) {
// TODO Auto-generated method stub
final BoundleBuffer buf = new ConditionMain().new BoundleBuffer();
new Thread(new Runnable() {
public void run() {
for (int i=0;i<1000;i++) {
try {
buf.put(i);
System.out.println("入值:" + i);
Thread.sleep(200);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
for (int i=0;i<1000;i++) {
try {
int x = buf.take();
System.out.println("出值:" + x);
Thread.sleep(2000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public class BoundleBuffer{
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Integer[] items = new Integer[10];
int putptr,takeptr,count;
public void put(int x) throws InterruptedException{
System.out.println("put wait lock");
lock.lock();
System.out.println("put get lock");
try {
while(count == items.length) {
System.out.println("buffer full,please wait");
notFull.await();
}
items[putptr] = x;
if(++putptr == items.length)
putptr = 0;
++count;
notEmpty.signal();
}finally {
lock.unlock();
}
}
public int take() throws InterruptedException {
System .out.println("take wait lock");
lock.lock();
System .out.println("take get lock");
try {
while (count == 0){
System.out.println("no elements, please wait");
notEmpty.await();
}
int x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
}
4.BlockingQueue
简单介绍。这是一个阻塞的队列超类接口,concurrent包下很多架构都基于这个队列。BlockingQueue是一个接口,此接口的实现类有:ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue 。每个类的具体使用可以参考API。
这些实现类都遵从共同的接口定义(一目了然,具体参考api):
抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() 不可用 不可用
5.CompletionService
1.CompletionService是一个接口,用来保存一组异步求解的任务结果集。api的解释是:将新生产的异步任务与已完成的任务结果集分离开来。
2.CompletionService依赖于一个特定的Executor来执行任务。实际就是此接口需要多线程处理一个共同的任务,这些多线程由一个指定的线程池来管理。CompletionService的实现类ExecutorCompletionService。
3.api的官方代码示例参考ExecutorCompletionService类的api(一个通用分制概念的函数)。
4.使用示例:如有时我们需要一次插入大批量数据,那么可能我们需要将1w条数据分开插,异步执行。如果某个异步任务失败那么我们还要重插,那可以用CompletionService来实现。下面是简单代码:
(代码中1w条数据分成10份,每次插1000条,如果成功则返回true,如果失败则返回false。那么忽略数据库的东西,我们假设插1w条数据需10s,插1k条数据需1s,那么下面的代码分制后,插入10条数据需要2s。为什么是2s呢?因为我们开的线程池是8线程,10个异步任务就有两个需要等待池资源,所以是2s,如果将下面的8改为10,则只需要1s。)
public class CompletionServiceTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(8); //需要2s,如果将8改成10,则只需要1s
CompletionService<Boolean> cs = new ExecutorCompletionService<Boolean>(pool);
Callable<Boolean> task = new Callable<Boolean>(){
public Boolean call(){
try {
Thread.sleep(1000);
System.out.println("插入1000条数据完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
};
};
System.out.println(getNow()+"--开始插入数据");
for(int i=0;i<10;i++){
cs.submit(task);
}
for(int i=0;i<10;i++){
try {
//ExecutorCompletionService.take()方法是阻塞的,如果当前没有完成的任务则阻塞
System.out.println(cs.take().get());
//实际使用时,take()方法获取的结果可用于处理,如果插入失败,则可以进行重试或记录等操作
} catch (InterruptedException|ExecutionException e) {
e.printStackTrace();
}
}
System.out.println(getNow()+"--插入数据完成");
pool.shutdown();
}
public static String getNow(){
SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
return sdf.format(new Date());
}
}
5.CompletionService 与Callable+Future的对比:
在上面的Callable中说过,Callable+Future能实现任务的分治,但是有个问题就是:不知道call()什么时候完成,需要人为控制等待。
而jdk通过CompetionService已经将此麻烦简化,通过CompletionService将异步任务完成的与未完成的区分开来(正如api的描述),我们只用去取即可。
CompletionService有什么好处呢?
如上所说:1)将已完成的任务和未完成的任务分开了,无需开发者操心;2)隐藏了Future类,简化了代码的使用。
6.CountDownLatch
1.CountDownLatch:api解释:一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。个人理解是CountDownLatch让可以让一组线程同时执行,然后在这组线程全部执行完前,可以让另一个线程等待。
就好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。那么CountDownLatch就可以控制10个选手同时出发,和公布成绩的时间。
CountDownLatch 是一个通用同步工具,它有很多用途。将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器,或入口:在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。用 N 初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
代码示例可参考api的示例。(重要)
2.代码示例:
参考链接中的示例:http://blog.csdn.net/xsl1990/article/details/18564097
个人示例:
public class CountDownLatchTest {
private static SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
public static void main(String[] args) {
final CountDownLatch start = new CountDownLatch(1); //用一个信号控制一组线程的开始,初始化为1
final CountDownLatch end = new CountDownLatch(10); //要等待N个线程的结束,初始化为N,这里是10
Runnable r = new Runnable(){
public void run(){
try {
start.await(); //阻塞,这样start.countDown()到0,所有阻塞在start.await()处的线程一起执行
Thread.sleep((long) (Math.random()*10000));
System.out.println(getNow()+"--"+Thread.currentThread().getName()+"--执行完成");
end.countDown();//非阻塞,每个线程执行完,让end--,这样10个线程执行完end倒数到0,主线程的end.await()就可以继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i=0;i<10;i++){
new Thread(r).start(); //虽然开始了10个线程,但所有线程都阻塞在start.await()处
}
System.out.println(getNow()+"--线程全部启动完毕,休眠3s再让10个线程一起执行");
try {
Thread.sleep(3*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getNow()+"--开始");
start.countDown(); //start初始值为1,countDown()变成0,触发10个线程一起执行
try {
end.await(); //阻塞,等10个线程都执行完了才继续往下。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getNow()+"--10个线程都执行完了,主线程继续往下执行!");
}
private static String getNow(){
return sdf.format(new Date());
}
}
7.CyclicBarrier
1.一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。也就是说,这一组线程的执行分几个节点,每个节点往下执行,都需等待其他线程,这就需要这种等待具有循环性。CyclicBarrier在这样的情况下就很有用。
2.CyclicBarrier与CountDownLacth的区别:
1)CountDownLacth用于一个线程与一组线程之间的相互等待。常用的就是一个主线程与一组分治线程之间的等待:主线程发号令,一组线程同时执行;一组线程依次执行完,再唤醒主线程继续执行;
CyclicBarrier用于一组线程执行时,每个线程执行有多个节点,每个节点的处理需要相互等待。如:对5个文件进行处理,按行将各个文件数字挑出来合并成一行,排序,并输出到另一个文件,那每次处理都需要等待5个线程读入下一行。(api示例可供参考)
2)CountDownLacth的处理机制是:初始化一个值N(相当于一组线程有N个),每个线程调用一次countDown(),那么cdLatch减1,等所有线程都调用过countDown(),那么cdLatch值达到0,那么线程从await()处接着玩下执行。
CyclicBarrier的处理机制是:初始化一个值N(相当于一组线程有N个),每个线程调用一次await(),那么barrier加1,等所有线程都调用过await(),那么barrier值达到初始值N,所有线程接着往下执行,并将barrier值重置为0,再次循环下一个屏障;
3)由2)可以知道,CountDownLatch只可以使用一次,而CyclicBarrier是可以循环使用的。
3.个人用于理解的示例:
public class CyclicBarrierTest {
private static final CyclicBarrier barrier = new CyclicBarrier(5,
new Runnable(){
public void run(){ //每次线程到达屏障点,此方法都会执行
System.out.println("\n--------barrier action--------\n");
}
});
public static void main(String[] args) {
for(int i=0;i<5;i++){
new Thread(new CyclicBarrierTest().new Worker()).start();
}
}
class Worker implements Runnable{
public void run(){
try {
System.out.println(Thread.currentThread().getName()+"--第一阶段");
Thread.sleep(getRl());
barrier.await(); //每一次await()都会阻塞,等5个线程都执行到这一步(相当于barrier++操作,加到初始化值5),才继续往下执行
System.out.println(Thread.currentThread().getName()+"--第二阶段");
Thread.sleep(getRl());
barrier.await(); //每一次5个线程都到达共同的屏障节点,会执行barrier初始化参数中定义的Runnable.run()
System.out.println(Thread.currentThread().getName()+"--第三阶段");
Thread.sleep(getRl());
barrier.await();
System.out.println(Thread.currentThread().getName()+"--第四阶段");
Thread.sleep(getRl());
barrier.await();
System.out.println(Thread.currentThread().getName()+"--第五阶段");
Thread.sleep(getRl());
barrier.await();
System.out.println(Thread.currentThread().getName()+"--结束");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static long getRl(){
return Math.round(10000);
}
}
4.参考api的示例。
api的示例自己看,就是加深印象。
但是api中有一点描述:如果屏障操作在执行时不依赖于正挂起的线程,则线程组中的任何线程在获得释放时都能执行该操作。为方便此操作,每次调用 await() 都将返回能到达屏障处的线程的索引。然后,您可以选择哪个线程应该执行屏障操作,例如:
if (barrier.await() == 0) {
<span style="white-space:pre"> </span> // log the completion of this iteration
}
就是说,barrier.await()还会返回一个int值。这个返回值到底是什么呢?不是返回的线程的索引,返回的是:N-进入等待线程数,如5个线程,5线程都进入等待,那返回值就是0(具体可以参看源码)。那么barrier.await()==0也可以看做是一个N线程都达到公共屏障的信号,然后在此条件下处理原本需要放在Runnable参数中的逻辑。不用担心多线程会多次执行此逻辑,N个线程只有一个线程barrier.await()==0。
8.Exchanger
1.Exchanger可以在对中对元素进行配对和交换的线程的同步点。api上不是太好理解,个人理解说白了就是两个线程交换各自使用的指定内存数据。
2.场景:
api中有示例,两个线程A、B,各自有一个数据类型相同的变量a、b,A线程往a中填数据(生产),B线程从b中取数据(消费)。具体如何让a、b在内存发生关联,就由Exchanger完成。
api中说:Exchanger 可能被视为 SynchronousQueue 的双向形式。怎么理解呢?传统的SynchronousQueue存取需要同步,就是A放入需要等待B取出,B取出需要等待A放入,在时间上要同步进行。而Exchanger在B取出的时候,A是同步在放入的。即:1)A放入a,a满,然后与B交换内存,那A就可以操作b(b空),而B可以操作a;2)等b被A存满,a被B用完,再交换;3)那A又填充a,B又消费b,依次循环。两个内存在一定程度上是同时被操作的,在时间上不需要同步。
再理解就是:如果生产需要5s,消费需要5s。SynchronousQueue一次存取需要10s,而Exchanger只需要5s。4.注意事项:
目前只知道Exchanger只能发生在两个线程之间。但实际上Exchanger的源码是有多个插槽(Slot),交换是通过线程ID的hash值来定位的。目前还没搞懂?待后续。
如果一组线程aGroup操作a内存,一组线程bGroup操作b内存,如何交换?能不能交换?
3.代码示例:
public class ExchangerTest {
private SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
private static Exchanger<Queue<Integer>> changer = new Exchanger<Queue<Integer>>();
public static void main(String[] args) {
new Thread(new ExchangerTest().new ProducerLoop()).start();
new Thread(new ExchangerTest().new ConsumerLoop()).start();
}
class ProducerLoop implements Runnable{
private Queue<Integer> pBuffer = new LinkedBlockingQueue<Integer>();
private final int maxnum = 10;
@Override
public void run() {
try{
for(;;){
Thread.sleep(500);
pBuffer.offer((int) Math.round(Math.random()*100));
if(pBuffer.size() == maxnum){
System.out.println(getNow()+"--producer交换前");
pBuffer = changer.exchange(pBuffer);
System.out.println(getNow()+"--producer交换后");
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
class ConsumerLoop implements Runnable{
private Queue<Integer> cBuffer = new LinkedBlockingQueue<Integer>();
@Override
public void run() {
try{
for(;;){
if(cBuffer.size() == 0){
System.out.println("\n"+getNow()+"--consumer交换前");
cBuffer = changer.exchange(cBuffer);
System.out.println(getNow()+"--consumer交换后");
}
System.out.print(cBuffer.poll()+" ");
Thread.sleep(500);
}
}catch(Exception e){
e.printStackTrace();
}
}
}
private String getNow(){
return sdf.format(new Date());
}
}
4.注意事项:
目前只知道Exchanger只能发生在两个线程之间。但实际上Exchanger的源码是有多个插槽(Slot),交换是通过线程ID的hash值来定位的。目前还没搞懂?待后续。
如果一组线程aGroup操作a内存,一组线程bGroup操作b内存,如何交换?能不能交换?