龙空技术网

2000字讲透JDK源码剖析之多线程基础——final关键字/无锁编程

程序员高级码农II 78

前言:

现时小伙伴们对“java无锁化编程”大约比较讲究,看官们都想要知道一些“java无锁化编程”的相关文章。那么小编也在网络上搜集了一些关于“java无锁化编程””的相关知识,希望大家能喜欢,大家一起来学习一下吧!

final关键字-- 构造函数溢出问题

考虑下面的代码:

答案是:a,b未必一定等于1,2。和DCL的例子类似,也就是构造函数溢出问题。obj=new Example()这行代码,分解成三个操作:

① 分配一块内存;

② 在内存上初始化i=1,j=2;

③ 把obj指向这块内存。操作②和操作③可能重排序,因此线程B可能看到未正确初始化的值。对于构造函数溢出,通俗来讲,就是一个对象的构造并不是“原子的”,当一个线程正在构造对象时,另外一个线程却可以读到未构造好的“一半对象”。

final的happen-before语义

要解决这个问题,不止有一种办法。

办法1:给i,j都加上volatile关键字。

办法2:为read/write函数都加上synchronized关键字。

如果i,j只需要初始化一次,则后续值就不会再变了,还有办法3,为其加上final关键字。

之所以能解决问题,是因为同volatile一样,final关键字也有相应的happen-before语义:

(1)对final域的写(构造函数内部),happen-before于后续对final域所在对象的读。

(2)对final域所在对象的读,happen-before于后续对final域的读。

通过这种happen-before语义的限定,保证了final域的赋值,一定在构造函数之前完成,不会出现另外一个线程读取到了对象,但对象里面的变量却还没有初始化的情形,避免出现构造函数溢出的问题。

关于final和volatile的特性与背后的原理,到此为止就讲完了,在后续Concurrent包的源码分析中会反复看到这两个关键字的身影。

接下来总结常用的几个happen-before规则。

happen-before规则总结

(1)单线程中的每个操作,happen-before于该线程中任意后续操作。

(2)对volatile变量的写,happen-before于后续对这个变量的读。

(3)对synchronized的解锁,happen-before于后续对这个锁的加锁。

(4)对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的读。

四个基本规则再加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外的部分,程序都可能被重排序,都需要开发者小心地处理内存可见性问题。

图1-9表示了volatile背后的原理。

图1-9从底向上看volatile背后的原理

综合应用:无锁编程

提到多线程编程,就绕不开“锁”,在Java中就是指synchronized关键字和Lock。在Linux中,主要是指pthread的mutex。但锁又是性能杀手,所以很多的前辈大师们研究如何可以不用锁,也能实现线程安全。无锁编程是一个庞大而深入的话题,既涉及底层的CPU架构(例如前面讲的内存屏障),又涉及不同语言的具体实现。在

一写一读的无锁队列:内存屏障

一写一读的无锁队列即Linux内核的kfifo队列,一写一读两个线程,不需要锁,只需要内存屏障。

一写多读的无锁队列:volatile关键字

在Martin Fowler关于LMAX架构的介绍中,谈到了Disruptor。Disruptor是一个开源的并发框架,能够在无锁的情况下实现Queue并发操作。

Disruptor的RingBuffer之所以可以做到完全无锁,也是因为“单线程写”,这是“前提的前提”。离开了这个前提条件,没有任何技术可以做到完全无锁。借用Disruptor官方提到的一篇博客文章Sharing Data Among Threads Without Contention,也就是single-writerprinciple。

在这个原则下,利用 volatile 关键字可以实现一写多读的线程安全。具体来说,就是RingBuffer有一个头指针,对应一个生产者线程;多个尾指针对应多个消费者线程。每个消费者线程只会操作自己的尾指针。所有这些指针的类型都是volatile变量,通过头指针和尾指针的比较,判断队列是否为空。

多写多读的无锁队列:CAS

同内存屏障一样,CAS(Compare And Set)也是CPU提供的一种原子指令。在第2章中会对CAS进行详细的解释。

基于CAS和链表,可以实现一个多写多读的队列。具体来说,就是链表有一个头指针head和尾指针tail。入队列,通过对tail进行CAS操作完成;出队列,对head进行CAS操作完成。

在第3章讲Lock的实现的时候,将反复用到这种队列,会详细展开介绍。

无锁栈

无锁栈比无锁队列的实现更简单,只需要对 head 指针进行 CAS操纵,就能实现多线程的入栈和出栈。

在第4章讲工具类的实现的时候,会用到无锁栈。

无锁链表

相比无锁队列与无锁栈,无锁链表要复杂得多,因为无锁链表要在中间插入和删除元素。

在后面,介绍ConcurrentSkipListMap实现的时候,会讲到并发的跳查表。其实现就是基于无锁链表的,到时会详细展开论述。

本篇给大家讲解的内容是JDK源码剖析之多线程基础——final关键字/无锁编程

下篇文章给大家介绍的内容是JDK源码剖析之Atomic类——AtomicInteger和AtomicLong

标签: #java无锁化编程