浅谈Java 并发的底层实现
|
并发编程的目的是让程序运行更快,但是使用并发并不定会使得程序运行更快,只有当程序的并发数量达到一定的量级的时候才能体现并发编程的优势。所以谈并发编程在高并发量的时候才有意义。虽然目前还没有开发过高并发量的程序,但是学习并发是为了更好理解一些分布式架构。那么当程序的并发量不高,比如是单线程的程序,单线程的执行效率反而比多线程更高。这又是为什么呢?熟悉操作系统的应该知道,CPU是通过给每个线程分配时间片的方式实现多线程的。这样,当CPU从一个任务切换到另一个任务的时候,会保存上一个任务的状态,当执行完这个任务的时候CPU就会继续上一个任务的状态继续执行。这个过程称为上下文切换。 在Java多线程中,volatile关键字个synchronized关键字扮演了重要的角色,它们都可以实现线程的同步,但是在底层是如何实现的呢? volatile volatile只能保证变量对各个线程的可见性,但不能保证原子性。关于 Java语言 volatile 的使用方法就不多说了,我的建议是 除了 配合package java.util.concurrent.atomic 中的类库,其他情况一概别用。更多的解释 参见 这篇文章。 引子 参见如下代码
package org.go;
public class Go {
volatile int i = 0;
private void inc() {
i++;
}
public static void main(String[] args) {
Go go = new Go();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++)
go.inc();
}).start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(go.i);
}
}
每次执行上述代码结果都不同,输出的数字总是小于10000.这是因为在进行inc()的时候,i++并不是原子操作。或许有些人会提议说用 synchronized 来同步inc(),或者 用 package java.util.concurrent.locks 下的锁去控制线程同步。但它们都不如下面的解决方案:
package org.go;
import java.util.concurrent.atomic.AtomicInteger;
public class Go {
AtomicInteger i = new AtomicInteger(0);
private void inc() {
i.getAndIncrement();
}
public static void main(String[] args) {
Go go = new Go();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++)
go.inc();
}).start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(go.i);
}
}
这时,如果你不了解 atomic 的实现,你一定会不屑的怀疑 说不定 AtomicInteger 底层就是使用锁来实现的所以也未必高效。那么究竟是什么,我们来看看。 原子类的内部实现 无论是AtomicInteger 或者是 ConcurrentLinkedQueue的节点类ConcurrentLinkedQueue.Node,他们都有个静态变量 以接口 getAndIncrement()的实现举例 AtomicInteger.java
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current,next))
return current;
}
}
public final boolean compareAndSet(int expect,int update) {
return unsafe.compareAndSwapInt(this,valueOffset,expect,update);
}
留意这个for循环,只有在compareAndSet成功时才会返回。否则就一直compareAndSet。 调用了compareAndSet实现。此处,我注意到 Oracle JDK的实现是略有不同的,如果你查看JDK下的src,你可以看到Oracle JDK是调用的Unsafe的getAndIncrement(),但我相信Oracle JDK实现Unsafe.java的时候应该也是只调用compareAndSet,因为一个compareAndSet就可以实现增加、减少、设值的原子操作了。 Unsafe.java public native boolean compareAndSwapInt(Object obj,long offset,int expect,int update); 通过JNI调用的C++的实现。 natUnsafe.cc
jboolean
sun::misc::Unsafe::compareAndSwapInt (jobject obj,jlong offset,jint expect,jint update)
{
jint *addr = (jint *)((char *)obj + offset);
return compareAndSwap (addr,update);
}
static inline bool
compareAndSwap (volatile jint *addr,jint old,jint new_val)
{
jboolean result = false;
spinlock lock;
if ((result = (*addr == old)))
*addr = new_val;
return result;
}
Unsafe::compareAndSwapInt调用 static 函数 compareAndSwap。而compareAndSwap又使用spinlock作为锁。这里的spinlock有LockGuard的意味,构造时加锁,析构时释放。 我们需要聚焦在spinlock里。这里是保证spinlock释放之前都是原子操作的真正实现。 什么是spinlock spinlock,即自旋锁,一种循环等待(busy waiting)以获取资源的锁。不同于mutex的阻塞当前线程、释放CPU资源以等待需求的资源,spinlock不会进入挂起、等待条件满足、重新竞争CPU的过程。这意味着只有在 等待锁的代价小于线程执行上下文切换的代价时,Spinlock才优于mutex。 natUnsafe.cc
class spinlock
{
static volatile obj_addr_t lock;
public:
spinlock ()
{
while (! compare_and_swap (&lock,1))
_Jv_ThreadYield ();
}
~spinlock ()
{
release_set (&lock,0);
}
};
以一个静态变量 static volatile obj_addr_t lock; 作为标志位,通过C++ RAII实现一个Guard,所以所谓的锁其实是 静态成员变量obj_addr_t lock,C++中volatile 并不能保证同步,保证同步的是构造函数里调用的 compare_and_swap和一个static变量lock.这个lock变量是1的时候,就需要等;是0的时候,就通过原子操作把它置为1,表示自己获得了锁。 这里会用一个static变量实在是一个意外,如此相当于所有的无锁结构都共用同一个变量(实际就是size_t)来区分是否加锁。当这个变量置为1时,其他用到spinlock的都需要等。 为什么不在sun::misc::Unsafe添加一个私有变量 volatile obj_addr_t lock;,并作为构造参数传给spinlock?这样相当于每个UnSafe共享一个标志位,效果会不会好一些? _Jv_ThreadYield在下面的文件里,通过系统调用sched_yield(man 2 sched_yield)让出CPU资源。宏HAVE_SCHED_YIELD在configure里定义,意味着编译时如果取消定义,spinlock就称为真正意义的自旋锁了。 posix-threads.h
inline void
_Jv_ThreadYield (void)
{
#ifdef HAVE_SCHED_YIELD
sched_yield ();
#endif /* HAVE_SCHED_YIELD */
}
这个lock.h在不同平台有着不同的实现,我们以ia64(Intel AMD x64)平台举例,其他的实现可以在 这里 看到。 ia64/locks.h
typedef size_t obj_addr_t;
inline static bool
compare_and_swap(volatile obj_addr_t *addr,obj_addr_t old,obj_addr_t new_val)
{
return __sync_bool_compare_and_swap (addr,old,new_val);
}
inline static void
release_set(volatile obj_addr_t *addr,obj_addr_t new_val)
{
__asm__ __volatile__("" : : : "memory");
*(addr) = new_val;
}
__sync_bool_compare_and_swap 是gcc内建函数,汇编指令"memory"完成内存屏障。
总之,硬件上保证多核CPU同步,而Unsafe的实现也是尽可能的高效。GCC-java的还算高效,相信Oracle 和 OpenJDK不会更差。 原子操作 和 GCC内建的原子操作 原子操作 Java的表达式以及C++的表达式,都不是原子操作,也就是说 你在代码里: //假设i是线程间共享的变量 i++; 在多线程环境下,i的访问是非原子性的,实际分成如下三个操作数:
编译器会改变执行的时序,因此执行结果可能并非所期望的。 GCC内建的原子操作 gcc内建了如下的原子操作,这些原子操作从4.1.2被加入。而之前,他们是使用内联的汇编实现的。 type __sync_fetch_and_add (type *ptr,type value,...) type __sync_fetch_and_sub (type *ptr,...) type __sync_fetch_and_or (type *ptr,...) type __sync_fetch_and_and (type *ptr,...) type __sync_fetch_and_xor (type *ptr,...) type __sync_fetch_and_nand (type *ptr,...) type __sync_add_and_fetch (type *ptr,...) type __sync_sub_and_fetch (type *ptr,...) type __sync_or_and_fetch (type *ptr,...) type __sync_and_and_fetch (type *ptr,...) type __sync_xor_and_fetch (type *ptr,...) type __sync_nand_and_fetch (type *ptr,...) bool __sync_bool_compare_and_swap (type *ptr,type oldval type newval,...) type __sync_val_compare_and_swap (type *ptr,...) __sync_synchronize (...) type __sync_lock_test_and_set (type *ptr,...) void __sync_lock_release (type *ptr,...) (编辑:安卓应用网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
