从Java到CPU指令系列之 - 锁和原子操作是怎么实现的 How Lock and atomic works

给急性格的读者: 请参考《Intel 64 and IA32 Architectures Software Develeloper‘s Manual》。其中有CMPXCHG指令和LOCK指令前缀。或者AMD等其他厂商的开发指南。

在多线程编程中,对某一资源的同步操作是保证资源状态一致性的关键。这个需要同步的资源可以是单个简单的变量,也可以是多个变量,或者是某些外部资源。对他们同步操作的含义就是同一时间点,最多只能有一个线程在操作这些资源,也就是排他性。并且一系列操作必须一气呵成,中间不允许其他线程做相关的操作,这就是原子性。所以我们在程序中需要让一段代码,在同一时间最多只能有一个线程执行。在Java里我们有Synchronized关键字来标注一段代码在任意时刻只能最多有一个线程在执行。在新的Java Concurrent包里边提供了Lock相关的类。通过Lock我们可以实现与Synchronized同样的效果。这段代码通常我们叫它临界区(Critical Section)。

但是Lock和Synchronize是怎么实现的呢?

从代码上看起来好像在某个地方有个开关。一个线程执行到临界区时,检查这个开关,如果是0代表可以进入,然后把开关设成1,进入,退出,把开关设成0。看起来很完美?其实没有那么简单。因为你这个开关本身可能被多个线程同时检查,它们同时检查到了0,同时进入。。啊 这可不好。再弄个开关?好像陷入无穷无尽的开关也解决不了问题。更天才的想法是,把自己的线程号赋予这个变量,然后等待一小段时间,再检查这个变量是不是自己的进程号,如果是自己,恭喜抢到了。等一小段时间是为了防止其它线程同时检查了,并在你检查之后写了它的线程号。但是,这个等待时间可能很难确定,CPU的线程调度让以上线程间的操作无序且无法知道。看看,这个问题真的貌似不好通过简单的程序诡计解决啊。也许你可以想出更天才的纯软件解决方案,但它可能不那么可靠或者高效。

软件解决不了的问题,其实利用硬件很容易解决。大多CPU都提供了原子操作。请参考开头我提到的文档。CPU保证了某些对单个变量的比较和交换操作是原子的。也就是比较某个数是不是你预期的,如果是,赋予你给的变量,否则不做操作。可以想象CPU在执行此类指令时,可能要暂停多核情况下其它核心的执行,霸占总线,让这个操作不被打断-除非一个CPU周期就可以做完,并且可能要清理CPU的缓存。。。 但是这些还是比我们上边提到的纯软件方法快速且可靠吧!向硬件工程师致敬!

好了,回头看看我们的Java怎么用到CMPXCHG的。

首先进入Java世界。请看ReentrantLock$FairSync.tryAcquire。有没有看到一句话跟我们说的CMPXCHG是同义词?对了,就是compareAndSetState(0, acquires)。一路着下去。。 到了Unsafe.compareAndSwapInt(针对Sun/Oracle Java).这个方法是native的-我们到达了Java世界的边缘。

然后,让我们勇敢的进入C++的世界。请找到OpenJDK的unsafe.cpp。找到:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

是不是找到了atomic.cpp。你看到了很多平台判断的预处理指令。啊,神奇的预处理,C语言实现跨平台的法宝。看起来到此为止,我们也到达了C/C++世界的边缘。

最后,向汇编语言-CPU指令的等价符号-前进。找个比较亲民的平台的把,x86下的atomic_linux_x86.inline.hpp。是的HPP,这里还不是.S。因为C/C++跟汇编的世界交界处不像到Java的世界那样有一堵不透风的墙。汇编可以自由的嵌入C/C++。到此我们终于找到了CPU指令了。

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

接下来还有CPU微指令,逻辑电路,与非门。哈哈,只是开个玩笑,我们还是到此打住。向硬件工程师再次致敬!

从Java到CPU指令系列之 - 锁和原子操作是怎么实现的 How Lock and atomic works

时间: 2024-08-16 00:56:07

从Java到CPU指令系列之 - 锁和原子操作是怎么实现的 How Lock and atomic works的相关文章

无锁-CAS原子操作

