浅谈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,他们都有个静态变量
 private static final sun.misc.Unsafe UNSAFE;,这个类是实现原子语义的C++对象sun::misc::Unsafe的Java封装。想看看底层实现,正好我手边有gcc4.8的源代码,对照本地路径,很方便找到Github的路径,看这里。

以接口 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, expect, 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, 0, 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"完成内存屏障。

  1. 一般地,如果CPU硬件支持指令 cmpxchg (该指令从硬件保障原子性,毫无疑问十分高效),那么__sync_bool_compare_and_swap就应该是用cmpxchg来实现的。
  2. 不支持cmpxchg的CPU架构 可以用lock指令前缀,通过锁CPU总线的方式实现。
  3. 如果连lock指令都不支持,有可能通过APIC实现

总之,硬件上保证多核CPU同步,而Unsafe的实现也是尽可能的高效。GCC-java的还算高效,相信Oracle 和 OpenJDK不会更差。

原子操作 和 GCC内建的原子操作

原子操作

Java的表达式以及C++的表达式,都不是原子操作,也就是说 你在代码里:

 //假设i是线程间共享的变量
i++; 

在多线程环境下,i的访问是非原子性的,实际分成如下三个操作数:

  1. 从缓存取到寄存器
  2. 在寄存器加1
  3. 存入缓存

编译器会改变执行的时序,因此执行结果可能并非所期望的。

GCC内建的原子操作

gcc内建了如下的原子操作,这些原子操作从4.1.2被加入。而之前,他们是使用内联的汇编实现的。

 type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)

type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)

__sync_synchronize (...)

type __sync_lock_test_and_set (type *ptr, type value, ...)

void __sync_lock_release (type *ptr, ...)

需要注意的是:

  1. __sync_fetch_and_add 和 __sync_add_and_fetch 的关系 对应于 i++ 和 ++i。其他类推
  2. CAS的两种实现,bool版本的 如果对比oldval与ptr成功并给ptr设值newval 返回true;另一个 返回 原本*ptr的值
  3. __sync_synchronize 添加一个完全的内存屏障

OpenJDK 的相关文件

下面列出一些Github上 OpenJDK9的原子操作实现,希望能帮助需要了解的人。毕竟OpenJDK比Gcc的实现应用更广泛一些。————但终究没有Oracle JDK的源码,虽然据说OpenJDK与 Oracle的源码差距很小。

AtomicInteger.java

Unsafe.java::compareAndExchangeObject

unsafe.cpp::Unsafe_CompareAndExchangeObject

oop.inline.hpp::oopDesc::atomic_compare_exchange_oop

atomic_linux_x86.hpp::Atomic::cmpxchg

 inline jlong  Atomic::cmpxchg  (jlong  exchange_value, volatile jlong*  dest, jlong  compare_value, cmpxchg_memory_order order) {
 bool mp = os::is_MP();
 __asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
            : "=a" (exchange_value)
            : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
            : "cc", "memory");
 return exchange_value;
}

这里需要给不熟悉C/C++的Java程序员提示一下,嵌入汇编指令的格式如下

 __asm__ [__volatile__](assembly template//汇编模板
    : [output operand list]//输入列表
    : [input operand list]//输出列表
    : [clobber list])//破坏列表

汇编模板中的%1,%3,%4对应于后面的参数列表{"r" (exchange_value),"r" (dest),"r" (mp)},参数列表以逗号分隔,从0排序。输出参数放第一个冒号右边,输出参数放第二个冒号右边。"r"表示放到通用寄存器,"a"表示寄存器EAX,有"="表示用于输出(写还)。cmpxchg指令隐含使用EAX寄存器即参数%2.

其他细节就不在此罗列了,Gcc的实现是把要交换的指针传下来,对比成功后直接赋值(赋值非原子),原子性通过spinlock保证。

