Java - 并发 atomic, synchronization and volatile

虽是读书笔记,可是如转载请注明出处 http://segmentfault.com/blog/exploring/
.. 拒绝伸手复制党html


一个问题:

i++为何是非线程安全的?java

先来解释下什么叫“线程安全” :算法

Thread Safe describe some code that can be called from multiple threads without corrupting the state of the object or simply doing the thing the code must do in right order.segmentfault

即一段代码能够被多个线程调用,调用过程当中对象的状态不出现冲突,或者对象按照正确的顺序进行了操做。数组

i++ 线程安全是指咱们读取一个值但愿的是每一次读取到的值都是上一次+1 。缓存

i++是分为三个步骤,获取i的值;temp = i+1操做;temp写入i; 若是存在两个线程,都执行i++. 正常状况应该是线程A 先执行,获得1; 线程B再执行,获得2.安全

可是又经常出现:多线程

  • 线程A : 获取i的值;获得0;temp = i+1操做;获得i= 1;
  • 线程B : 获取i的值;获得0;temp = i+1操做;获得i= 1;
  • 线程A : i = temp 赋值 i =1 被写入;
  • 线程B :i = temp 赋值 i =1 被写入;

或者更形象的举例:线程A,B对i不停的进行操做,A执行i++, B执行打印。程序的逻辑是每次加1后就打,这样应该输出的结果是顺序的不断加1。因为i++不是原子操做,在执行的过程当中发生了线程的切换,i+1没有被回写以前就被2访问了,这时打印的仍是原来的数字,并非预期的+1。并发

线程的这种交叉操做会致使线程不安全。在Java中能够有不少方法来保证线程安全,即原子化 —— 同步,使用原子类,实现并发锁,使用volatile关键字,使用不变类和线程安全类。post

名词解释:何为 Atomic?

Atomic 一词跟原子有点关系,后者曾被人认为是最小物质的单位。计算机中的 Atomic 是指不能分割成若干部分的意思。若是一段代码被认为是 Atomic, 原子操做是指一个不受其余操做影响的操做任务单元,原子操做不能中断。原子操做是在多线程环境下避免数据不一致必须的手段。一般来讲,原子指令由硬件提供,供软件来实现原子方法(某个线程进入该方法后,就不会被中断,直到其执行完成)

如何实现原子操做

为了解决这个问题,必须保证增长操做是原子的,在 JDK1.5 以前咱们可使用同步技术(synchonized关键字, 锁)来作到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和 long 类型的装类,它们能够自动的保证对于他们的操做是原子的而且不须要使用同步。

同步技术/锁 :synchronized 关键字修饰,给方法自动获取和释放锁

public class Example {
    private int value = 0;    

    public synchronized int getNextValue(){
        return value++;
    }
}

或者

public class Example {
    private int value = 0;

    public int getNextValue() {
        synchronized (this) {
            return value++;
        }
    }
}

或者想对其余对象加锁,而非当前对象

public class Example {
    private int value = 0;

    private final Object lock = new Object();

    public int getNextValue() {
        synchronized (lock) {
            return value++;
        }
    }
}

Volatile

关键词:可见性

  • 当对非volatile变量进行读写的时候,每一个线程先从内存拷贝变量到CPU缓存中。若是计算机有多个CPU,每一个线程可能在不一样的CPU上被处理,这意味着每一个线程能够考虑到不一样的CPU cache中。
    图片描述
    而声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。
  • 编译器能够改变指令执行的顺序以使吞吐量最大化,这种顺序上的便会致使内存的值不一样步。

volatile关键字为实例域的同步访问提供了一种免锁机制。若是声明一个域为volatile. 一些状况就能够确保多线程访问到的变量是最新的。(并发要求)

javapublic class SharedObject{
    public volatile int counter = 0;
    }

The problem with multiple threads that do not see the latest value of a variable because that value has not yet been written back to main memory by another thread, is called a "visibility" problem. The updates of one thread are not visible to other threads.

