java 并发编程
1.线程
1.1 创建线程的方式
- 实现Runnable接口
- 继承Thread类
- 线程池创建线程
- Callable创建线程
public class CreateThreadTest {
private static ExecutorService pool = Executors.newFixedThreadPool(10);
class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread extends Thread is running");
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable implements Runnable is running");
}
}
public static void main(String[] args) {
CreateThreadTest a = new CreateThreadTest();
MyThread thread1 = a.new MyThread();
Thread thread2 = new Thread(a.new MyRunnable());
thread1.start();
thread2.start();
pool.execute(() ->{
System.out.println("pool created thread is runing");
});
pool.submit(() ->{
System.out.println("callable thread is running");
return "callable";
});
}
总结:
- 继承Thread 类只是重写了run方法,线程执行则会执行重写的run方法
- 实现runnable接口,创建线程需要作为target传给 Thread的构造器,run方法执行时,如果target不为空,则执行target的run方法,即新实现的接口run方法
- 以上俩种,实际上都需要通过Thread的构造器来创建线程,只是一个时重写run方法,一个是动态的替换了执行类target;
- 线程池创建线程也是基于以上俩字形式;
- 使用实现接口的方式,更加灵活,职责更加鲜明
1.2 线程的几种状态
- NEW
- runnable
- waiting
- Time waiting
- blocked
- terminated
wait/notify 和 sleep 方法的异同
-
相同点
- 实现线程阻塞
- 都可以响应线程interrupt
-
不同点
- wait方法必须在synchronized保护的代码中使用,sleep则不需要
- 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁
- wait可以不定义超时时间
- wait/notify属于Object类,sleep时Thread的方法
1.3 如何优雅的停止线程
- 最正确的停止线程的方式为interrupt
- catch 中 throw InterruptedException
- Thread.currentThread().interrupt();
- 不进行try catch ,method throws InterruptedException
//不优雅的停止线程
public static void main(String[] args) {
Thread thread1 = new Thread(() ->{
try {
Thread.sleep(10000L);
//当该线程的上游想知道thread1是否正常执行结束时,应该抛出InterruptedException
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("不优雅的停止线程");
//1.throw e
//2.Thread.currentThread().interrupt();
//3. method throws InterruptedException
}
});
thread1.start();
}
-
使用volatile标记位来实现线程的停止会有什么问题?
以下代码不会正常的输入canceled;
当线程的执行存在某种阻塞的操作时,即使volatile标记会成功标记取消,但线程阻塞时不会重新进行判断,标记位不会生效的;
如何处理下面的程序?
package com.lww.thread; public class StopThreadTest { class MyProducerRunable implements Runnable{ private volatile boolean canceled = false; public void setCanceled(boolean canceled) { this.canceled = canceled; } @Override public void run() { try { while (!canceled){ //模拟一个阻塞的操作 Thread.sleep(Integer.MAX_VALUE); } } catch (Exception e) { e.printStackTrace(); Thread.currentThread().interrupt(); }finally { System.out.println("Thread is canceled"); } } } public static void main(String[] args) throws InterruptedException { StopThreadTest a = new StopThreadTest(); MyProducerRunable runable = a.new MyProducerRunable(); Thread producer_thread = new Thread(runable); producer_thread.start(); Thread.sleep(1000L); runable.setCanceled(true); } }
interrupt 能正确的终止线程,producer_thread.interrupt(); 后 finally能正确的执行canceled
public static void main(String[] args) throws InterruptedException { StopThreadTest a = new StopThreadTest(); MyProducerRunable runable = a.new MyProducerRunable(); Thread producer_thread = new Thread(runable); producer_thread.start(); Thread.sleep(1000L); //runable.setCanceled(true); producer_thread.interrupt(); }
1.4 线程安全
什么是线程安全?
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的。
访问共享变量、依赖时序操作、使用不是线程安全类时,存在的线程问题:
-
运行结果错误
i++操作等等;
-
发布和初始化导致线程安全问题
子线程中初始化值,主线程在未初始化结束使用值等等
-
活跃性问题
- 死锁
- 活锁
- 线程一直在处理无法处理的同一条数据
- 饥饿
- 线程的优先级不同,导致低优先级的线程可能一直获取不到CPU时间分片
2.线程池
2.1 线程池的作用
- 减少线程生命周期的开销
- 控制内存和CPU的使用
- 统一管理
2.2 创建线程池的参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
默认的线程工厂如下:
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
2.3 线程池的拒绝策略
-
AbortPolicy
会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException
-
DiscardPolicy
当新任务被提交后直接被丢弃掉,也不会给你任何的通知
-
DiscardOldestPolicy
会丢弃任务队列中的头结点,通常是存活时间最长的任务
-
CallerRunsPolicy
如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务
2.4 常见的线程池
- FixedThreadPool
- 无界队列LinkedBlockingQueue
- CachedThreadPool
- 线程数无界SynchronousQueue
- ScheduledThreadPool
- 延迟队列DelayedWorkQueue,基于时间执行任务
- SingleThreadExecutor
- 无界队列LinkedBlockingQueue ,线程数1
- SingleThreadScheduledExecutor
- 延迟队列DelayedWorkQueue,基于时间执行任务
- ForkJoinPool(java7)
2.5 线程池问题
- FixedThreadPool和SingleThreadExecutor 由于无界队列,容易OOM
- CachedThreadPool 不控制线程数,导致内存不足
- ScheduledThreadPool和SingleThreadScheduledExecutor ,延时队列也是无界的,容易OOM
2.6 选择合适的线程数
*线程数 = CPU 核心数 (1+平均等待时间/平均工作时间)
-
CPU密集型任务
加密、解密、压缩、计算等占用CPU的任务
-
耗时IO型任务
数据库、文件的读写,网络通信等任务
2.7 关闭线程池
- shutdown -> 执行完正在执行的任务和队列中等待的任务后才彻底关闭
- shutdownNow -> 给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回
- isShutdown ->是否执行了 shutdown 或者 shutdownNow 方法
- isTerminating -> 执行了shutdown 或者 shutdownNow 方法,但未完全terminated 返回true
- isTerminated -> 检测线程池已关闭及其线程池中的所有任务都已经都执行完毕
- awaitTermination -> 判断线程池状态
/**
* Initiates an orderly shutdown in which previously submitted
* tasks are executed, but no new tasks will be accepted.
* Invocation has no additional effect if already shut down.
*
* <p>This method does not wait for previously submitted tasks to
* complete execution. Use {@link #awaitTermination awaitTermination}
* to do that.
*
* @throws SecurityException {@inheritDoc}
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
/**
* Attempts to stop all actively executing tasks, halts the
* processing of waiting tasks, and returns a list of the tasks
* that were awaiting execution. These tasks are drained (removed)
* from the task queue upon return from this method.
*
* <p>This method does not wait for actively executing tasks to
* terminate. Use {@link #awaitTermination awaitTermination} to
* do that.
*
* <p>There are no guarantees beyond best-effort attempts to stop
* processing actively executing tasks. This implementation
* cancels tasks via {@link Thread#interrupt}, so any task that
* fails to respond to interrupts may never terminate.
*
* @throws SecurityException {@inheritDoc}
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
public boolean isShutdown() {
return ! isRunning(ctl.get());
}
/**
* Returns true if this executor is in the process of terminating
* after {@link #shutdown} or {@link #shutdownNow} but has not
* completely terminated. This method may be useful for
* debugging. A return of {@code true} reported a sufficient
* period after shutdown may indicate that submitted tasks have
* ignored or suppressed interruption, causing this executor not
* to properly terminate.
*
* @return {@code true} if terminating but not yet terminated
*/
public boolean isTerminating() {
int c = ctl.get();
return ! isRunning(c) && runStateLessThan(c, TERMINATED);
}
public boolean isTerminated() {
return runStateAtLeast(ctl.get(), TERMINATED);
}
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (;;) {
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
if (nanos <= 0)
return false;
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}
通过线程池执行方法查看线程池运行原理:
- 运行线程数小于corePoolSize,则直接新建线程执行任务
- 线程池在运行状态时,任务队列未满(添加任务成功)下,再次检查是否需要创建线程(距离上次检测可能存在线程结束),如果可工作线程为0则新建线程
- 线程池停止或者新建线程失败,则拒绝策略
/**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
*
* @param command the task to execute
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
3.锁
3.1 锁的分类
-
偏向锁、轻量级锁、重量级锁
这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。
无锁 --> 偏向锁 —> 轻量级锁 -----> 重量级锁
- 偏向锁:如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。
- 轻量级锁:轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。
- 重量级锁:重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。
-
悲观锁、乐观锁
- 悲观锁 :是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁。
- 乐观锁 :并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。
-
自旋锁、非自旋锁
- 自旋锁:如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。
- 非自旋锁:没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞。
-
公平锁、非公平锁
- 公平锁:FIFO的思想
- 非公平锁:插队思想
-
可重入锁、不可重入锁
- 可重入锁:ReentrantLock,线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。
- 不可重入锁:线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。
-
共享锁、独占锁(排他锁)或者称作读锁和写锁
- 共享锁:共享锁指的是我们同一把锁可以被多个线程同时获得
- 独占锁:这把锁只能同时被一个线程获得
-
可中断锁、不可中断锁
3.2 synchronized 锁实现
- synchronized 在方法体上,字节码中是通过ACC_SYSCHRONINZED来标记的,代表使用该标记的方法在执行前需要获取执行对象this的monitor锁,执行结束后会自动释放monitor锁
- synchronized在代码块中,字节码中时通过monitorenter和monitorexit的出现来加锁和释放锁的
3.3 synchrinized和lock的异同
- 同:保证可见性、资源线程安全、可重入锁
- 异
- 用法不同、synchrinized加解锁是隐性的,不需要手动释放,lock需要手动unlock
- lock可以不用加锁顺序反序解锁,synchrinized不行;
- lock比较灵活,例如lockInterruptibly、tryLock、可实现公平锁和
- synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制
- JDK6 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差。
3.4 ReadWriteLock
- 规则:读读共享、读写互斥、写读互斥、写写互斥
- 适用情况:读多写少的共享资源,可提高并发效率
ReentrantReadWriteLock
- 公平锁 :R锁和W锁都不能插队
- 非公平锁:W锁可以插队,减少W锁线程饥饿发生,W锁插队成功的条件为:在当前没有任何其他线程持有读锁和写锁的时候,才能插队成功。
- 锁的升降级:只能从写锁降级为读锁,不能从读锁升级为写锁。什么时候需要降级?当写操作结束后,需要释放W锁,但是又需要读取共享数据进行后续处理时,我们可以将W锁降级R锁后释放W锁,然后读取共享数据,最后释放R锁。
3.5 自旋锁
自旋锁的特点:它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。
好处:自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。
缺点:临界区时间变长,则获取到锁的时间就变长,效率可能还不如线程切换的开销。
适用场景:自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率。如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源。
4.并发容器
jvm常见的指令,可用于分析可见性和代码的执行
- lock(锁定):在某一个线程在读取主内存的时候需要把变量锁定。
- unlock(解锁):某一个线程读取玩变量值之后会释放锁定,别的线程就可以进入操作
- read(读取):从主内存中读取变量的值并放入工作内存中
- load(加载):从read操作得到的值放入工作内存变量副本中
- use(使用):把工作内存中的一个变量值传递给执行引擎
- assign(赋值):它把一个从执行引擎接收到的值赋值给工作内存的变量
- store(存储):把工作内存中的一个变量的值传送到主内存中
- write(写入):把store操作从工作内存中一个变量的值传送到主内存的变量中。
这里我再引入一张别的地方被我搜来的图供大家一起理解:
4.1.线程不安全的HashMap
- HashMap的底层结构
- 如何添加元素
- 如何删除元素
- 扩容
- 为什么扩容时是2的倍数,有什么好处?
HashMap的几个参数
属性 | 说明 | JDK8 | JDK7 |
---|---|---|---|
DEFAULT_INITIAL_CAPACITY | 默认初始容量 1 << 4 = 16 | 是 | 是 |
MAXIMUM_CAPACITY | 最大容量 1 << 30 | 是 | 是 |
DEFAULT_LOAD_FACTOR | 默认加载因子 0.75f | 是 | 是 |
TREEIFY_THRESHOLD | 链表转换红黑树的临界值 8 | 是 | 否 |
UNTREEIFY_THRESHOLD | 红黑树转换链表的临界值 6 | 是 | 否 |
MIN_TREEIFY_CAPACITY | 链表转换红黑树的容量临界值 64 | 是 | 否 |
modCount | 数据改变次数的统计 | 是 | 是 |
threshold | 扩容临界值 | 是 | 是 |
loadFactor | 加载因子,默认为0.75,和扩容有关 | 是 | 是 |
HashMap PUT实现
- 判断数hash组是否为空,为空则resize() 数组
- 通过hash定位元素在数组中的索引,判断数组中该节点是否为NULL
- 为NULL则新建Node进行赋值
- 不为NULL,则有:
- hash值是否相等&&key值是否相等,相等则值用新值覆盖
- 节点是否为红黑树,是则插入红黑树
- 否,则为链表,遍历链表,
- key如果在链表中存在,则用新值覆盖,
- 如果不存在,则新节点插入链表尾部
- 判断链表长度是否超过TREEIFY_THRESHOLD:8
- 超过则调用转换方法
- 转换方法中,节点数超过 MIN_TREEIFY_CAPACITY 则链表转红黑树
- 小于则扩容
- ++modCount和++size
- size 大于threshold(扩容临界值),则进行扩容
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap GET实现
- 计算key的hash
- hash计算数组的index
- 找出index的节点
- 第一个节点hash和key相等,直接返回first node
- 不相等
- 是否为红黑树,是则在树中查找
- 链表,则遍历链表,找出节点
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashMap的扩容
- 为什么数组扩容,每次*2呢?
- 首先node对应数组的索引为hash & (length-1)
- 扩容后,node对应的数组的索引为hash & (2*length -1)
- 例如hash值为21 (10101),则有21 & 15 = 5,扩容后,21 & 31 = 21 = (5+16)
- 例如hash值为13 (01101),则与13 & 15 = 13 ,扩容后,13 & 31 = 13 = (13)
- 规律为 hash值对应的数组length的首位位数如果为1,则扩容后的数组位置为原index+length
- 首位位数如果为0,则扩容后的数组位置不变,WOW,是不是很巧妙,扩容过后,既可以减少节点数组位置的转换,同时也可将部门节点按照原数组位置进行扩容(+length)
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4.2.ConcurrentHashMap和HashTable
-
ConcurrentHashMap
在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现;
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作;
分析 JAVA8 中的PUT源码:
- 如果table为空,则初始化数组,重新循环
- 如果数组位置节点为空,则CAS创建节点
- 如果当前正在扩容,则获取扩容后的数组,重新循环
- 使用synchronized 对节点Node 进行加锁
- 如果node节点为链表,则在链表尾部插入节点
- 如果为红黑树,则插入红黑树中
- 最后判断是否需要在转换成功红黑树
/** * Maps the specified key to the specified value in this table. * Neither the key nor the value can be null. * * <p>The value can be retrieved by calling the {@code get} method * with a key that is equal to the original key. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with {@code key}, or * {@code null} if there was no mapping for {@code key} * @throws NullPointerException if the specified key or value is null */ public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }
get 的源码为:可以看到并没有进行锁操作
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
-
HashTable
HashTable的get、put等方法都使用synchronized关键字来实现的,是一种悲观锁的实现
public synchronized V put(K key, V value) public synchronized V get(Object key)
4.3.CopyOnWriteArrayList
适用场景:
- 读操作可以尽可能的快,而写即使慢一些也没关系
- 读多写少
- 对读写锁规则的升级:读取是完全不用加锁的,写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取;
- 含义:当容器需要被修改的时候,不直接修改当前容器,而是先将当前容器进行 Copy,复制出一个新的容器,然后修改新的容器,完成修改之后,再将原容器的引用指向新的容器。这样就完成了整个修改过程。
- 迭代期间允许修改集合内容。
缺点:
- 写时内存占用问题 (Copy副本)
- 在元素较多或者复杂的情况下,复制的开销很大
- 数据一致性问题
实现:
写时通过ReentrantLock锁实现加锁过程(Copy副本并添加元素)
5.队列
5.1 阻塞队列
- ArrayBlockingQueue:有界队列,内部使用数组实现,使用ReentrantLock实现线程安全
- LinkedBlockingQueue:无界队列,最大容量Integer.MAX_VALUE;
- SynchronousQueue: 容量为0,没有一个地方来暂存元素,每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。
- PriorityBlockingQueue:支持优先级的无界阻塞队列,自定义compareTo()实现队列中元素的先后;
- DelayQueue:无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,拥有了比较和排序的能力
5.2 阻塞队列是如何实现的?
ArrayBlockingQueue 的源码如下:
put方法:使用ReentrantLock实现加锁,notFull 队列未满Condition实现阻塞等待
offer: 使用ReentrantLock实现加锁,队列已满则返回false,不阻塞等待
take方法: 使用ReentrantLock实现加锁,notEmpty 队列不为空 Condition实现阻塞等待
poll: 使用ReentrantLock实现加锁,队列为空返回Null,不阻塞等待
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and throwing an
* {@code IllegalStateException} if this queue is full.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Collection#add})
* @throws IllegalStateException if this queue is full
* @throws NullPointerException if the specified element is null
*/
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and {@code false} if this queue
* is full. This method is generally preferable to method {@link #add},
* which can fail to insert an element only by throwing an exception.
*
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
/**
* Inserts the specified element at the tail of this queue, waiting
* up to the specified wait time for space to become available if
* the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return itemAt(takeIndex); // null when queue is empty
} finally {
lock.unlock();
}
}
ConcurrentLinkedQueue:
- 使用乐观锁的机制实现线程安全
- 循环+CAS来进行入队列
/**
* Inserts the specified element at the tail of this queue.
* As the queue is unbounded, this method will never return {@code false}.
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
5.3 选择合适的队列
- 需要排序或者延迟则选择PriorityBlockingQueue、DelayQueue
- 是否需要存储,根据存储的大小选择对应的有界队列或无界队列,无需存储则SynchronousQueue
- 如果不确定容量则更加是否能够扩容选择队列
- 根据对应的内存结构和加锁粒度及其锁的分类选择队列,对存储空间有限制的可以选择ArrayBlockingQueue,对并发情况一般可以选择无阻塞队列ConcurrentLinkedQueue,
6.CAS
7.ThreadLocal
7.1 使用场景
-
ThreadLocal用来保存每个线程独享的对象,可以解决线程安全的问题
-
ThreadLocal用来保存每个线程独有的信息,提供其他方法方便获取,例如用户登录信息等等
-
内存泄漏问题:Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → 可能泄漏的value实例
- 当threadLocal已经不被使用,但是一直存活不终止,即使将threadLocal的引用置为NULL,但是当threadLocal中的Value一直不回收,则引用链就一致存在,无法回收
- 即使threadLocal虽然为WeakReference,value不回收则不会回收
- 使用threadLocal中的remove方法解决此问题
8.AQS (AbstractQueuedSynchronizer)
AQS的作用:AQS 是一个用于构建锁、同步器等线程协作工具类的模板框架
8.1 AQS的实现有哪些?
- Semaphore
- CountDownLatch
- ReentrantLock
- CyclicBarrier
- ReentrantReadWriteLock
- ThreadPoolExcutor