OpenJDK的实现是把要交换的指针传下来,直接通过汇编指令cmpxchgq赋值,原子性通过汇编指令保证。当然gcc的spinlock底层也是通过cmpxchgq保证的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java 并发编程学习笔记之Synchronized底层优化

    一.重量级锁 上篇文章中向大家介绍了Synchronized的用法及其实现的原理.现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的.但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的.而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因.因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为"重量级锁"

  • 浅谈Java 并发的底层实现

    并发编程的目的是让程序运行更快,但是使用并发并不定会使得程序运行更快,只有当程序的并发数量达到一定的量级的时候才能体现并发编程的优势.所以谈并发编程在高并发量的时候才有意义.虽然目前还没有开发过高并发量的程序,但是学习并发是为了更好理解一些分布式架构.那么当程序的并发量不高,比如是单线程的程序,单线程的执行效率反而比多线程更高.这又是为什么呢?熟悉操作系统的应该知道,CPU是通过给每个线程分配时间片的方式实现多线程的.这样,当CPU从一个任务切换到另一个任务的时候,会保存上一个任务的状态,当执行

  • 浅谈Java并发编程之Lock锁和条件变量

    简单使用Lock锁 Java 5中引入了新的锁机制--java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接口有3个实现它的类:ReentrantLock.ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁.读锁和写锁.lock必须被显式地创建.锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例

  • 浅谈Java并发中ReentrantLock锁应该怎么用

    目录 1.重入锁 说明 2.中断响应 说明 3.锁申请等待限时 tryLock(long, TimeUnit) tryLock() 4.公平锁 说明 源码(JDK8) 重入锁可以替代关键字 synchronized . 在 JDK5.0 的早期版本中,重入锁的性能远远优于关键字 synchronized , 但从 JDK6.0 开始, JDK 在关键字 synchronized 上做了大量的优化,使得两者的性能差距并不大. 重入锁使用 ReentrantLock 实现 1.重入锁 package

  • 浅谈Java并发编程基础知识

    进程和线程 在并行程序中进程和线程是两个基本的运行单元,在Java并发编程中,并发主要核心在于线程 1. 进程 一个进程有其专属的运行环境,一个进程通常有一套完整.私有的运行时资源:尤其是每个进程都有其专属的内存空间. 通常情况下,进程等同于运行的程序或者应用,然而很多情况下用户看到的一个应用实际上可能是多个进程协作的.为了达到进程通信的目的,主要的操作系统都实现了Inter Process Communication(IPC)资源,例如pipe和sockets,IPC不仅能支持同一个系统中的进

  • 浅谈Java并发中的内存模型

    什么是JavaMemoryModel(JMM)? JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Java开发者无需关注不同平台之间的差异,达到一次编译,随处运行的目的,这也正是Java的设计目的之一. CPU和内存 在讲JMM之前,我想先和大家聊聊硬件层面的东西.大家应该都知道执行运算操作的CPU本身是不具备存储能力的,它只负责根据指令对传递进来的数据做相应的运算,而数据存储这一任务则交给内存去完成.虽然内存的运行速度虽然比起硬盘快非常多,但是和3GHZ,4GH

  • 浅谈Java并发 J.U.C之AQS:CLH同步队列

    CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态. 在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread).状态(waitStatus).前驱节点(prev).后继节点(next),其定义如下: static final class Node

  • 浅谈Java分布式架构下如何实现分布式锁

    01分布式锁运用场景 互联网秒杀,抢优惠卷,接口幂等性校验.咱们以互联网秒杀为例. @RestController @Slf4j publicclassIndexController{ @Autowired privateRedissonredission; @Autowired privateStringRedisTemplatestringRedisTemplate; @RequestMapping("/deduct_stock") publicStringdeductStock(

  • 浅谈Java非阻塞同步机制和CAS

    什么是非阻塞同步 非阻塞同步的意思是多个线程在竞争相同的数据时候不会发生阻塞,从而能够在更加细粒度的维度上进行协调,从而极大的减少线程调度的开销,从而提升效率.非阻塞算法不存在锁的机制也就不存在死锁的问题. 在基于锁的算法中,如果一个线程持有了锁,那么其他的线程将无法进行下去.使用锁虽然可以保证对资源的一致性访问,但是在挂起和恢复线程的执行过程中存在非常大的开销,如果锁上面存在着大量的竞争,那么有可能调度开销比实际工作开销还要高. 悲观锁和乐观锁 我们知道独占锁是一个悲观锁,悲观锁的意思就是假设

  • 浅谈Java循环中的For和For-each哪个更快

    目录 for-each实现方法 基准测试 原因分析 结论 对于Java循环中的For和For-each,哪个更快 通过本文,您可以了解一些集合遍历技巧. Java遍历集合有两种方法.一个是最基本的for循环,另一个是jdk5引入的for each.通过这种方法,我们可以更方便地遍历数组和集合.但是你有没有想过这两种方法?哪一个遍历集合更有效? for-each实现方法 For-each不是一种新语法,而是Java的语法糖.在编译时,编译器将此代码转换为迭代器实现,并将其编译为字节码.我们可以通过

  • 浅谈Java多线程处理中Future的妙用(附源码)

    java 中Future是一个未来对象,里面保存这线程处理结果,它像一个提货凭证,拿着它你可以随时去提取结果.在两种情况下,离开Future几乎很难办.一种情况是拆分订单,比如你的应用收到一个批量订单,此时如果要求最快的处理订单,那么需要并发处理,并发的结果如果收集,这个问题如果自己去编程将非常繁琐,此时可以使用CompletionService解决这个问题.CompletionService将Future收集到一个队列里,可以按结果处理完成的先后顺序进队.另外一种情况是,如果你需要并发去查询一

随机推荐