一个线程对对象进行了操做,对象发生了变化,这种变化应该对其余线程是可见的。可是默认对这点没有任何保障。因此咱们使用了Synchonized. 另外一种方法是使用volatile关键字确保多线程对对象读写的可见性(可是只是在某些状况能够保证同步,好比一个线程读,而后写在了volatile变量上,其余线程只是进行读操做; 若是多个线程都进行读写,那么就必定要在用synchronized)。volatile只确保了可见性,并不能确保原子性。

当咱们使用 volatile 关键字去修饰变量的时候,因此线程都会直接读取该变量而且不缓存它。这就确保了线程读取到的变量是同内存中是一致的

原子操做类

几乎 java.util.concurrent 包中的全部类都使用原子变量,而不使用同步。缘由是 同步(lock)机制并非一个轻量级的操做,它存在一些缺点。缺点以下

via Baptiste Vicht

When several threads try to acquire the same lock, one or more threads will be suspended and they will be resumed later. When the critical section is little, the overhead is really heavy especially when the lock is often acquired and there is a lot of contention. Another disadvantage is that the other threads waiting of the lock cannot do something else during waiting and if the thread who has the lock is delayed (due to a page fault or the end of the time quanta by example), the others threads cannot take their turn.

JUC这包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具备排他性,即当某个线程进入方法,执行其中的指令时,不会被其余线程打断,而别的线程就像自旋锁同样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另外一个线程进入,这只是一种逻辑上的理解。其实是借助硬件的相关指令来实现的,不会阻塞线程 (或者说只是在硬件级别上阻塞了)。

根据修改的数据类型,能够将 JUC 包中的原子操做类能够分为 4 类。

  1. 基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
  2. 数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
  3. 引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
  4. 对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。

这些类都是基于CAS实现的。处理器提供了CAS操做来实现非加锁的原子操做。

引用《Java Concurrency in Practice》里的一段描述:

在这里,CAS 指的是现代 CPU 普遍支持的一种对内存中的共享数据进行操做的一种特殊指令。这个指令会对内存中的共享数据作原子的读写操做。简单介绍一下这个指令的操做过程:首先,CPU 会将内存中将要被更改的数据与指望的值作比较。而后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。不然便不作操做。最后,CPU 会将旧的数值返回。这一系列的操做是原子的。它们虽然看似复杂,但倒是 Java 5 并发机制优于原有锁机制的根本。简单来讲,CAS 的含义是 “我认为原有的值应该是什么,若是是,则将原有的值更新为新值,不然不作修改,并告诉我原来的值是多少”。
CSA的优势:Compare and Set 是一个非阻塞的算法,这是它的优点。由于使用的是 CPU 支持的指令,提供了比原有的并发机制更好的性能和伸缩性。能够认为通常状况下性能更好,而且也更容易使用

使用原子类实现i++方法

public class AtomicCounter {
    private final AtomicInteger value = new AtomicInteger(0);

    public int getValue(){
        return value.get();
    }

    public int getNextValue(){
        return value.incrementAndGet();
    }

    public int getPreviousValue(){
        return value.decrementAndGet();
    }
}

一个线程安全的栈

public class Stack {
    private final AtomicReference<Element> head = new AtomicReference<Element>(null);

    public void push(String value){
        Element newElement = new Element(value);

        while(true){
            Element oldHead = head.get();
            newElement.next = oldHead;

            //Trying to set the new element as the head
            if(head.compareAndSet(oldHead, newElement)){
                return;
            }
        }
    }

    public String pop(){
        while(true){
            Element oldHead = head.get();

            //The stack is empty
            if(oldHead == null){
                return null;
            }

            Element newHead = oldHead.next;

            //Trying to set the new element as the head
            if(head.compareAndSet(oldHead, newHead)){
                return oldHead.value;
            }
        }
    }

    private static final class Element {
        private final String value;
        private Element next;

        private Element(String value) {
            this.value = value;
        }
    }
}

总结

总结说来,synchronized 实现的同步能确保线程安全,实现可见性和原子性;可是代价大,效率低,更慢;
volatile 可以实现多线程操做产生变化的可见性,可是不能实现原子性。
atomic 类 是一种更轻量级的方法实现可见性和原子性

想更一进步的支持我,请扫描下方的二维码,你懂的~
图片描述

相关文章
相关标签/搜索