CAS原子操作--Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是 CMPXCHG 汇编指令. 大家应该还记得操作系统里面关于"原子操作"的概念,一个操作是原子的(atomic),如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构.原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分.有了这个原子操作这个保证我们就可以实现无锁了. 相对

Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)

一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专家又往往建议我们远离它.比如Thread这个很基础的类,其中很重要的线程状态字段,就是用volatile来修饰,见代码 /* Java thread status for tools, * initialized to indicate thread 'not yet started' */   p

Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

概要 本章,我们对JUC包中的信号量Semaphore进行学习.内容包括:Semaphore简介Semaphore数据结构Semaphore源码分析(基于JDK1.7.0_40)Semaphore示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3534050.html Semaphore简介 Semaphore是一个计数信号量,它的本质是一个"共享锁". 信号量维护了一个信号量许可集.线程可以通过调用acquire()来获取信号量的许可

java多线程并发系列之锁的深入了解

上一篇博客中 : java多线程.并发系列之 (synchronized)同步与加锁机制 .介绍了java中Synchronized和简单的加锁机制,在加锁的模块中介绍了 轮询锁和定时锁,简单回顾下 轮询锁:利用tryLock来获取两个锁,如果不能同时获得,那么回退并重新尝试. 定时锁:索取锁的时候可以设定一个超时时间,如果超过这个时间还没索取到锁,则不会继续堵塞而是放弃此次任务. 锁的公平性 在公平的锁上,线程将按照它们发出请求的顺序来获取锁 上面似乎忘记了还有一种可中断锁和可选择粒度锁 可中

Java多线程系列--“JUC锁”05之 非公平锁

获取非公平锁(基于JDK1.7.0_40) 非公平锁和公平锁在获取锁的方法上,流程是一样的:它们的区别主要表现在"尝试获取锁的机制不同".简单点说,"公平锁"在每次尝试获取锁时,都是采用公平策略(根据等待队列依次排序等待):而"非公平锁"在每次尝试获取锁时,都是采用的非公平策略(无视等待队列,直接尝试获取锁,如果锁是空闲的,即可获取状态,则获取锁).在前面的"Java多线程系列--"JUC锁"03之 公平锁(一)&q

Java多线程系列--“JUC锁”04之 公平锁(二)

前面一章,我们学习了"公平锁"获取锁的详细流程:这里,我们再来看看"公平锁"释放锁的过程."公平锁"的获取过程请参考"Java多线程系列–"JUC锁"03之 公平锁(一)",锁的使用示例请参考"Java多线程系列–"JUC锁"02之 互斥锁ReentrantLock". 注意(01)这里是以"公平锁"来进行说明.(02)关于本章的术语,如"

Java并发编程:synchronized和锁优化

每天学习一点点 编程PDF电子书.视频教程免费下载:http://www.shitanlife.com/code 1. 使用方法 synchronized 是 java 中最常用的保证线程安全的方式,synchronized 的作用主要有三方面: 确保线程互斥的访问代码块,同一时刻只有一个方法可以进入到临界区 保证共享变量的修改能及时可见 有效解决重排序问题 语义上来讲,synchronized主要有三种用法: 修饰普通方法,锁的是当前对象实例(this) 修饰静态方法,锁的是当前 Class

Java 虚拟机:互斥同步、锁优化及synchronized和volatile

互斥同步 互斥同步(Mutual Exclusion & Synchronization)是常见的一种并发正确性保证手段.同步是指子啊多个线程并发访问共享数据时,保证共享数据在同一时刻只能被一个(或者是一些,使用信号量的时候)线程使用.而互斥是实现同步的一种手段,临界区(Critial Section).互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式.因此,在这四个字里面,互斥是因,同步是果:互斥是方法,同步是目的. synchronized的实现 在Java中,大家都

2020年Java多线程与并发系列22道高频面试题(附思维导图和答案解析)

前言 现在不管是大公司还是小公司,去面试都会问到多线程与并发编程的知识,大家面试的时候这方面的知识一定要提前做好储备. 关于多线程与并发的知识总结了一个思维导图,分享给大家 1.Java中实现多线程有几种方法 (1)继承Thread类: (2)实现Runnable接口: (3)实现Callable接口通过FutureTask包装器来创建Thread线程: (4)使用ExecutorService.Callable.Future实现有返回结果的多线程(也就是使用了ExecutorService来管