ArrayList
为啥是线程不安全的?
ArrayList的 add()
方法写操作的时候为了保证并发性和效率,没有加synchronize
关键字。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ArrayList 在多线程环境下会出现的异常:
java.util.ConcurrentModificationException
解决办法
//方法一:vector(线程安全)
//add() 方法有synchronize 关键字修饰
List<String> list = new Vector<>();
//方法二:Collections(工具类)
List<String> list = Collections.synchronizedList(new ArrayList<>());
CopyOnWriteArrayList<>()
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();
}
}
//最终方法:CopyOnWriteArrayList<>()
/*
* 写时复制
* CopyOnWrite容器即写时复制的容器。往一个容器中添加元素的时候,如直接往当前容器Object[]添加,
* 而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newElements,然后往新的容器中添加元素,
* 添加完毕之后,再将原容器的引用指向新容器setArray(newElements);
* 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
* 所以CopyOnWrite容器也是一种读写分离的思想。读和写在不同的容器中。
*/
List<String> list = new CopyOnWriteArrayList<>();
public class ArrayListDemo {
public static void main(String[] args) {
//List<String> list = new ArrayList<>();
//List<String> list = new Vector<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
Set
解决线程安全问题同上,
CopyOnWriteArraySet
HashSet 底层是 HashMap
public HashSet() {
map = new HashMap<>();
}
至于键值对,HashSet 只关注键值key,value是一个固定的常量PRESENT
private static final Object PRESENT = new Object();
……
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
Map
HashMap 解决线程不安全,使用 ConcurrentHashMap
公平锁 非公平锁
公平锁 是指,多个线程按照申请锁的顺序来获取锁。(先到先得)
非公平锁 是指,多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。(先抢先得)
JUC(并发包)中 ReentrantLock
的创建可以指定构造函数的 boolean 类型来得到公平锁或非公平锁,默认是非公平锁(false)
ReentrantLock 和 synchronized 默认都是非公平锁,非公平锁的优点在于吞吐量比公平锁大。
可重入锁
可重入锁又称递归锁
指的是线程可以进入任何一个它已经拥有的锁 所同步着的代码块。
可重入锁的最大作用就是避免死锁。
ReentrantLock
和 Synchronized
是典型的可重入锁。
自旋锁
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是群换会消耗CPU。
读写锁
独占锁(写锁):是指该锁一次只能被一个线程所持有。ReentrantLock 和 Synchronized 都是独占锁。
共享锁(读锁):是指该锁可以被多个线程所持有。
ReadWriteLock(接口)—> ReentrantReadWriteLock
ReentrantReadWriteLock
其读锁是共享锁,写锁是独占锁。
读锁的共享锁可以保证并发读是非常高效的,读写 写读 写写 的过程是互斥的。
readLock()
writeLock()
//既保证了一致性,又保证了并发性
Synchronized 和 Lock的区别
- synchronized 是 JVM层面的,是Java关键字,底层是
monitorenter
和monitorexit
,不可中断,不需要手动释放锁。 - lock 属于API层面,是jdk1.5 之后的具体类,可中断,需要手动释放锁。
线程池
线程池做的工作主要是,处理过程中将任务放入队列,然后在线程创建后启动这些任务。如果线程数量超过了最大线程数量,超出的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用、控制最大并发数、管理线程。
Java中的线程池是通过 Executor
框架实现的。(Executor、Executors(辅助工具类)、ExecutorService、ThreadPoolExecutor)
获得使用线程的第四种方法:通过线程池:
//创建一个定长的线程池,可控制线程最大并发数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
//LinkedBlockingQueue()阻塞队列
//创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
//LinkedBlockingQueue()阻塞队列
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, 2147483647, 60L,
TimeUnit.SECONDS,
new SynchronousQueue());
}
//SynchronousQueue()同步队列
以上底层源码都是 ThreadPoolExecutor
线程池的参数
//底层七个参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {……}
corePoolSize
:线程池的核心线程数.maximumPoolSize
:线程池能够容纳同时执行的最大线程数,>=1.keepAliveTime
:多余的空闲线程的存活时间。(当前线程池数量超过corePoolSize时,并且空闲时间达到keepAliveTime值,多余空闲的线程会被销毁直到只剩下corePoolSize个线程为止)TimeUnit
:keepAliveTime 的单位.workQueue
:任务队列,被提交,但尚未被执行的任务.threadFactory
:表示生成线程池中工作线程的线程工厂,用于创建线程,一般使用默认的.handler
:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数时如何来拒绝请求执行的runnable的策略。
重点
-
在创建了线程池后,等待提交过来的任务请求。
-
当调用
execute()
方法添加一个请求任务时,线程池会做如下判断:2.1 如果正在运行的线程数量小于
corePoolSize
,那么马上创建线程运行此任务;
2.2 如果正在运行的线程数量大于或等于corePoolSize
,那么将这个任务放入队列;
2.3 如果此时队列满了,但正在运行的线程数量小于maximumPoolSize
,那么要创建非核心线程去运行任务;
2.4 如果队列满了,且正在运行的线程数量大于或等于maximumPoolSize
,那么线程会启动饱和拒绝策略。 -
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程空闲超过一定时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程数大于 corePoolSize ,那么这个线程就会被停掉;
所以线程池的所有任务完成后它最终会收缩到 corePoolSize 的大小。
GC
垃圾回收算法
引用计数
复制
标记-清除
标记-整理