java GUC 知识整理

110 阅读14分钟

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;
//默认并发度,兼容1.7及之前版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//加载/扩容因子,实际使用n - (n >>> 2)
private static final float LOAD_FACTOR = 0.75f;
//链表转红黑树的节点数阀值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转链表的节点数阀值
static final int UNTREEIFY_THRESHOLD = 6;
//当数组长度还未超过64,优先数组的扩容,否则将链表转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//扩容时任务的最小转移节点数
private static final int MIN_TRANSFER_STRIDE = 16;
//sizeCtl中记录stamp的位数
private static int RESIZE_STAMP_BITS = 16;
//帮助扩容的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//size在sizeCtl中的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

//存放Node元素的数组,在第一次插入数据时初始化
transient volatile Node<K,V>[] table;
//一个过渡的table表,只有在扩容的时候才会使用
private transient volatile Node<K,V>[] nextTable;
//基础计数器值(size = baseCount + CounterCell[i].value)
private transient volatile long baseCount;
//控制table初始化和扩容操作
private transient volatile int sizeCtl;
//节点转移时下一个需要转移的table索引
private transient volatile int transferIndex;
//元素变化时用于控制自旋
private transient volatile int cellsBusy;
// 保存table中的每个节点的元素个数 2的幂次方
// size = baseCount + CounterCell[i].value
private transient volatile CounterCell[] counterCells;


hash计算公式:hash = (key.hashCode ^ (key.hashCode >>> 16)) & HASH_BITS
索引计算公式:(table.length-1)&hash

put

1、计算当前key的hash值,根据hash值计算索引 ii=(table.length - 1) & hash);

2、如果当前table为null,说明是第一次进行put操作,调用initTable()初始化table3、如果索引 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

//调用native方法根据index拷贝原数组的前半段
            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。各种状态的转化如下:

//内部持有的callable任务,运行完毕后置空
private Callable<V> callable;

//从get()中返回的结果或抛出的异常
private Object outcome; // non-volatile, protected by state reads/writes

//运行callable的线程
private volatile Thread runner;

//使用Treiber栈保存等待线程
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();
             //String result = futureTask.get(2000, TimeUnit.MILLISECONDS);
             System.out.println("Task result: " + result);

FutureTask可以通过cancel(boolean mayInterruptIfRunning)方法来尝试取消任务的执行

状态查询
FutureTask提供了方法来查询任务的状态,如isDone()方法可以判断任务是否已经完成,isCancelled()方法可以判断任务是否被取消。

//可结合线程池  submit 方法处理


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实现等待线程执行完成的操作