CAS
1. 一句话总结
- compare and swap 比较并交换;在更新值的时候检查下值有没有发生变化,如果没有发生变化则更新
- 通过底层硬件获得原子性语句的支持。
哪条原子性语句呢? - cpu的
lock cmpxchgl
2. ABA问题
- 如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题
- 解决思路:使用版本号
- 在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题
3. 是否要自旋 (比如 while(true){})
CAS的实现方式不同,
- 如果自旋就会导致本线程,本CPU不停的空转,浪费时间片;
- 解决方案可以参照jdk8之后的 jdk#LongAdder设计
- 如果不自旋,就是超级乐观的锁设计,只适合线程冲突小的系统,因为不自旋的话,compare失败的线程就结束了(除非你再设计一套唤醒机制, 比如下面的AQS)
4.java的实现
java中使用Unsafe类调用c++代码
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这个本地方法在openjdk中依次调用的c++代码为:
- unsafe.cpp
- atomic.cpp
- atomicwindowsx86.inline.hpp 最终走到下面的代码, 如果是多核cpu,就锁内存总线,保证原子性
/ alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
AQS
是AbstractQueuedSynchronizer类的缩写,抽象队列同步器
1. java中的常见应用
- CountDownLatch
- ReenTrantLock
- Semphare
2. AQS的原理,用一句话概括
如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中等待被唤醒(防止过度自旋,浪费cpu)。
3. 和synchronized的区别
4. 为什么要有一个队列
将获取锁失败的线程放进队列等待,仍有被唤醒并获取锁的机会,这个机会和策略由CLH队列实现;
CLH:Craig、Landin and Hagersten(3个外国人)队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
5. CLH队列第一个节点为什么是虚节点
目的 :∵节点入队不是原子操作,首节点是虚节点就是为了解决高并发场景下的线程安全问题
在第一个线程设置state的时候,CLH队列为空; 在第二个线程设置的时候,CAS失败,加入CLH队列的时候,首节点Node不是线程2,而是AQS无参构造器生成的节点,第二个节点才是线程2
我们关注AQS的hasQueuedPredecessors()方法,是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False,说明当前线程可以争取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中
// java.util.concurrent.locks.ReentrantLock
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
- 当h != t时: 如果(s = h.next) == null,等待队列正在有线程进行初始化,但只是进行到了Tail指向Head,没有将Head指向Tail,此时队列中有元素,需要返回True(这块具体见下边代码分析)。
- 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。
- 如果此时s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;
- 如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列 如果之后还有线程竞争,就在队列中继续排队就行。
6. AQS核心方法
- 加锁 acquire()
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到tryAcquire是尝试拿锁,如果获取失败,才会加入队列中
而tryAcquire(), 发现AQS内只是简单实现,实际使用时需要自己Override, 比如ReenTrantLock就自己重写了
// java.util.concurrent.locks.AbstractQueuedSynchronizer
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
- 进入队列
// java.util.concurrent.locks.AbstractQueuedSynchronizer
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 标记等待过程中是否中断过
boolean interrupted = false;
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点)
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,final修饰,不能被覆写。上述代码主要的思路是:
- 开始自旋,不断尝试获取锁
- 如果获取到锁就结束,返回 ; 否则就休息一会再尝试获取锁
- finally代码块中,在获取锁最终失败后,会将节点状态置为CANCELLED
- 不断校验当前节点的前驱节点,如果状态是CANCELLED, 就剔除这个节点,将当前节点往前移动
- 如果前驱节点状态是SIGNAL(人家前驱节点有资格获取锁),当前节点就休息(park, 阻塞)会吧
- 借用LockSupport的静态方法park()
- 返回当前线程的应该中断与否,上层方法会执行Thread.interrupted()进行中断
ps
- 中断和阻塞的区别
- Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己
- 中断是调用Thread.interrupt(),设置一个状态而已,true表示已中断,false表示未中断。设置线程中断不影响线程的继续执行,但是线程设置中断后,线程内调用了wait、jion、sleep方法中的一种, 立马抛出一个 InterruptedException,且中断标志被清除,重新设置为false。
- 阻塞才能使线程真正地停下来,需要设置线程为阻塞,使用LockSupport.park(),具体原理此处不表。
7. 【总结】关键节点
CLH队列中Node节点的状态
- 何时出队列
- 如何出队列
- 何时阻塞
- 何时中断
- 何时唤醒 总的来说,一个线程获取锁失败了,被放入等待队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。
synchronized
1. 用法
- 用于非静态方法
- 用于静态方法
- 用于锁对象
- 用于代码块
public class useSynchronized {
//1.修饰普通方法, 锁是当前类的某一对象,即this
public synchronized void normalMethod(){
}
//2.修饰静态方法,锁是useSynchronized.class
public synchronized static void staticMethod(){
}
//没有同步的方法
public void methodWithoutSync(){
}
public void methodA(){
synchronized (useSynchronized.class){
//3.同步代码块,锁是类对象
}
}
public void methodB(A a){
synchronized (a){
//4.同步代码块
}
}
2.一句话总结
轻量级锁使用CAS, 底层时lock cmpxchg指令
重量级锁用的管程monitor,底层使用c++ ObjectMonitor, 是对象私有的
3. 锁升级
其中, 无锁、偏向、轻量级锁都是在用户态;重量级锁需要向内核态申请资源
升级过程
4.锁升级条件
偏向锁升级轻量级锁--条件:
- 只要有别的线程竞争
轻量级锁升级重量级锁--条件
- 【曾经是】竞争超过10次 or 等待线程超过 1/2 CPU核数 or 耗时过长 or 有wait操作
- 【现在是】自适应的,jvm自动升级
轻量级锁发生竞争时:
- 各个线程使用 CAS 操作,对对象的 markword进行比较--交换,尝试写上自己线程id
5.预习知识
- markWord
6.基本原理
- 偏向锁
- 在对象头markword中,写了第一个线程的thread Id,寄希望与没有人改它,所以连CAS都不用做
- 在偏向锁到轻量级锁升级的过程中,
- 使用CAS,不停比较存储的mardWord和本线程的id
- CAS, 底层是lock cmpxchg
- 轻量级锁(自旋锁)
- 尝试使用CAS去 给markword写入线程独有的lockRecord起始地址 ,如果失败就一直自旋
- 重量级锁
-
用的 monitor管程
-
synchornized 方法,JVM使用ACC_SYNCHRONIZED标识来实现。即JVM通过在方法访问标识符(flags)中加入ACC_SYNCHRONIZED来实现同步功能, 根本上也是monitorenter和monitorexit。
-
synchornized代码块,JVM使用monitorenter和monitorexit两个指令实现同步。即JVM为代码块的前后真正生成了两个字节码指令来实现同步功能的。
-
-
底层使用的 c++ ObjectMonitor类来实现上面的原语,锁住的对象,在锁升级到重量级时,自动生成一个ObjectMonitor对象,作为内置锁,用来让各个线程争抢。
-
无论上面两个指令中的哪一个,均是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
-
巧了,wait(), nitify(), notifyall()正是互斥量模型的原语,以及java object类的方法
【为什么这些方法都在Object类(而不是Thread类)呢?
- 这就是该类所私有的ObjectMonitor对象的c++方法】
ReentrantLock
1. 原理
CAS + AQS
// java.util.concurrent.locks.ReentrantLock#NonfairSync
// 非公平锁
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}
2. 和ReenTrantReadWriteLock的区别
它和后者都分别实现了AQS,彼此之间没有继承或实现的关系。
ReenTrantLock是互斥锁(独占锁), 也就是一次只能有一个线程持有锁
ReenTrantReadWriteLock是共享锁, 一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作(修改数据)
3. 加锁和解锁流程
Atomic类
1. 一句话总结
- atomic类是通过自旋+CAS操作+volatile变量实现的。
2. jdk8 的LongAdder 和 AtomicLong相比,优化了哪里?
高并发下N多线程同时去操作一个变量会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。既然AtomicLong性能问题是由于过多线程同时去竞争同一个变量的更新而降低的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源,这就是LongAdder的原理。
LongAdder则是内部维护一个Cells数组,每个Cell里面有一个初始值为0的long型变量,在同等并发量的情况下,争夺单个变量的线程会减少,这是变相的减少了争夺共享资源的并发量,另外多个线程在争夺同一个原子变量时候,如果失败并不是自旋CAS重试,而是尝试获取其他原子变量的锁,最后当获取当前值时候是把所有变量的值累加后再加上base的值返回的。
ThreadLocal
1.简介
对比其他解决线程安全的方案,更轻量级,开销更小,且更简单。
2.如何使用
static ThreadLocal<A> threadLocal= new ThreadLocal<>();
threadLocal.set(1996);
int temp = threadLocal.get();
threadLocal.remove();
3、一句话总结
- 每个Thread对象,其ThreadLocalMap都不一样, 即线程私有;
- 通过弱引用 来避免内存泄漏
4.原理
每个线程都有1个ThreadLocalMap变量, key是ThreadLocal对象, value是Entry对象,其属性只有Object value
# java.lang.Thread
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadlocal = null
而ThreadLocalMap本身是ThreadLocal的静态内部类, 其内部又维护了另一个静态内部类Entry的实例数组
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
//省略下面的
}
通过ThreadLocal.get()时,ThreadLocal转交给
当前线程的ThreadLocalMap去get()
get()的源码如下
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
5.为什么使用弱引用WeakReference
目的:防止内存泄漏
6.为什么要用hashCode()? hash方法怎么设计的?
- 因为实现Map的时候,使用简单的数组Entry[] 来存储节点, (而不是TreeMap等方式实现)
- 如果要根据key来获取Entry 及value, 使用hash比较常见。
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
上面源码中的magic number,其实是斐波那契数列的魔数,每次hash得到的结果
下面就枚举了,size分别为16,32, 64的情况下,每次hash得到的index
16:0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9
32:0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25
64:0 7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57
观察上面的index,我们会发现:
使用这样的hash方法,在数组填满前,不会冲突。
7.hash冲突的时候怎么解决?
- 冲突时,采用开放地址法,寻找合适的插入位置。index++往后追加
- 这种方式在冲突频繁的时候,查询性能就差一些,所以尽量要避免冲突。 使用斐波那契hash算法就是尽量避免冲突
volatile
1.简介
轻量级地解决线程同步问题
2.作用
- 【保证线程可见性】
- 【禁止指令重排序】
- 【不保证原子性】
3.原理
-
一句话原理 voaltile关键字 --> jvm 字节码【ACC_VOLATILE】 --> c++【voaltile关键字 】--> 【lock addl】 实现
-
实现可见性 通过Lock前缀实现可见性,lock指令下一条指令, 写完缓存后,当前CPU回写主存, 并立即通知其他线程重新读缓存行
-
实现有序性
JMM层面的“内存屏障”: LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
- JVM的实现会在volatile读写前后均加上内存屏障,在一定程度上保证有序性。如下所示:
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
但上面的内存屏障只是jvm的实现要求, Hot spot实现还是汇编的lock addl指令实现的
addl把寄存器的值加0,相当于一个空操作,关键还是想要addl前面的[lock] (之所以用addl,不用空操作专用指令nop,是因为lock前缀不允许配合nop指令使用)