java GUC
CAS Unsafe
compare and swap比较并替换
CAS有三个参数:需要读写的内存位值(V)、进行比较的预期原值(A)和拟写入的新值(B)。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作
我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少
AtomicInteger内部都是使用了Unsafe类来进行CAS操作
Unsafe是实现CAS的核心类,Unsafe类提供了硬件级别的原子操作
AQS
AbstractQueuedSynchronizer,简称AQS。是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来,如常用的ReentrantLock、Semaphore、CountDownLatch等
AQS支持独占锁(Exclusive)和共享锁(Share)两种模式
独占锁:也叫互斥锁、排它锁,只能被一个线程获取到(如ReentrantLock、ReadWriteLock的写锁)
共享锁:可以被多个线程同时获取(如CountDownLatch、ReadWriteLock的读锁)
同步队列:在线程尝试获取资源失败后,会进入同步队列队尾,给前继节点设置一个唤醒信号后,自身进入等待状态(通过LockSupport.park(this)),直到被前继节点唤醒
条件队列:是为Condition实现的一个同步器,一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。需要注意的是,如果一个线程被唤醒(condition.signal())后,它会从条件队列转移到同步队列来等待获取锁,后面对条件队列进行源码分析时会再详细讲解。
acquire(int):独占模式下获取锁/资源(写锁lock.lock()内部实现)
release(int):独占模式下释放锁/资源(写锁lock.unlock()内部实现)
acquireShared(int):共享模式下获取锁/资源(读锁lock.lock()内部实现)
releaseShared(int):共享模式下释放锁/资源(读锁lock.unlock()内部实现)
ReentrantLock
可重入的互斥锁,也被称为“独占锁”,可以被单个线程多次获取
lock() 获取失败后,线程进入等待队列自旋或休眠,直到锁可用,并且忽略中断的影响
lockInterruptibly() 线程进入等待队列park后,如果线程被中断,则直接响应中断(抛出InterruptedException)
tryLock() 获取锁失败后直接返回,不进入等待队列
tryLock(long time, TimeUnit unit) 获取锁失败等待给定的时间后返回获取结果
ReetrantLock通过AQS实现了自己的同步器Sync,分为公平锁FairSync和非公平锁NonfairSync。在构造时,通过所传参数boolean fair来确定使用那种类型的锁。
synchronized和ReentrantLock的选择
ReentrantLock在加锁和内存上提供的语义与内置锁synchronized相同,此外它还提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁。从性能方面来说,synchronized上做了大量优化,使得两者的性能差距不大。synchronized的优点就是简洁
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
try{
lock.lock();
condition.await();
}finally{
lock.unlock();
}
CountDownLatch
同步辅助类,通过AQS实现的一个闭锁。在其他线程完成它们的操作之前,允许一个多个线程等待。简单来说,CountDownLatch中有一个锁计数,在计数到达0之前,线程会一直等待
CountDownLatch doneSignal = new CountDownLatch(3);
doneSignal.await();
doneSignal.countDown(); 计数 -1
总计数 = 3 当计数为零,这取消线程等待
ConcurrentHashMap
支持并发检索和并发更新的线程安全的HashMap(但不允许空key或value)
jdk 8 通过CAS+synchronized来保证并发安全
HashMap 是非线程安全的哈希表,常用于单线程程序中。
Hashtable 是线程安全的哈希表,由于是通过内置锁 synchronized 来保证线程安全,在资源争用比较高的环境下,Hashtable 的效率比较低。
ConcurrentHashMap 是一个支持并发操作的线程安全的HashMap,但是他不允许存储空key或value。使用CAS+synchronized来保证并发安全,在并发访问时不需要阻塞线程,所以效率是比Hashtable 要高的。
Node 类别
Node<K, V>: 保存k-v、k的hash值和链表的 next 节点引用
TreeNode<K, V>: 红黑树节点类,当链表长度>=8且数组长度>=64时,Node 会转为 TreeNode
reeBin<K, V>: 封装了 TreeNode,红黑树的根节点,也就是说在 ConcurrentHashMap 中红黑树存储的是 TreeBin 对象
核心参数
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;
hash计算公式:hash = (key.hashCode ^ (key.hashCode >>> 16)) & HASH_BITS
索引计算公式:(table.length-1)&hash
put
1、计算当前key的hash值,根据hash值计算索引 i (i=(table.length - 1) & hash);
2、如果当前table为null,说明是第一次进行put操作,调用initTable()初始化table;
3、如果索引 i 位置的节点 f 为空,则直接把当前值作为新的节点直接插入到索引 i 位置;
4、如果节点 f 的hash为-1(f.hash == MOVED(-1)),说明当前节点处于移动状态(或者说是其他线程正在对 f 节点进行转移/扩容操作),此时调用helpTransfer(tab, f)帮助转移/扩容;
5、如果不属于上述条件,说明已经有元素存储到索引 i 处(即发生了哈希碰撞),此时需要对索引 i 处的节点 f 进行 put or update 操作,首先使用内置锁 synchronized 对节点 f 进行加锁:
6、如果f.hash>=0,说明 i 位置是一个链表,并且节点 f 是这个链表的头节点,则对 f 节点进行遍历,此时分两种情况:
7、如果链表中某个节点e的key与当前key的hash相同,则对这个节点e的value进行修改操作。
8、如果遍历到链表尾都没有找到与当前key相同的节点,则把当前K-V作为一个新的节点插入到这个链表尾部。
9、如果节点 f 是TreeBin节点(f instanceof TreeBin),说明索引 i 位置的节点是一个红黑树,则调用putTreeVal方法找到一个已存在的节点进行修改,或者是把当前K-V放入一个新的节点(put or update)。
10、完成插入后,如果索引 i 处是一个链表,并且在插入新的节点后节点数>8,则调用treeifyBin把链表转换为红黑树。
最后,调用addCount更新元素数量
CopyOnWriteArrayList
线程安全的 ArrayList,通过内部的 volatile 数组和显式锁 ReentrantLock 来实现线程安全
适合元素比较少,并且读取操作高于更新(add/set/remove)操作的场景
由于每次更新需要复制内部数组,所以更新操作开销比较大
内部的迭代器 iterator 使用了“快照”技术,存储了内部数组快照, 所以它的 iterator 不支持remove、set、add操作,但是通过迭代器进行并发读取时效率很高。
核心参数
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
内部使用了一个 volatile 数组(array)来存储数据,保证了多线程环境下的可见性。在更新数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给 array。正由于这个原因,涉及到数据更新的操作效率很低。
add
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
LinkedBlockingQueue
单向链表结构的自定义容量的阻塞队列,元素操作按照FIFO(first-in-first-out 先入先出)的顺序,使用显式锁 ReentrantLock 和 Condition 来保证线程安全。链表结构的队列通常比基于数组的队列(ArrayBlockingQueue)有更高的吞吐量,但是在并发环境下性能却不如数组队列
核心参数
private final int capacity;
private final AtomicInteger count = new AtomicInteger();
transient Node<E> head;
private transient Node<E> last;
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
若某线程(线程A)要取出数据时,队列正好为空,则该线程会执行notEmpty.await()进行等待;当其它某个线程(线程B)向队列中插入了数据之后,会调用notEmpty.signal()唤醒notEmpty上的等待线程。此时,线程A会被唤醒从而得以继续运行。 此外,线程A在执行取操作前,会获取takeLock,在取操作执行完毕再释放takeLock。
若某线程(线程H)要插入数据时,队列已满,则该线程会它执行notFull.await()进行等待;当其它某个线程(线程I)取出数据之后,会调用notFull.signal()唤醒notFull上的等待线程。此时,线程H就会被唤醒从而得以继续运行。 此外,线程H在执行插入操作前,会获取putLock,在插入操作执行完毕才释放putLock。
添加元素的方法有offer()、put(),put是在队列已满的情况下等待,而offer则直接返回结果
获取元素的方法有poll()、take()、peek() take在队列为空的情况下会一直等待,poll不等待直接返回结果,peek是获取但不移除头结点元素
SynchronousQueue
同步阻塞队列,它的每个插入操作都要等待其他线程相应的移除操作,反之亦然
SynchronousQueue(后面称SQ)内部没有容量,所以不能通过peek方法获取头部元素;也不能单独插入元素,可以简单理解为它的插入和移除是“一对”对称的操作。
使用了双重队列(Dual queue)和双重栈(Dual stack)存储数据
DelayQueue
无界延时阻塞队列,元素顺序按照过期时间排序,通过显式锁 ReentrantLock 保证并发安全,队列中的存储的元素必须实现 Delayed 接口,也就是说只允许放入可以“延期”的元素
存储元素必须实现Delayed接口
内部持有一个ReentrantLock保证线程安全
使用优先级队列PriorityQueue实现元素存储
持有一个优化内部阻塞通知的线程leader
用于实现阻塞的Condition对象
DelayQueue 添加或入列操作方法包括put、add、offer,都是通过offer方法实现
DelayQueue 的出列或获取元素方法包括poll、take、peek,poll直接获取元素,如果队列中没有届期元素返回null;take会一直等待元素可用;peek只获取但不移除元素。相对来说,take方法内包括了另外两个方法的逻辑
FutureTask
可取消的异步运算,可以把它看作是一个异步操作的结果的占位符,它将在未来的某个时刻完成,并提供对其结果的访问
FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证
FutureTask 内部维护了一个由volatile修饰的int型变量—state,代表当前任务的运行状态,state有七种状态:
NEW:新建
COMPLETING:完成
NORMAL:正常运行
EXCEPTIONAL:异常退出
CANCELLED:任务取消
INTERRUPTING:线程中断中
INTERRUPTED:线程已中断
在这七种状态中,有四种任务终止状态:NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED。各种状态的转化如下:
private Callable<V> callable;
private Object outcome;
private volatile Thread runner;
private volatile WaitNode waiters;
FutureTask 通过get()方法获取任务执行结果。如果任务处于未完成的状态(state <= COMPLETING),就调用awaitDone方法(后面单独讲解)等待任务完成。任务完成后,通过report方法获取执行结果或抛出执行期间的异常
FutureTask是 Java 中的一个类,它实现了RunnableFuture接口,该接口继承自Runnable和Future接口。这使得FutureTask既可以作为一个任务在单独的线程中执行,又可以返回一个结果,并且可以查询任务的执行状态、等待任务完成等
Callable<Integer> task = () -> {
System.out.println("Task is running.");
Thread.sleep(2000);
return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("Main thread is doing other things.");
Integer result = futureTask.get();
System.out.println("Task result: " + result);
FutureTask可以通过cancel(boolean mayInterruptIfRunning)方法来尝试取消任务的执行
状态查询
FutureTask提供了方法来查询任务的状态,如isDone()方法可以判断任务是否已经完成,isCancelled()方法可以判断任务是否被取消。
interface Future<V>
boolean cancel(boolean var1) 取消
boolean isCancelled() 是否取消状态
boolean isDone() 是否完成
V get() 阻塞获取执行结果
V get(long var1, TimeUnit var3) 阻塞获取执行结果,可超时
Thread.yield
Thread.yield()是一个静态方法。它的作用是提示当前正在执行的线程放弃执行权,使当前线程从运行状态转换为就绪状态,从而让具有相同优先级的其他线程有机会执行。
不保证效果
调用Thread.yield()只是一个提示,不能保证操作系统一定会切换到其他线程执行。实际上,具体的线程调度策略由操作系统决定,因此不能依赖Thread.yield()来精确控制线程的执行顺序。
优先级影响
线程的优先级也会影响调度。即使调用了Thread.yield(),如果有更高优先级的线程处于就绪状态,操作系统可能会优先选择执行更高优先级的线程。
性能影响
频繁地调用Thread.yield()可能会导致性能下降,因为它会增加线程切换的开销。所以,应该谨慎使用Thread.yield(),只在确实需要提高公平性或避免某个线程长时间占用 CPU 的情况下使用。
Thread.join
Thread.join()方法用于等待一个线程终止
当一个线程调用另一个线程的join()方法时,调用线程将被阻塞,直到被调用线程完成执行
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread 1 completed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread resumed after thread1 completed.");
在这个例子中,主线程调用thread1.join()后会进入阻塞状态,直到thread1执行完毕,主线程才会继续执行。
join():无参数形式,等待被调用线程执行完毕。
join(long millis):等待被调用线程执行指定的最长时间(以毫秒为单位)。如果在指定时间内被调用线程完成执行,方法返回;如果超时,方法也会返回,调用线程不再等待。
join(long millis, int nanos):等待被调用线程执行指定的时间,包含毫秒和纳秒部分
可以使用 CountDownLatch 和 FutureTask 来替代join实现等待线程执行完成的操作