资讯专栏INFORMATION COLUMN

Java - 并发 atomic, synchronization and volatile

StonePanda / 1300人阅读

摘要:线程的这种交叉操作会导致线程不安全。原子操作是在多线程环境下避免数据不一致必须的手段。如果声明一个域为一些情况就可以确保多线程访问到的变量是最新的。并发要求一个线程对对象进行了操作,对象发生了变化,这种变化应该对其他线程是可见的。

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

一个问题:

i++为什么是非线程安全的?

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

  

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.

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

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关键字,使用不变类和线程安全类。

名词解释:何为 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 类。

基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;

数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;

引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;

对象的属性修改类型: 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 head = new AtomicReference(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 类 是一种更轻量级的方法实现可见性和原子性

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64313.html

相关文章

  • BATJ都爱问的多线程面试题

    摘要:今天给大家总结一下,面试中出镜率很高的几个多线程面试题,希望对大家学习和面试都能有所帮助。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。使用可以禁止的指令重排,保证在多线程环境下也能正常运行。 下面最近发的一些并发编程的文章汇总,通过阅读这些文章大家再看大厂面试中的并发编程问题就没有那么头疼了。今天给大家总结一下,面试中出镜率很高的几个多线...

    高胜山 评论0 收藏0
  • Java并发】线程安全性

    摘要:另一个是使用锁的机制来处理线程之间的原子性。依赖于去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程对其进行操作的。 线程安全性 定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 线程安全性主要体现在三个方面:原子性、可见性...

    刘玉平 评论0 收藏0
  • 并发 - 基础

    摘要:异步非阻塞方式,任务的完成的通知由其他线程发出。并发并行死锁饥饿活锁死锁线程持有,线程持有。如等,在多线程情况下,该操作不是原子级别的而是原子的,所以一般用于状态标记。 同步/异步、阻塞/非阻塞 同步/异步是 API 被调用者的通知方式。阻塞/非阻塞则是 API 调用者的等待方式(线程挂机/不挂起)。 同步非阻塞 Future方式,任务的完成要主线程自己判断。如NIO,后台有多个任务在...

    phpmatt 评论0 收藏0
  • 猫头鹰的深夜翻译:核心JAVA并发(一)

    摘要:简介从创建以来,就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的开发人员理解核心的并发概念以及如何使用它们。请求操作系统互斥,并让操作系统调度程序处理线程停放和唤醒。 简介 从创建以来,JAVA就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的JAVA开发人员理解核心的并发概念以及如何使用它们。 (博主将在其中加上自己的理解以及自己想出的例子作为补充) 概念 ...

    Richard_Gao 评论0 收藏0
  • java并发系列 - 第21天:java中的CAS操作,java并发的基石

    摘要:方法由两个参数,表示期望的值,表示要给设置的新值。操作包含三个操作数内存位置预期原值和新值。如果处的值尚未同时更改,则操作成功。中就使用了这样的操作。上面操作还有一点是将事务范围缩小了,也提升了系统并发处理的性能。 这是java高并发系列第21篇文章。 本文主要内容 从网站计数器实现中一步步引出CAS操作 介绍java中的CAS及CAS可能存在的问题 悲观锁和乐观锁的一些介绍及数据库...

    zorro 评论0 收藏0

发表评论

0条评论

StonePanda

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<