集合
java中的集合一般分为List、Map、Set、Queue。
List
列表集合
- ArrayList:最常用的List,底层为Object数组,继承了
RandomAccess, Cloneable, java.io.Serializable
等接口。
继承RandomAccess表示该类支持快速随机访问,尽量用for(int i = 0; i < size; i++) 来遍历。反之使用Iterator迭代器。
继承Cloneable表示该类支持浅拷贝。
继承java.io.Serializable表示该类支持序列化
- LinkedList:底层实现为双向链表。不支持随机访问。实现Queue接口,支持队列操作。
- Vector:线程安全的ArrayList,所有方法全部使用synchronized标记。虽然方法都是线程安全的,但是如果使用Iterator迭代器进行遍历的话,还是线程不安全的。但是使用foreach()方法进行遍历是线程安全的。
- CopyOnWriteArrayList:底层实现为Object数组,方法是线程不安全的,但是遍历是线程安全的。通过拷贝List实现遍历安全。
Map
键值对集合
- HashMap:最常用的Map,线程不安全,允许key和value为null,内部使用Node数组保存k-v数据。默认大小为16,调节因子为0.75(表示当元素达到数组大小的0.75时,数据进行扩容,扩大两倍)。通过计算key的hash值来确定key-value的位置。如果出现位置冲突,jdk1.8前是形成链表,jdk1.8及以后引入链表+红黑树,默认链表元素大于8时红黑树化,树元素小于6时链表化。
- HashTable:线程不安全,key和value不允许null,内部使用Entry数组保存数据。不常用,为什么不用HashMap。
- SynchronizedMap:线程安全,由synchronizedMap创建,实现线程安全的方式是通过synchronized调用相关方法。并发性能差。
- ConcurrentHashMap:线程安全的HashMap,实现方式为CAS操作(jdk1.8之前为分段锁)。并发性能好。
Set
不重复的集合
- HashSet:最常用的Set,一般用来过滤数据、保存配置,底层使用HashMap的key保存数据(实现数据不重复)。
- TreeSet:可以按某个顺序保存数据,通过构造方法传入Comparator实现。
Queue
队列
- LinkedList:上面有
- BlockingQueue:阻塞队列,多线程中实现线程的通信,经常使用。常用LinkedBlockingQueue,通过分离读写锁实现并发。还有ArrayBlockingQueue,单锁实现并发。
多线程
synchronizd
java中,每个对象都自带监视器。synchronizd有三种使用方式:
-
synchronizd(object){...}
,对任意代码块加锁。线程会尝试获取object的监视器,成功获取的线程进入临界区。其他线程阻塞。直到临界区代码执行完毕,释放object的监视器,其他线程再次进行竞争。保证了临界区一次只有一个线程进入。 - synchronizd修饰普通方法,线程会尝试获取该方法对象的监视器。如果两个不同对象调用同一个synchronizd修饰的方法,无法保证线程执行的唯一性,synchronizd不会发挥作用。
- synchronizd修饰静态方法,线程会尝试获取该方法类的监视器。Class对象的唯一性会保证synchronizd发挥作用。
如果1中的object为Class对象,那么效果与3相同。
synchronizd为可重入锁。可以在获取对象的监视器后,再次调用同对象的synchronizd。
volatile
volatile可以保证所有线程看到的变量值都是最新的。任何线程修改了volatile变量,会立刻反映到其他线程中。
如果变量不用volatile修饰,那么线程对变量的修改不会立刻反映到其他线程中,因为有本地缓存。如果不用volatile,可以用synchronizd(Lock)实现变量的刷新。离开synchronizd代码块时,所有的修改会强制刷新入主内存。
volatile保证可见性,不保证原子性。如果有对volatile变量的原子性操作,需要加锁。
volatile可以防止 JVM 进行指令重排优化。
禁止重排序的规则为:第一个操作为volatile读;第二个操作为volatile写;第一个操作为volatile写,第二个操作为volatile读。
禁止重排序可以防止因为重排序在多线程环境下引起的顺序一致性错误。
private static Map<String,String> value ;
private static volatile boolean flag = fasle ;
//以下方法发生在线程 A 中 初始化 Map
public void initMap(){
//耗时操作
value = getMapValue() ;//1
flag = true ;//2
}
//发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
sleep() ;
}
//dosomething
doSomeThing(value);
}
如果flag不用volatile修饰,那么线程A有可能会先执行2再执行1,这样会造成错误。同时volatile修饰的变量可以保证线程A改变flag的值之后,线程B立刻可见。否则容易因为缓存导致死循环。
Lock
锁接口,主要实现类为ReentrantLock、ReadLock和WriteLock。
- ReentrantLock:可重入锁,最普通的Lock实现类。
- ReentrantReadWriteLock:读写分离锁,继承自ReadWriteLock,内部实现了ReadLock和WriteLock,读锁和写锁。通过分离读写提高并发性。允许同时多个读线程,只允许同时单个写线程(此时不允许读)。适用于读多写少。
- void lock():加锁操作
- void lockInterruptibly() throws InterruptedException:可以抛出中断异常的加锁
- boolean tryLock():尝试获取锁,返回结果
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException:尝试在指定的时间内获取锁,可以被中断。
- void unlock():解锁操作,这个操作是必须的,一般放在finally中执行。
如果想用Lock进行同步操作,注意多个线程中操作同一个Lock。
ThreadPoolExecutor
ThreadPoolExecutor线程池,可以自定义线程池的所有参数。
推荐通过new ThreadPoolExecutor()创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:线程池核心线程,线程池启动和平时运行的线程数。
- maximumPoolSize:线程池最大线程。当workQueue中放满之后,会启动额外的线程,最大为maximumPoolSize。
- keepAliveTime:当线程池中线程的数量超过corePoolSize时,闲置线程会在keepAliveTime后被回收,直到线程数变为corePoolSize。
- unit:keepAliveTime的时间单位
- workQueue:任务阻塞队列,当任务无法被线程池中的线程接手处理时,会暂时存放在workQueue阻塞队列中。直到线程池处理完当前任务,然后从workQueue中获取任务进行处理。
- threadFactory:线程工厂,在新线程创建时对其进行初始化。
- handler:拒绝策略。当workQueue已满,线程数达到maximumPoolSize,线程池会执行拒绝策略。
线程池规则
线程池的线程执行规则跟任务队列有很大的关系。
下面都假设任务队列没有大小限制:
- 如果线程数量 <= 核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。
- 如果线程数量 > 核心线程数,但 <= 最大线程数,并且任务队列是LinkedBlockingDeque的时候,超过核心线程数量的任务会放在任务队列中排队。
- 如果线程数量 > 核心线程数,但 <= 最大线程数,并且任务队列是SynchronousQueue的时候,线程池会创建新线程执行任务,这些任务也不会被放在任务队列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。
- 如果线程数量 > 核心线程数,并且 > 最大线程数,当任务队列是LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。也就是当任务队列是LinkedBlockingDeque并且没有大小限制时,线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。
- 如果线程数量 > 核心线程数,并且 > 最大线程数,当任务队列是SynchronousQueue的时候,线程池会执行拒绝策略。
任务队列大小有限时:
- 当LinkedBlockingDeque塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时,线程池会执行拒绝策略。
- SynchronousQueue没有数量限制。因为他根本不保持这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时。线程池会执行拒绝策略。
多线程通信
- wait和notify:最基础的线程通信,wait使线程等待,notify唤醒等待线程。通过对象实现,类似
synchronizd(object)
。 - CountDownLatch:倒计数器,await检查计数器是否为0,countDown使计数减1。
- CyclicBarrier:实现线程间的相互等待,当所有线程调用await后,所有线程开始向下执行。
- Callable:有返回值的Runnable,可以使用FutureTask接收返回值,会阻塞。如果使用线程池,则不会阻塞。
- BlockingQueue:阻塞队列。生产者-消费者模型中经常使用。