CPU 总线锁
synchronized代码块可以保证同一个时刻只能有一个线程进入代码竞争区,synchronized代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会flush到主存,不管这些变量是不是volatile类型的。
处理器使用总线锁就是解决这个问题的。总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。总线锁把CPU和内存之间通信锁住,使得锁定期间,其他处理器也不能操作其他内存地址的数据,所以总线锁的开销比较大。
CPU MESI协议+内存屏障
- MESI协议(保障cpu缓存一致性)CPU默认遵循
在多核CPU中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题,而一致性协议正是用于保证多个CPU cache之间缓存共享数据的一致性。
当CPU从cache中读取数据时候,会检查其他cpu cache是否有相同地址,如果相同则检查cache line的状态判断该数据是否有效,无效则从主存中获取数据,或根据一致性协议发生一次cache-to-cache的数据推送。
-
内存屏障(保障可见性和有序性**),Volatile指令显式指定**
-
禁止JVM层面和CPU执行层面的指令重排。
-
将当前处理器缓存行数据刷立即写到系统主内存。
可见性是指,在多线程环境,共享变量的操作对于每个线程来说,都是内存可见的,也就是每个线程获取的volatile变量都是最新值;并且每个线程对volatile变量的修改,都直接刷新到主存。
参考:www.jianshu.com/p/2643c9ea1…
CAS共享变量更新方法原理
基于依赖volatile关键字实现,用轻量的自旋方式解决了volatile的原子性问题。从而实现了并发安全的三个特性,确保并发安全。
cas操作在各层面语义:
|
java语言层面 |
比如:getAndAddInt方法 |
|
虚拟机字节码 |
compareAndSwapInt本地方法 |
|
操作系统/汇编指令 |
Compare-and-Swap原子指令 |
|
CPU硬件 |
java语言层面直接调用cpu指令实现,不需要用户态到内核态的切换。但是存在轮询对于cpu的消耗。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
由这段代码看到:
unsafe.compareAndSwapInt(this, valueOffset, expect, update)
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
那么 CAS 核心重点来了,compareAndSwapInt 就是实现 CAS 的核心方法,其原理是如果 var1 中的 value 值和 var5 相等,就证明没有其他线程改变过这个变量,那么就把 value 值更新为 var5 + var4,其中 var4 是更新的增量值;反之如果没有更新,那么 CAS 就一直采用自旋的方式继续进行操作(其实就是个 while 循环),这一步也是一个原子操作。
举例分析:
- 设定 AtomicInteger 的 value 原始值为 A,从 Java 内存模型得知,线程 1 和线程 2 各自持有一份 value 的副本,值都是 A。
- 线程 1 通过getIntVolatile(var1, var2)拿到 value 值 A,这时线程 1 被挂起。
- 线程 2 也通过getIntVolatile(var1, var2)方法获取到 value 值 A,并执行compareAndSwapInt方法比较内存值也为 A,成功修改内存值为 B。
- 这时线程 1 恢复执行compareAndSwapInt方法比较,发现自己手里的值 A 和内存的值 B 不一致,说明该值已经被其它线程提前修改过了。
- 线程 1 重新执行getIntVolatile(var1, var2)再次获取 value 值,因为变量 value 被 volatile 修饰,所以其它线程对它的修改,线程 A 总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
上面的方法,有几个重要的参数:
(1)this,Unsafe 对象本身,需要通过这个类来获取 value 的内存偏移地址。
(2)valueOffset,value 变量的内存偏移地址。
(3)expect,期望更新的值。
(4)update,要更新的最新值。
如果原子变量中的 value 值等于 expect,则使用 update 值更新该值并返回 true,否则返回 false。
当然,如果感兴趣,也可以继续深入学习用到了哪些硬件 CPU 指令。
底层硬件通过将 CAS 里的多个操作在硬件层面语义实现上,通过一条处理器指令保证了原子性操作。这些指令如下所示:
(1)测试并设置(Tetst-and-Set)
(2)获取并增加(Fetch-and-Increment)
(3)交换(Swap)
(4)比较并交换(Compare-and-Swap)
(5)加载链接/条件存储(Load-Linked/Store-Conditional)
前面三条大部分处理器已经实现,后面的两条是现代处理器当中新增加的。而且根据不同的体系结构,指令存在着明显差异。
CAS存在的问题
- ABA问题
有两个线程A, B,共享数据i = 1;线程A读取i,线程B读取i,做了+1-1的操作,然后将结果写回内存。线程A通过case发现数据仍然为1,会继续操作。
- 轮询耗时问题。
等待线程不释放CPU,如果并发较大的场景,迟迟不能进入共享数据的更新,则CPU会对该线程进行长时间的空转,导致CPU资源浪费。
参考:bbs.huaweicloud.com/blogs/13615…
AQS工具类原理
AQS(AbstractQueuedSynchronizer**)是一个用来构建锁和同步器的框架**,使用AQS能简单且高效地构造出应用广泛的大量的同步器。
比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AQS 中有两个重要的成员:
- **成员变量 state。****用于表示锁现在的状态,用 volatile 修饰,****同时对 state 的操作都是使用 CAS 进行的。**state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减。
- **还有一个双向链表,**链表除了头结点外,每一个节点都记录了线程的信息,代表一个等待线程。这是一个 FIFO 的链表。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性和有序性
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
看个AQS(AbstractQueuedSynchronizer)原理图:
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。
AQS定义两种资源共享方式
在同步队列中,还存在2中模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁。
-
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
-
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
-
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
-
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
参考:
COW容器(写时复制)原理
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
读的时候不需要加锁,如果读的时候有其他线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为开始读的那一刻已经确定了读的对象是旧对象。
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单等场景。
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}