Java并发

一,初衷

一直以来,对java并发的认识,仅仅停留在多线程的使用,以及synchronized等宏观层面上,但对于底层实现却毫无认识,这使得在使用并发解决问题时,不能游刃有余知根知底。于是花了点时间研究这个专题,并梳理成文,以求更为深刻的认识。

二,并发和并行

  • 并发(Concurrency)

并发是指,多个任务的启动,执行,停止在时间周期上是重叠交叉而不是线性的,比如A任务执行期间,B任务可能已经进入结束周期。并发并不意味着这两个任务就一定是在同一瞬间执行着,比如在单核cpu机器上的两个任务在某个时间片内,只能有一个任务执行。

  • 并行(Parallelism)

并行一定是指两个任务在多个cpu上,同一瞬间都执行着

参考地址

三,并发的目的和困难

所有的并发其目的,都是为了提高对受限资源(比如cpu资源)高效使用。无论何种方式的并发应该都面临两个困难

  • 如何防止并发导致的共享数据混乱
  • 如何最大限度的提高并发效率

Java中采用多线程来实现并发。

四,如何防止并发导致的数据混乱

为了防止数据混乱,首先要了解导致并发数据混乱的原因。导致数据混乱的原因也有二

  • 没有对共享资源的顺序访问控制
  • 数据可见性不一致导致

比如代码

`public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }`

如果两个线程同时读到counter==1000,那么它们势必都会执行一次counter=0。但是在写入ram时,总有一个是先写一个是后写。先写入的线程成功后,counter已经不是1000了,但后写入的线程却再次在counter等于1000的基础上将其写为0,这显然是不对的。这即是对共享代码undateCounter没有控制顺序访问带来的数据混乱

同样,如果两个线程是顺序访问该段代码,线程A先访问,它从其cpu缓存中读出的counter为1000,再将counter设为0,但并没有将其及时写回RAM(内存),或者即便已写入RAM,线程b从另外一个cpu缓存中读出的counter依然为1000,这时线程b还是会执行一次counter=0的代码。这既是,线程a,b由于cpu缓存的存在,导致对counter修改彼此不可见带来的数据混乱

顺带提下底层计算机计算模型,如图:

cpu寄存器从ram中读取数据进行计算,完成后将其再写入ram。现代cpu引入多核概念,并且为了提高访问速度,引入了cpu缓存层,这便可能导致上面提到的数据可见性问题。

五,如何解决数据可见性问题?——volatile

通过对变量加volatile声明,来使cpu对数据的存取绕过cpu缓存,直接操作RAM。java底层使用了write-happen-before-read规则,保证对volatile变量的读取不光是直接从ram读最新的数据,并且在读的过程中,任何对volatile变量的写操作都会先于read进行,保证read始终读取的是最新值。通过这种机制,保证了任意对volatile变量的写操作,对所有线程都是可见的。

六,如何控制共享资源的顺序访问?——原子操作

原子操作:一个操作要么成功,要么失败,没有中间状态,且完成之前,其对数据的更改对其它操作者不可见。

我理解为,保证不受干扰(包括不受其它线程对数据操作带来的干扰),不可拆解的最小执行单元,叫做原子操作

java中对引用变量和除开long和double之外的所有原始类型变量的读写都是原子操作。

对volatile声明的所有变量的读写都是原子操作(包括long和double)

值得再次强调的是,只是对变量的读或者写是原子操作,这可以理解为计算机底层保证了某一瞬间对ram某地址的访问只有一个指令独享操作(不可能并行的对同一块ram地址空间进行写,否则数据将混乱)。但诸如i++这中表达式则不是原子操作,因为它包含了read-update-write三步操作(先从内存中读出i 叫read,再对i加1叫update,然后再将其写入ram叫write)。

大多数业务都可以拆解成read-update-write这三步(例如,按条件read到数据,然后根据一定的业务逻辑将其update,最后再write回数据库)。如果我们能控制好read-update-write的原子性,那么就能解决并发来的数据混乱,那我们如何来保证read-update-write的原子性呢(计算机底层是控制不了了)?

在java中,我们可以通加锁来实现(比如synchronized或lock)——悲观锁

例如对于i++,用以下代码来实现原子操作:

`    public synchronized void  increment() {
        i = i +1;
    }`

假若两线程同时执行该代码,那么最先获得锁的线程将会执行该段代码,并保证数据最终写回到RAM(而不是cpu缓存中),但再未执行完成之前,后续尝试获取该锁的线程都将被block。也既是说,这是一种悲观锁,如果锁的粒度过大,那并发的效率会很低。对于这种变量的简单read-update-write,jdk从1.5开始提供了更为高效的并发实现,他们被放在java.util.concurrent.atomic包中

java.util.concurrent.atomic——乐观锁

该包中提供了对各种变量做最基本read-update-write操作的原子性保证,以AtomicInteger类为例。首先其内部使用了一个volatile int来保证该整形数据在多线程之间的可见性

`private volatile int value;`

这样对该数据读时,能保证高效,且始终最新

`    public final int get() {
        return value;
    }`

累加的实现代码如下:

`    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }`

可以看到,对value进行累加时,每次先取出内存中最新的数据,假设为1,对其加1,将2放在next变量中,而在将next写回内存之前,value可能已经被其它线程累加至3了,如果直接将next中的2写回,会导致其余线程的写丢失。java底层使用了compare and swap(简称CAS)机制来解决该问题:在最终写入内存时,先比较内存中最新的值,同累加之前读出来的value是否一致,不一致则写失败,循环重读再累加,再compare and set ,直到成功返回。

java中最终执行内存层面的CAS操作是由sun.misc.Unsafe类来实现。

可以看到compare and swap机制同hibernate的乐观锁version机制类似。这种方式不会阻塞其它线程,也没有锁获取和释放的开销,相对高效很多。该包中还提供了AtomicReference类来保证非原始类型变量的原子操作

转载请注明出处