高并发第三弹:线程安全-原子性

线程安全性?

感谢 [原子性]https://blog.csdn.net/fanrenxiang/article/details/80623884

线程安全性主要体现在三个方面:原子性、可见性、有序性

  • 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
  • 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

本章主要关注一下原子性的方面

说到原子性,一共有两个方面需要学习一下,一个是JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作,另一个是使用锁的机制来处理线程之间的原子性。锁包括:synchronized、Lock

Atomic包中的类与CAS(compareAndSwap):

atomic包下提供了AtomicBoolean/AtomicLong/AtomicInteger三个原子更新基本类型,以AtomicInteger为例,其他两种基本类似。以下是AtomicInteger囊括的大致方法

  1. public final int getAndSet(int newValue) //给AtomicInteger设置newValue并返回加oldValue
  2. public final boolean compareAndSet(int expect, int update) //如果输入的值和期望值相等就set并返回true/false
  3. public final int getAndIncrement() //对AtomicInteger原子的加1并返回当前自增前的value
  4. public final int getAndDecrement() //对AtomicInteger原子的减1并返回自减之前的的value
  5. public final int getAndAdd(int delta) //对AtomicInteger原子的加上delta值并返加之前的value
  6. public final int incrementAndGet() //对AtomicInteger原子的加1并返回加1后的值
  7. public final int decrementAndGet() //对AtomicInteger原子的减1并返回减1后的值

public final int addAndGet(int delta) //给AtomicInteger原子的加上指定的delta值并返回加后的值

采用的是incrementAndGet方法,此方法的源码中调用了一个名为unsafe.getAndAddInt的方法

public final int incrementAndGet() {
     return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

而getAndAddInt方法的具体实现为:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

在此方法中,方法参数为要操作的对象Object var1、期望底层当前的数值为var2、要修改的数值var4。定义的var5为真正从底层取出来的值。采用do..while循环的方式去获取底层数值并与期望值进行比较,比较成功才将值进行修改。而这个比较再进行修改的方法就是compareAndSwapInt就是我们所说的CAS,它是一系列的接口,比如下面罗列的几个接口。使用native修饰,是底层的方法。CAS取的是compareAndSwap三个单词的首字母.

另外,示例代码中的count可以理解为JMM中的工作内存,而这里的底层数值即为主内存,如果看过我上一篇文章的盆友就能把这一块的知识点串联起来了。

AtomicLong 与 LongAdder

LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。 
??缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。

AtomicBoolean

这个类中值得一提的是它包含了一个名为compareAndSet的方法,这个方法可以做到的是控制一个boolean变量在一件事情执行之前为false,事情执行之后变为true。或者也可以理解为可以控制某一件事只让一个线程执行,并仅能执行一次。

AtomicIntegerFieldUpdater

这个类的核心作用是要更新一个指定的类的某一个字段的值。并且这个字段一定要用volatile修饰同时还不能是static的。

ABA问题

AtomicStampReference与CAS的ABA问题

ABA : 其实 比如  A想变成B,那么内存中的预期值就A,过去看还真是A,然后就交换;但是内存中的 A有可能是别的线程进行修改了的.A-->C-->D-->A.这种就和最初的设计思想不符.那么就加入了AtomicStampReference。

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

private volatile Pair<V> pair;

private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&   //排除新的引用和新的版本号与底层的值相同的情况
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
}

AtomicStampReference的处理思想是,每次变量更新的时候,将变量的版本号+1,之前的ABA问题中,变量经过两次操作以后,变量的版本号就会由1变成3,也就是说只要线程对变量进行过操作,变量的版本号就会发生更改。从而解决了ABA问题。

解释一下上边的源码: 
类中维护了一个volatile修饰的Pair类型变量current,Pair是一个私有的静态类,current可以理解为底层数值。 
compareAndSet方法的参数部分分别为期望的引用、新的引用、期望的版本号、新的版本号。 
return的逻辑为判断了期望的引用和版本号是否与底层的引用和版本号相符,并且排除了新的引用和新的版本号与底层的值相同的情况(即不需要修改)的情况(return代码部分3、4行)。条件成立,执行casPair方法,调用CAS操作。

AtomicLongArray

这个类实际上维护了一个Array数组,我们在对数值进行更新的时候,会多一个索引值让我们更新。

原子性,提供了互斥访问,同一时刻只能有一个线程来对它进行操作。那么在java里,保证同一时刻只有一个线程对它进行操作的,除了Atomic包之外,还有锁的机制。JDK提供锁主要分为两种:synchronized和Lock。接下来我们了解一下synchronized。

synchronized

依赖于JVM去实现锁,因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程对其进行操作的。 
synchronized是java中的一个关键字,是一种同步锁。它可以修饰的对象主要有四种:

修饰代码块:大括号括起来的代码,作用于调用的对象

修饰方法:整个方法,作用于调用的对象

修饰静态方法:整个静态方法,作用于所有对象

修饰类:括号括起来的部分,作用于所有对象      

具体的我就不写了 网上太多了 

这里有个有趣的测试synchronized锁住的是代码还是对象

这个是很形象的写了  读后解读Synchronized下的三种锁:偏向锁  轻量锁  重量锁

Lock我需要整理 下次再说吧.

因为现在 synchronized现在加入了多种优化手段.其实效率来说 不低了.如果不能判断用Lock和synchronized  那就用synchronized吧

复制一份总结

原子性操作各方法间的对比

  • synchronized:不可中断锁,适合竞争不激烈,可读性好   (其实我觉得现在效率可以了,可以直接用)
  • Lock:可中断锁,多样化同步,竞争激烈时能维持常态
  • Atomic:竞争激烈时能维持常态,比Lock性能好,每次只能同步一个值

原文地址:https://www.cnblogs.com/aihuxi/p/9665064.html

时间: 2024-07-29 09:34:40

高并发第三弹:线程安全-原子性的相关文章

并发与高并发(八)-线程安全性-原子性-synchronized

前言 闲暇时刻,谈一下曾经在多线程教程中接触的同步锁synchronized,相当于复习一遍吧. 主要介绍 synchronized:依赖JVM Lock:依赖特殊的CPU指令,代码实现,ReetrantLock 主体内容 一.那么我们主要先讲解一下关于同步锁synchronized的作用范围. 1.修饰代码块:作用范围-大括号括起来的代码,作用于调用这个代码块的对象,如果不同对象调用该代码块就不会同步. 2.修饰方法:作用范围-整个方法,作用于调用这个方法的对象. 3.修饰静态方法:作用范围-

聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则

在前几篇将Java内存模型的那些事基本上把这个域底层的概念都解释清楚了,聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障 这篇分析了在X86平台下,volatile,synchronized, CAS操作都是基于Lock前缀的汇编指令来实现的,关于Lock指令有两个要点: 1. lock会锁总线,总线是互斥的,所以lock后面的写操作会写入缓存和内存,可以理解为在lock后面的写缓存和写内存这两个动作称为了一个原子操作.当总线被锁时,其他的CPU是无法使用总线的,也就让其他的读写都等

聊聊高并发(三十五)理解内存屏障

在聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了硬件层提供了满足某些一致性需求的能力,Java内存模型利用了硬件层提供的能力指定了一系列的语法和规则,让Java开发者可以隔绝这种底层的实现专注于并发逻辑的开发.这篇我们来看看硬件层是如何提供这些实现一致性需求的能力的. 硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力.拿X86平台来说,有几种主要的内存屏障 1. ifence

聊聊高并发(三十二)实现一个基于链表的无锁Set集合

Set表示一种没有反复元素的集合类,在JDK里面有HashSet的实现,底层是基于HashMap来实现的.这里实现一个简化版本号的Set,有下面约束: 1. 基于链表实现.链表节点依照对象的hashCode()顺序由小到大从Head到Tail排列. 2. 如果对象的hashCode()是唯一的.这个如果实际上是不成立的,这里为了简化实现做这个如果.实际情况是HashCode是基于对象地址进行的一次Hash操作.目的是把对象依据Hash散开.所以可能有多个对象地址相应到一个HashCode.也就是

Java并发编程三个性质:原子性、可见性、有序性

并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确 线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的CPU调度顺序,线程个数.指令重排的影响,偶然触发 线程安全的定义 比如说一个类,不论通过怎样的调度执行顺序,并且调用处不用对其进行同步操作,其都能表现出正确的行为,则这个类就是线程安全的 并发编程三个概念 原子性: 一个操作或多个操作要么全部执行且执行过程不被中断,要么不执行 可见性: 多个线程修改同一

高并发第八弹:J.U.C起航(java.util.concurrent)

java.util.concurrent是JDK自带的一个并发的包主要分为以下5部分: 并发工具类(tools) 显示锁(locks) 原子变量类(aotmic) 并发集合(collections) Executor线程执行器 我们今天就说说 并发集合,除开 Queue,放在线程池的时候讲 先介绍以下 CopyOnWrite: Copy-On-Write简称COW,是一种用于程序设计中的优化策略.其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy

谈论高并发(三)锁的一些基本概念

并发编程概念的一些基本的了解是非常重要的.告诉我们认为表明,在方向上的主要问题. 这个讲锁的一些基本概念. 在正常情况下,我们说的锁都指的是"互斥"锁.因为有一些特殊的锁,例"读写锁",一点都没有互斥. 排斥锁. 锁是处理并发的一种同步手段.单线程程序和并发程序的终于目的都是要保证程序的正确性,可是最大的差别是: 单线程程序的正确性仅仅关注程序的执行结果和目标是一致的 并发程序的正确性除了执行结果正确外,还包括了活性的特性,所谓活性,指的就是程序无死锁,无饥饿 所以

聊聊高并发(三)锁的一些基本概念

理解并发编程的一些基本概念很重要,给我们思考问题指明一个基本的方向.这篇说一说锁的一些基本概念. 在通常情况下我们说的锁都指的是"互斥"锁,因为在还存在一些特殊的锁,比如"读写锁",不完全是互斥的.这篇文章说的锁专指互斥锁. 锁是处理并发的一种同步手段.单线程程序和并发程序的最终目的都是要保证程序的正确性,但是最大的区别是: 单线程程序的正确性只关注程序的运行结果和目标是一致的 并发程序的正确性除了运行结果正确外,还包含了活性的特性,所谓活性,指的就是程序无死锁,无

谈论高并发(三十)解析java.util.concurrent各种组件(十二) 认识CyclicBarrier栅栏

这次谈话CyclicBarrier栅栏,如可以从它的名字可以看出,它是可重复使用. 它的功能和CountDownLatch类别似,也让一组线程等待,然后开始往下跑起来.但也有在两者之间有一些差别 1. 不同的对象等.CountDownLatch组线程等待的是一个事件.或者说是一个计数器归0的事件.而CyclicBarrier等待的对象是线程,仅仅有线程都到齐了才往下运行 2. 使用方式不同,这个也是由等待的对象不同引起的,CountDownLatch须要调用await()来让线程等待.调用cou